feat: Add persistent settings and multiple layouts support
- Add settings manager to persist symbol, timeframe, and layouts - Support multiple layouts for comprehensive chart analysis - Remove debug screenshots for cleaner logs - Update AI analysis with professional trading prompt - Add multi-screenshot analysis for better trading insights - Update analyze API to use saved settings and multiple layouts
This commit is contained in:
51
Dockerfile
Normal file
51
Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Dockerfile for Next.js 15 + Puppeteer/Chromium + Prisma + Tailwind + OpenAI
|
||||||
|
FROM node:20-slim
|
||||||
|
|
||||||
|
# Install system dependencies for Chromium
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
wget \
|
||||||
|
ca-certificates \
|
||||||
|
fonts-liberation \
|
||||||
|
libappindicator3-1 \
|
||||||
|
libasound2 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libcups2 \
|
||||||
|
libdbus-1-3 \
|
||||||
|
libdrm2 \
|
||||||
|
libgbm1 \
|
||||||
|
libnspr4 \
|
||||||
|
libnss3 \
|
||||||
|
libx11-xcb1 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxdamage1 \
|
||||||
|
libxrandr2 \
|
||||||
|
xdg-utils \
|
||||||
|
--no-install-recommends && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Chromium (Debian 12+ uses 'chromium' instead of 'chromium-browser')
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
chromium \
|
||||||
|
--no-install-recommends && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install dependencies
|
||||||
|
COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* .npmrc* ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy the rest of the app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Set environment variables for Puppeteer
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
|
|
||||||
|
# Start the app
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
53
app/api/analyze/route.ts
Normal file
53
app/api/analyze/route.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { aiAnalysisService } from '../../../lib/ai-analysis'
|
||||||
|
import { tradingViewCapture } from '../../../lib/tradingview'
|
||||||
|
import { settingsManager } from '../../../lib/settings'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { symbol, layouts, timeframe } = await req.json()
|
||||||
|
|
||||||
|
// Load current settings
|
||||||
|
const settings = await settingsManager.loadSettings()
|
||||||
|
|
||||||
|
// Use provided values or fall back to saved settings
|
||||||
|
const finalSymbol = symbol || settings.symbol
|
||||||
|
const finalTimeframe = timeframe || settings.timeframe
|
||||||
|
const finalLayouts = layouts || settings.layouts
|
||||||
|
|
||||||
|
if (!finalSymbol) {
|
||||||
|
return NextResponse.json({ error: 'Missing symbol' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseFilename = `${finalSymbol}_${finalTimeframe}_${Date.now()}`
|
||||||
|
const screenshots = await tradingViewCapture.capture(finalSymbol, `${baseFilename}.png`, finalLayouts, finalTimeframe)
|
||||||
|
|
||||||
|
let result
|
||||||
|
if (screenshots.length === 1) {
|
||||||
|
// Single screenshot analysis
|
||||||
|
const filename = path.basename(screenshots[0])
|
||||||
|
result = await aiAnalysisService.analyzeScreenshot(filename)
|
||||||
|
} else {
|
||||||
|
// Multiple screenshots analysis
|
||||||
|
const filenames = screenshots.map(screenshot => path.basename(screenshot))
|
||||||
|
result = await aiAnalysisService.analyzeMultipleScreenshots(filenames)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return NextResponse.json({ error: 'Analysis failed' }, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
...result,
|
||||||
|
settings: {
|
||||||
|
symbol: finalSymbol,
|
||||||
|
timeframe: finalTimeframe,
|
||||||
|
layouts: finalLayouts
|
||||||
|
},
|
||||||
|
screenshots: screenshots.map(s => path.basename(s))
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
return NextResponse.json({ error: e.message }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/api/auto-trading/route.ts
Normal file
33
app/api/auto-trading/route.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getAutoTradingService } from '../../../lib/auto-trading'
|
||||||
|
|
||||||
|
const autoTradingService = getAutoTradingService()
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { action, config } = await req.json()
|
||||||
|
if (action === 'start') {
|
||||||
|
autoTradingService.start()
|
||||||
|
return NextResponse.json({ status: 'started' })
|
||||||
|
}
|
||||||
|
if (action === 'stop') {
|
||||||
|
autoTradingService.stop()
|
||||||
|
return NextResponse.json({ status: 'stopped' })
|
||||||
|
}
|
||||||
|
if (action === 'config' && config) {
|
||||||
|
autoTradingService.setConfig(config)
|
||||||
|
return NextResponse.json({ status: 'config updated', config: autoTradingService })
|
||||||
|
}
|
||||||
|
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
||||||
|
} catch (e: any) {
|
||||||
|
return NextResponse.json({ error: e.message }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
// Return current config/status
|
||||||
|
return NextResponse.json({
|
||||||
|
config: autoTradingService,
|
||||||
|
running: !!autoTradingService['intervalId']
|
||||||
|
})
|
||||||
|
}
|
||||||
15
app/api/screenshot/route.ts
Normal file
15
app/api/screenshot/route.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { tradingViewCapture } from '../../../lib/tradingview'
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { symbol, filename } = await req.json()
|
||||||
|
if (!symbol || !filename) {
|
||||||
|
return NextResponse.json({ error: 'Missing symbol or filename' }, { status: 400 })
|
||||||
|
}
|
||||||
|
const filePath = await tradingViewCapture.capture(symbol, filename)
|
||||||
|
return NextResponse.json({ filePath })
|
||||||
|
} catch (e: any) {
|
||||||
|
return NextResponse.json({ error: e.message }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
14
app/api/trading-history/route.ts
Normal file
14
app/api/trading-history/route.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import prisma from '../../../lib/prisma'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const trades = await prisma.trade.findMany({
|
||||||
|
orderBy: { executedAt: 'desc' },
|
||||||
|
take: 50
|
||||||
|
})
|
||||||
|
return NextResponse.json(trades)
|
||||||
|
} catch (e: any) {
|
||||||
|
return NextResponse.json({ error: e.message }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/api/trading/route.ts
Normal file
21
app/api/trading/route.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { driftTradingService } from '../../../lib/drift-trading'
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const params = await req.json()
|
||||||
|
const result = await driftTradingService.executeTrade(params)
|
||||||
|
return NextResponse.json(result)
|
||||||
|
} catch (e: any) {
|
||||||
|
return NextResponse.json({ error: e.message }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const positions = await driftTradingService.getPositions()
|
||||||
|
return NextResponse.json({ positions })
|
||||||
|
} catch (e: any) {
|
||||||
|
return NextResponse.json({ error: e.message }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
7
app/globals.css
Normal file
7
app/globals.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
|
}
|
||||||
19
app/layout.tsx
Normal file
19
app/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import './globals.css'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Trading Bot Dashboard',
|
||||||
|
description: 'AI-powered trading bot dashboard with auto-trading, analysis, and developer tools.'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className="bg-gray-950 text-gray-100 min-h-screen">
|
||||||
|
<main className="max-w-5xl mx-auto py-8">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
app/page.tsx
Normal file
11
app/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import AIAnalysisPanel from '../components/AIAnalysisPanel'
|
||||||
|
import Dashboard from '../components/Dashboard'
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AIAnalysisPanel />
|
||||||
|
<Dashboard />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
app/screenshots/[filename]/route.ts
Normal file
22
app/screenshots/[filename]/route.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
context: any
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const screenshotsDir = path.join(process.cwd(), 'screenshots')
|
||||||
|
const filePath = path.join(screenshotsDir, context.params.filename)
|
||||||
|
const file = await fs.readFile(filePath)
|
||||||
|
return new NextResponse(file, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'image/png',
|
||||||
|
'Content-Disposition': `inline; filename="${context.params.filename}"`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
return NextResponse.json({ error: e.message }, { status: 404 })
|
||||||
|
}
|
||||||
|
}
|
||||||
106
components/AIAnalysisPanel.tsx
Normal file
106
components/AIAnalysisPanel.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use client"
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim())
|
||||||
|
const timeframes = [
|
||||||
|
{ label: '1m', value: '1' },
|
||||||
|
{ label: '5m', value: '5' },
|
||||||
|
{ label: '15m', value: '15' },
|
||||||
|
{ label: '1h', value: '60' },
|
||||||
|
{ label: '4h', value: '240' },
|
||||||
|
{ label: '1d', value: 'D' },
|
||||||
|
{ label: '1w', value: 'W' },
|
||||||
|
{ label: '1M', value: 'M' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function AIAnalysisPanel() {
|
||||||
|
const [symbol, setSymbol] = useState('BTCUSD')
|
||||||
|
const [layout, setLayout] = useState(layouts[0])
|
||||||
|
const [timeframe, setTimeframe] = useState('60')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [result, setResult] = useState<any>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
async function handleAnalyze() {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
setResult(null)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/analyze', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ symbol, layout, timeframe })
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Unknown error')
|
||||||
|
setResult(data)
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-900 rounded-lg shadow p-6 mb-8">
|
||||||
|
<h2 className="text-xl font-bold mb-4 text-white">AI Chart Analysis</h2>
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<input
|
||||||
|
className="input input-bordered flex-1"
|
||||||
|
value={symbol}
|
||||||
|
onChange={e => setSymbol(e.target.value)}
|
||||||
|
placeholder="Symbol (e.g. BTCUSD)"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className="input input-bordered"
|
||||||
|
value={layout}
|
||||||
|
onChange={e => setLayout(e.target.value)}
|
||||||
|
>
|
||||||
|
{layouts.map(l => <option key={l} value={l}>{l}</option>)}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className="input input-bordered"
|
||||||
|
value={timeframe}
|
||||||
|
onChange={e => setTimeframe(e.target.value)}
|
||||||
|
>
|
||||||
|
{timeframes.map(tf => <option key={tf.value} value={tf.value}>{tf.label}</option>)}
|
||||||
|
</select>
|
||||||
|
<button className="btn btn-primary" onClick={handleAnalyze} disabled={loading}>
|
||||||
|
{loading ? 'Analyzing...' : 'Analyze'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-400 mb-2">
|
||||||
|
{error.includes('frame was detached') ? (
|
||||||
|
<>
|
||||||
|
TradingView chart could not be loaded. Please check your symbol and layout, or try again.<br />
|
||||||
|
<span className="text-xs">(Technical: {error})</span>
|
||||||
|
</>
|
||||||
|
) : error.includes('layout not found') ? (
|
||||||
|
<>
|
||||||
|
TradingView layout not found. Please select a valid layout.<br />
|
||||||
|
<span className="text-xs">(Technical: {error})</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
error
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-center gap-2 text-gray-300 mb-2">
|
||||||
|
<svg className="animate-spin h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path></svg>
|
||||||
|
Analyzing chart...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{result && (
|
||||||
|
<div className="bg-gray-800 rounded p-4 mt-4">
|
||||||
|
<div><b>Summary:</b> {result.summary}</div>
|
||||||
|
<div><b>Sentiment:</b> {result.marketSentiment}</div>
|
||||||
|
<div><b>Recommendation:</b> {result.recommendation} ({result.confidence}%)</div>
|
||||||
|
<div><b>Support:</b> {result.keyLevels?.support?.join(', ')}</div>
|
||||||
|
<div><b>Resistance:</b> {result.keyLevels?.resistance?.join(', ')}</div>
|
||||||
|
<div><b>Reasoning:</b> {result.reasoning}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
components/AutoTradingPanel.tsx
Normal file
35
components/AutoTradingPanel.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"use client"
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
export default function AutoTradingPanel() {
|
||||||
|
const [status, setStatus] = useState<'idle'|'running'|'stopped'>('idle')
|
||||||
|
const [message, setMessage] = useState<string>('')
|
||||||
|
|
||||||
|
async function handleAction(action: 'start'|'stop') {
|
||||||
|
setMessage('')
|
||||||
|
setStatus('idle')
|
||||||
|
const res = await fetch('/api/auto-trading', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action })
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
setStatus(action === 'start' ? 'running' : 'stopped')
|
||||||
|
setMessage(`Auto-trading ${action}ed`)
|
||||||
|
} else {
|
||||||
|
setMessage('Error: ' + (await res.text()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border rounded bg-gray-900">
|
||||||
|
<h2 className="text-lg font-bold mb-2">Auto-Trading Control</h2>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<button className="btn btn-primary" onClick={() => handleAction('start')}>Start</button>
|
||||||
|
<button className="btn btn-secondary" onClick={() => handleAction('stop')}>Stop</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400">Status: {status}</div>
|
||||||
|
{message && <div className="mt-2 text-yellow-400">{message}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
components/Dashboard-minimal.tsx
Normal file
11
components/Dashboard-minimal.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"use client"
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function DashboardMinimal() {
|
||||||
|
return (
|
||||||
|
<div className="p-8 text-center text-gray-400">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Trading Bot Dashboard</h1>
|
||||||
|
<p>Welcome! Please select a feature from the menu.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
68
components/Dashboard.tsx
Normal file
68
components/Dashboard.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
"use client"
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import AutoTradingPanel from './AutoTradingPanel'
|
||||||
|
import TradingHistory from './TradingHistory'
|
||||||
|
import DeveloperSettings from './DeveloperSettings'
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const [positions, setPositions] = useState<any[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchPositions() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/trading')
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setPositions(data.positions || [])
|
||||||
|
} else {
|
||||||
|
setError('Failed to load positions')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError('Error loading positions')
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
fetchPositions()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 p-8 bg-gray-950 min-h-screen">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<AutoTradingPanel />
|
||||||
|
<DeveloperSettings />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="bg-gray-900 rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-xl font-bold mb-4 text-white">Open Positions</h2>
|
||||||
|
{loading ? <div className="text-gray-400">Loading...</div> : error ? <div className="text-red-400">{error}</div> : (
|
||||||
|
<table className="w-full text-sm text-left">
|
||||||
|
<thead className="text-gray-400 border-b border-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th className="py-2">Symbol</th>
|
||||||
|
<th>Side</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Entry Price</th>
|
||||||
|
<th>Unrealized PnL</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{positions.map((pos, i) => (
|
||||||
|
<tr key={i} className="border-b border-gray-800 hover:bg-gray-800">
|
||||||
|
<td className="py-2">{pos.symbol}</td>
|
||||||
|
<td>{pos.side}</td>
|
||||||
|
<td>{pos.size}</td>
|
||||||
|
<td>{pos.entryPrice}</td>
|
||||||
|
<td>{pos.unrealizedPnl}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<TradingHistory />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
27
components/DeveloperSettings.tsx
Normal file
27
components/DeveloperSettings.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"use client"
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
export default function DeveloperSettings() {
|
||||||
|
const [env, setEnv] = useState('')
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
// Example: Save env to localStorage or send to API
|
||||||
|
localStorage.setItem('devEnv', env)
|
||||||
|
setMessage('Settings saved!')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border rounded bg-gray-900">
|
||||||
|
<h2 className="text-lg font-bold mb-2">Developer Settings</h2>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full mb-2"
|
||||||
|
placeholder="Custom ENV value"
|
||||||
|
value={env}
|
||||||
|
onChange={e => setEnv(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-primary w-full" onClick={handleSave}>Save</button>
|
||||||
|
{message && <div className="mt-2 text-green-400">{message}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
60
components/TradingHistory.tsx
Normal file
60
components/TradingHistory.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"use client"
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface Trade {
|
||||||
|
id: string
|
||||||
|
symbol: string
|
||||||
|
side: string
|
||||||
|
amount: number
|
||||||
|
price: number
|
||||||
|
status: string
|
||||||
|
executedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TradingHistory() {
|
||||||
|
const [trades, setTrades] = useState<Trade[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchTrades() {
|
||||||
|
const res = await fetch('/api/trading-history')
|
||||||
|
if (res.ok) {
|
||||||
|
setTrades(await res.json())
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
fetchTrades()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border rounded bg-gray-900">
|
||||||
|
<h2 className="text-lg font-bold mb-2">Trading History</h2>
|
||||||
|
{loading ? <div>Loading...</div> : (
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Symbol</th>
|
||||||
|
<th>Side</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Executed At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{trades.map(trade => (
|
||||||
|
<tr key={trade.id}>
|
||||||
|
<td>{trade.symbol}</td>
|
||||||
|
<td>{trade.side}</td>
|
||||||
|
<td>{trade.amount}</td>
|
||||||
|
<td>{trade.price}</td>
|
||||||
|
<td>{trade.status}</td>
|
||||||
|
<td>{trade.executedAt}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
- /app/node_modules
|
||||||
|
- ./screenshots:/app/screenshots
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
|
- PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
|
- TZ=Europe/Berlin
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
# Uncomment for debugging
|
||||||
|
# command: ["npm", "run", "dev"]
|
||||||
|
# entrypoint: ["/bin/bash"]
|
||||||
208
lib/ai-analysis.ts
Normal file
208
lib/ai-analysis.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import OpenAI from 'openai'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface AnalysisResult {
|
||||||
|
summary: string
|
||||||
|
marketSentiment: 'BULLISH' | 'BEARISH' | 'NEUTRAL'
|
||||||
|
keyLevels: {
|
||||||
|
support: number[]
|
||||||
|
resistance: number[]
|
||||||
|
}
|
||||||
|
recommendation: 'BUY' | 'SELL' | 'HOLD'
|
||||||
|
confidence: number // 0-100
|
||||||
|
reasoning: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AIAnalysisService {
|
||||||
|
async analyzeScreenshot(filename: string): Promise<AnalysisResult | null> {
|
||||||
|
try {
|
||||||
|
const screenshotsDir = path.join(process.cwd(), 'screenshots')
|
||||||
|
const imagePath = path.join(screenshotsDir, filename)
|
||||||
|
// Read image file
|
||||||
|
const imageBuffer = await fs.readFile(imagePath)
|
||||||
|
const base64Image = imageBuffer.toString('base64')
|
||||||
|
|
||||||
|
const prompt = `You are now a professional trading assistant focused on short-term crypto trading using 5–15min timeframes. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
|
||||||
|
|
||||||
|
Analyze the attached TradingView chart screenshot and provide a detailed trading analysis.
|
||||||
|
|
||||||
|
### WHEN GIVING A TRADE SETUP:
|
||||||
|
Be 100% SPECIFIC. Provide:
|
||||||
|
|
||||||
|
1. **ENTRY**
|
||||||
|
- Exact price level (with a ± entry buffer if needed)
|
||||||
|
- Rationale: e.g., "Rejection from 15 EMA + VWAP confluence near intraday supply"
|
||||||
|
|
||||||
|
2. **STOP-LOSS (SL)**
|
||||||
|
- Exact level (not arbitrary)
|
||||||
|
- Explain *why* it's there: "Above VWAP + failed breakout zone"
|
||||||
|
|
||||||
|
3. **TAKE PROFITS**
|
||||||
|
- TP1: Immediate structure (ex: previous low at $149.20)
|
||||||
|
- TP2: Extended target if momentum continues (e.g., $148.00)
|
||||||
|
- Mention **expected RSI/OBV behavior** at each TP zone
|
||||||
|
|
||||||
|
4. **RISK-TO-REWARD**
|
||||||
|
- Show R:R. Ex: "1:2.5 — Risking $X to potentially gain $Y"
|
||||||
|
|
||||||
|
5. **CONFIRMATION TRIGGER**
|
||||||
|
- Exact signal to wait for: e.g., "Bearish engulfing candle on rejection from VWAP zone"
|
||||||
|
- OBV: "Must be making lower highs + dropping below 30min average"
|
||||||
|
- RSI: "Should remain under 50 on rejection. Overbought ≥70 = wait"
|
||||||
|
|
||||||
|
6. **INDICATOR ANALYSIS**
|
||||||
|
- **RSI**: If RSI crosses above 70 while price is under resistance → *wait*
|
||||||
|
- **VWAP**: If price retakes VWAP with bullish momentum → *consider invalidation*
|
||||||
|
- **OBV**: If OBV starts climbing while price stays flat → *early exit or reconsider bias*
|
||||||
|
|
||||||
|
Return your answer as a JSON object with the following structure:
|
||||||
|
{
|
||||||
|
"summary": "Brief market summary",
|
||||||
|
"marketSentiment": "BULLISH" | "BEARISH" | "NEUTRAL",
|
||||||
|
"keyLevels": {
|
||||||
|
"support": [number array],
|
||||||
|
"resistance": [number array]
|
||||||
|
},
|
||||||
|
"recommendation": "BUY" | "SELL" | "HOLD",
|
||||||
|
"confidence": number (0-100),
|
||||||
|
"reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers"
|
||||||
|
}
|
||||||
|
|
||||||
|
Be concise but thorough. Only return valid JSON.`
|
||||||
|
const response = await openai.chat.completions.create({
|
||||||
|
model: "gpt-4o", // Updated to current vision model
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: prompt },
|
||||||
|
{ type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
max_tokens: 1024
|
||||||
|
})
|
||||||
|
const content = response.choices[0]?.message?.content
|
||||||
|
if (!content) return null
|
||||||
|
// Extract JSON from response
|
||||||
|
const match = content.match(/\{[\s\S]*\}/)
|
||||||
|
if (!match) return null
|
||||||
|
const json = match[0]
|
||||||
|
const result = JSON.parse(json)
|
||||||
|
// Optionally: validate result structure here
|
||||||
|
return result as AnalysisResult
|
||||||
|
} catch (e) {
|
||||||
|
console.error('AI analysis error:', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyzeMultipleScreenshots(filenames: string[]): Promise<AnalysisResult | null> {
|
||||||
|
try {
|
||||||
|
const screenshotsDir = path.join(process.cwd(), 'screenshots')
|
||||||
|
const images: any[] = []
|
||||||
|
|
||||||
|
for (const filename of filenames) {
|
||||||
|
const imagePath = path.join(screenshotsDir, filename)
|
||||||
|
const imageBuffer = await fs.readFile(imagePath)
|
||||||
|
const base64Image = imageBuffer.toString('base64')
|
||||||
|
images.push({ type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = `You are now a professional trading assistant focused on short-term crypto trading using 5–15min timeframes. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
|
||||||
|
|
||||||
|
Analyze the attached TradingView chart screenshots (multiple layouts of the same symbol) and provide a comprehensive trading analysis by combining insights from all charts.
|
||||||
|
|
||||||
|
### WHEN GIVING A TRADE SETUP:
|
||||||
|
Be 100% SPECIFIC. Provide:
|
||||||
|
|
||||||
|
1. **ENTRY**
|
||||||
|
- Exact price level (with a ± entry buffer if needed)
|
||||||
|
- Rationale: e.g., "Rejection from 15 EMA + VWAP confluence near intraday supply"
|
||||||
|
|
||||||
|
2. **STOP-LOSS (SL)**
|
||||||
|
- Exact level (not arbitrary)
|
||||||
|
- Explain *why* it's there: "Above VWAP + failed breakout zone"
|
||||||
|
|
||||||
|
3. **TAKE PROFITS**
|
||||||
|
- TP1: Immediate structure (ex: previous low at $149.20)
|
||||||
|
- TP2: Extended target if momentum continues (e.g., $148.00)
|
||||||
|
- Mention **expected RSI/OBV behavior** at each TP zone
|
||||||
|
|
||||||
|
4. **RISK-TO-REWARD**
|
||||||
|
- Show R:R. Ex: "1:2.5 — Risking $X to potentially gain $Y"
|
||||||
|
|
||||||
|
5. **CONFIRMATION TRIGGER**
|
||||||
|
- Exact signal to wait for: e.g., "Bearish engulfing candle on rejection from VWAP zone"
|
||||||
|
- OBV: "Must be making lower highs + dropping below 30min average"
|
||||||
|
- RSI: "Should remain under 50 on rejection. Overbought ≥70 = wait"
|
||||||
|
|
||||||
|
6. **INDICATOR ANALYSIS**
|
||||||
|
- **RSI**: If RSI crosses above 70 while price is under resistance → *wait*
|
||||||
|
- **VWAP**: If price retakes VWAP with bullish momentum → *consider invalidation*
|
||||||
|
- **OBV**: If OBV starts climbing while price stays flat → *early exit or reconsider bias*
|
||||||
|
|
||||||
|
Cross-reference all layouts to provide the most accurate analysis. If layouts show conflicting signals, explain which one takes priority and why.
|
||||||
|
|
||||||
|
Return your answer as a JSON object with the following structure:
|
||||||
|
{
|
||||||
|
"summary": "Brief market summary combining all layouts",
|
||||||
|
"marketSentiment": "BULLISH" | "BEARISH" | "NEUTRAL",
|
||||||
|
"keyLevels": {
|
||||||
|
"support": [number array],
|
||||||
|
"resistance": [number array]
|
||||||
|
},
|
||||||
|
"recommendation": "BUY" | "SELL" | "HOLD",
|
||||||
|
"confidence": number (0-100),
|
||||||
|
"reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers from all layouts"
|
||||||
|
}
|
||||||
|
|
||||||
|
Be concise but thorough. Only return valid JSON.`
|
||||||
|
|
||||||
|
const response = await openai.chat.completions.create({
|
||||||
|
model: "gpt-4o",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: prompt },
|
||||||
|
...images
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
max_tokens: 1500,
|
||||||
|
temperature: 0.1
|
||||||
|
})
|
||||||
|
|
||||||
|
const content = response.choices[0]?.message?.content
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('No content received from OpenAI')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON response
|
||||||
|
const jsonMatch = content.match(/\{[\s\S]*\}/)
|
||||||
|
if (!jsonMatch) {
|
||||||
|
throw new Error('No JSON found in response')
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysis = JSON.parse(jsonMatch[0])
|
||||||
|
|
||||||
|
// Validate the structure
|
||||||
|
if (!analysis.summary || !analysis.marketSentiment || !analysis.recommendation || !analysis.confidence) {
|
||||||
|
throw new Error('Invalid analysis structure')
|
||||||
|
}
|
||||||
|
|
||||||
|
return analysis
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI multi-analysis error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiAnalysisService = new AIAnalysisService()
|
||||||
82
lib/auto-trading.ts
Normal file
82
lib/auto-trading.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { tradingViewCapture } from './tradingview'
|
||||||
|
import { aiAnalysisService } from './ai-analysis'
|
||||||
|
import prisma from './prisma'
|
||||||
|
|
||||||
|
export interface AutoTradingConfig {
|
||||||
|
enabled: boolean
|
||||||
|
symbols: string[]
|
||||||
|
intervalMinutes: number
|
||||||
|
maxDailyTrades: number
|
||||||
|
tradingAmount: number
|
||||||
|
confidenceThreshold: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AutoTradingService {
|
||||||
|
private config: AutoTradingConfig
|
||||||
|
private intervalId: NodeJS.Timeout | null = null
|
||||||
|
private dailyTradeCount: Record<string, number> = {}
|
||||||
|
|
||||||
|
constructor(config: AutoTradingConfig) {
|
||||||
|
this.config = config
|
||||||
|
this.dailyTradeCount = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.intervalId || !this.config.enabled) return
|
||||||
|
this.intervalId = setInterval(() => this.run(), this.config.intervalMinutes * 60 * 1000)
|
||||||
|
this.run() // Run immediately on start
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId)
|
||||||
|
this.intervalId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
if (!this.config.enabled) return
|
||||||
|
for (const symbol of this.config.symbols) {
|
||||||
|
if ((this.dailyTradeCount[symbol] || 0) >= this.config.maxDailyTrades) continue
|
||||||
|
// 1. Capture screenshot
|
||||||
|
const filename = `${symbol}_${Date.now()}.png`
|
||||||
|
const screenshotPath = await tradingViewCapture.capture(symbol, filename)
|
||||||
|
// 2. Analyze screenshot
|
||||||
|
const analysis = await aiAnalysisService.analyzeScreenshot(filename)
|
||||||
|
if (!analysis || analysis.confidence < this.config.confidenceThreshold) continue
|
||||||
|
// 3. Execute trade (stub: integrate with driftTradingService)
|
||||||
|
// const tradeResult = await driftTradingService.executeTrade({ ... })
|
||||||
|
// 4. Save trade to DB
|
||||||
|
await prisma.trade.create({
|
||||||
|
data: {
|
||||||
|
symbol,
|
||||||
|
side: analysis.recommendation === 'BUY' ? 'LONG' : analysis.recommendation === 'SELL' ? 'SHORT' : 'NONE',
|
||||||
|
amount: this.config.tradingAmount,
|
||||||
|
price: 0, // To be filled with actual execution price
|
||||||
|
status: 'PENDING',
|
||||||
|
screenshotUrl: screenshotPath,
|
||||||
|
aiAnalysis: JSON.stringify(analysis),
|
||||||
|
executedAt: new Date(),
|
||||||
|
userId: 'system', // Or actual user if available
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.dailyTradeCount[symbol] = (this.dailyTradeCount[symbol] || 0) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config: Partial<AutoTradingConfig>) {
|
||||||
|
this.config = { ...this.config, ...config }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAutoTradingService() {
|
||||||
|
// Singleton pattern or similar
|
||||||
|
return new AutoTradingService({
|
||||||
|
enabled: false,
|
||||||
|
symbols: ['BTCUSD'],
|
||||||
|
intervalMinutes: 15,
|
||||||
|
maxDailyTrades: 10,
|
||||||
|
tradingAmount: 100,
|
||||||
|
confidenceThreshold: 80
|
||||||
|
})
|
||||||
|
}
|
||||||
13
lib/prisma.ts
Normal file
13
lib/prisma.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var prisma: PrismaClient | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const prisma = globalThis.prisma || new PrismaClient()
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
globalThis.prisma = prisma
|
||||||
|
}
|
||||||
|
|
||||||
|
export default prisma
|
||||||
88
lib/settings.ts
Normal file
88
lib/settings.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export interface TradingSettings {
|
||||||
|
symbol: string
|
||||||
|
timeframe: string
|
||||||
|
layouts: string[]
|
||||||
|
lastUpdated: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const SETTINGS_FILE = path.join(process.cwd(), 'trading-settings.json')
|
||||||
|
|
||||||
|
export class SettingsManager {
|
||||||
|
private static instance: SettingsManager
|
||||||
|
private settings: TradingSettings = {
|
||||||
|
symbol: 'BTCUSD',
|
||||||
|
timeframe: '5',
|
||||||
|
layouts: ['ai'],
|
||||||
|
lastUpdated: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static getInstance(): SettingsManager {
|
||||||
|
if (!SettingsManager.instance) {
|
||||||
|
SettingsManager.instance = new SettingsManager()
|
||||||
|
}
|
||||||
|
return SettingsManager.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSettings(): Promise<TradingSettings> {
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(SETTINGS_FILE, 'utf-8')
|
||||||
|
this.settings = JSON.parse(data)
|
||||||
|
console.log('Loaded settings:', this.settings)
|
||||||
|
} catch (error) {
|
||||||
|
console.log('No existing settings found, using defaults')
|
||||||
|
await this.saveSettings()
|
||||||
|
}
|
||||||
|
return this.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.settings.lastUpdated = Date.now()
|
||||||
|
await fs.writeFile(SETTINGS_FILE, JSON.stringify(this.settings, null, 2))
|
||||||
|
console.log('Settings saved:', this.settings)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save settings:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSettings(updates: Partial<TradingSettings>): Promise<TradingSettings> {
|
||||||
|
this.settings = { ...this.settings, ...updates }
|
||||||
|
await this.saveSettings()
|
||||||
|
return this.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettings(): TradingSettings {
|
||||||
|
return this.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSymbol(symbol: string): Promise<void> {
|
||||||
|
await this.updateSettings({ symbol })
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTimeframe(timeframe: string): Promise<void> {
|
||||||
|
await this.updateSettings({ timeframe })
|
||||||
|
}
|
||||||
|
|
||||||
|
async setLayouts(layouts: string[]): Promise<void> {
|
||||||
|
await this.updateSettings({ layouts })
|
||||||
|
}
|
||||||
|
|
||||||
|
async addLayout(layout: string): Promise<void> {
|
||||||
|
if (!this.settings.layouts.includes(layout)) {
|
||||||
|
const layouts = [...this.settings.layouts, layout]
|
||||||
|
await this.updateSettings({ layouts })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeLayout(layout: string): Promise<void> {
|
||||||
|
const layouts = this.settings.layouts.filter(l => l !== layout)
|
||||||
|
await this.updateSettings({ layouts })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsManager = SettingsManager.getInstance()
|
||||||
396
lib/tradingview.ts
Normal file
396
lib/tradingview.ts
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
import puppeteer, { Browser, Page, Frame } from 'puppeteer'
|
||||||
|
import path from 'path'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import { settingsManager } from './settings'
|
||||||
|
|
||||||
|
const TRADINGVIEW_EMAIL = process.env.TRADINGVIEW_EMAIL
|
||||||
|
const TRADINGVIEW_PASSWORD = process.env.TRADINGVIEW_PASSWORD
|
||||||
|
const TRADINGVIEW_LAYOUTS = (process.env.TRADINGVIEW_LAYOUTS || '').split(',').map(l => l.trim())
|
||||||
|
const PUPPETEER_EXECUTABLE_PATH = process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium'
|
||||||
|
|
||||||
|
export class TradingViewCapture {
|
||||||
|
private browser: Browser | null = null
|
||||||
|
private page: Page | null = null
|
||||||
|
private loggedIn = false
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (!this.browser) {
|
||||||
|
this.browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--disable-accelerated-2d-canvas',
|
||||||
|
'--no-first-run',
|
||||||
|
'--no-zygote',
|
||||||
|
'--disable-gpu'
|
||||||
|
],
|
||||||
|
executablePath: PUPPETEER_EXECUTABLE_PATH
|
||||||
|
})
|
||||||
|
console.log('Puppeteer browser launched')
|
||||||
|
}
|
||||||
|
if (!this.page) {
|
||||||
|
this.page = await this.browser.newPage()
|
||||||
|
await this.page.setViewport({ width: 1920, height: 1080 })
|
||||||
|
console.log('Puppeteer page created')
|
||||||
|
}
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
console.log('Logging in to TradingView...')
|
||||||
|
await this.login()
|
||||||
|
this.loggedIn = true
|
||||||
|
console.log('Logged in to TradingView')
|
||||||
|
}
|
||||||
|
return this.page
|
||||||
|
}
|
||||||
|
|
||||||
|
async login() {
|
||||||
|
if (!TRADINGVIEW_EMAIL || !TRADINGVIEW_PASSWORD) {
|
||||||
|
throw new Error('TradingView credentials not set in .env')
|
||||||
|
}
|
||||||
|
const page = this.page || (await this.browser!.newPage())
|
||||||
|
console.log('Navigating to TradingView login page...')
|
||||||
|
await page.goto('https://www.tradingview.com/#signin', { waitUntil: 'networkidle2' })
|
||||||
|
|
||||||
|
// Check if we're already logged in
|
||||||
|
try {
|
||||||
|
const loggedInIndicator = await page.waitForSelector('.tv-header__user-menu-button, [data-name="header-user-menu"]', { timeout: 3000 })
|
||||||
|
if (loggedInIndicator) {
|
||||||
|
console.log('Already logged in to TradingView')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Not logged in yet, proceeding with login...')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait for the login modal to appear and look for email input directly
|
||||||
|
console.log('Looking for email input field...')
|
||||||
|
|
||||||
|
// Try to find the email input field directly (new TradingView layout)
|
||||||
|
const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 })
|
||||||
|
|
||||||
|
if (emailInput) {
|
||||||
|
console.log('Found email input field directly')
|
||||||
|
await emailInput.click() // Click to focus
|
||||||
|
await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
|
||||||
|
|
||||||
|
// Find password field
|
||||||
|
const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 })
|
||||||
|
if (!passwordInput) {
|
||||||
|
throw new Error('Could not find password input field')
|
||||||
|
}
|
||||||
|
await passwordInput.click() // Click to focus
|
||||||
|
await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
|
||||||
|
|
||||||
|
// Find and click the sign in button
|
||||||
|
const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 })
|
||||||
|
if (!signInButton) {
|
||||||
|
// Try to find button with sign in text
|
||||||
|
const buttons = await page.$$('button')
|
||||||
|
let foundButton = null
|
||||||
|
for (const btn of buttons) {
|
||||||
|
const text = await page.evaluate(el => el.innerText || el.textContent, btn)
|
||||||
|
if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) {
|
||||||
|
foundButton = btn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundButton) {
|
||||||
|
throw new Error('Could not find sign in button')
|
||||||
|
}
|
||||||
|
await foundButton.click()
|
||||||
|
} else {
|
||||||
|
await signInButton.click()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Could not find email input field')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback: try to find email button first
|
||||||
|
console.log('Fallback: looking for email button...')
|
||||||
|
try {
|
||||||
|
await page.waitForSelector('button', { timeout: 15000 })
|
||||||
|
const buttons = await page.$$('button')
|
||||||
|
let emailBtn = null
|
||||||
|
|
||||||
|
// Look for email button with various text patterns
|
||||||
|
for (const btn of buttons) {
|
||||||
|
const text = await page.evaluate(el => el.innerText || el.textContent, btn)
|
||||||
|
if (text && (
|
||||||
|
text.trim().toLowerCase().includes('email') ||
|
||||||
|
text.trim().toLowerCase().includes('sign in with email') ||
|
||||||
|
text.trim().toLowerCase().includes('continue with email')
|
||||||
|
)) {
|
||||||
|
emailBtn = btn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailBtn) {
|
||||||
|
console.log('Found email button, clicking...')
|
||||||
|
await emailBtn.click()
|
||||||
|
await new Promise(res => setTimeout(res, 1000))
|
||||||
|
|
||||||
|
// Now fill in the form
|
||||||
|
const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 })
|
||||||
|
if (!emailInput) {
|
||||||
|
throw new Error('Could not find email input field after clicking email button')
|
||||||
|
}
|
||||||
|
await emailInput.click() // Click to focus
|
||||||
|
await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
|
||||||
|
|
||||||
|
const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 })
|
||||||
|
if (!passwordInput) {
|
||||||
|
throw new Error('Could not find password input field after clicking email button')
|
||||||
|
}
|
||||||
|
await passwordInput.click() // Click to focus
|
||||||
|
await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
|
||||||
|
|
||||||
|
const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 })
|
||||||
|
if (!signInButton) {
|
||||||
|
// Try to find button with sign in text
|
||||||
|
const buttons = await page.$$('button')
|
||||||
|
let foundButton = null
|
||||||
|
for (const btn of buttons) {
|
||||||
|
const text = await page.evaluate(el => el.innerText || el.textContent, btn)
|
||||||
|
if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) {
|
||||||
|
foundButton = btn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundButton) {
|
||||||
|
throw new Error('Could not find sign in button after clicking email button')
|
||||||
|
}
|
||||||
|
await foundButton.click()
|
||||||
|
} else {
|
||||||
|
await signInButton.click()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Could not find email button')
|
||||||
|
}
|
||||||
|
} catch (e2) {
|
||||||
|
console.error('Could not find or click email button:', e2)
|
||||||
|
const errorMessage = e2 instanceof Error ? e2.message : String(e2)
|
||||||
|
throw new Error('Could not find or click email button on TradingView login page. ' + errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for navigation or dashboard (main page)
|
||||||
|
try {
|
||||||
|
console.log('Waiting for login to complete...')
|
||||||
|
await page.waitForSelector('.tv-header__user-menu-button, .chart-container, [data-name="header-user-menu"]', { timeout: 30000 })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Login navigation did not complete.')
|
||||||
|
throw new Error('Login navigation did not complete.')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('TradingView login complete')
|
||||||
|
}
|
||||||
|
|
||||||
|
async capture(symbol: string, filename: string, layouts?: string[], timeframe?: string) {
|
||||||
|
console.log('Working directory:', process.cwd())
|
||||||
|
|
||||||
|
// Load settings and update if provided
|
||||||
|
const settings = await settingsManager.loadSettings()
|
||||||
|
if (symbol && symbol !== settings.symbol) {
|
||||||
|
await settingsManager.setSymbol(symbol)
|
||||||
|
}
|
||||||
|
if (timeframe && timeframe !== settings.timeframe) {
|
||||||
|
await settingsManager.setTimeframe(timeframe)
|
||||||
|
}
|
||||||
|
if (layouts && JSON.stringify(layouts) !== JSON.stringify(settings.layouts)) {
|
||||||
|
await settingsManager.setLayouts(layouts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use saved settings if not provided
|
||||||
|
const finalSymbol = symbol || settings.symbol
|
||||||
|
const finalTimeframe = timeframe || settings.timeframe
|
||||||
|
const finalLayouts = layouts || settings.layouts
|
||||||
|
|
||||||
|
console.log('Using settings:', { symbol: finalSymbol, timeframe: finalTimeframe, layouts: finalLayouts })
|
||||||
|
|
||||||
|
const page = await this.init()
|
||||||
|
// Add timeframe to TradingView URL if provided
|
||||||
|
let url = `https://www.tradingview.com/chart/?symbol=${finalSymbol}`
|
||||||
|
if (finalTimeframe) {
|
||||||
|
url += `&interval=${encodeURIComponent(finalTimeframe)}`
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
console.log('Navigating to TradingView chart:', url)
|
||||||
|
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 })
|
||||||
|
console.log('Successfully navigated to chart')
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Failed to load TradingView chart page:', e)
|
||||||
|
throw new Error('Failed to load TradingView chart page: ' + (e.message || e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture screenshots for each layout
|
||||||
|
const screenshots: string[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < finalLayouts.length; i++) {
|
||||||
|
const layout = finalLayouts[i]
|
||||||
|
console.log(`Processing layout ${i + 1}/${finalLayouts.length}: ${layout}`)
|
||||||
|
|
||||||
|
// Load the layout
|
||||||
|
await this.loadLayout(page, layout)
|
||||||
|
|
||||||
|
// Wait for layout to load
|
||||||
|
await new Promise(res => setTimeout(res, 3000))
|
||||||
|
|
||||||
|
// Generate filename for this layout
|
||||||
|
const layoutFilename = filename.replace('.png', `_${layout}.png`)
|
||||||
|
const screenshotsDir = path.join(process.cwd(), 'screenshots')
|
||||||
|
await fs.mkdir(screenshotsDir, { recursive: true })
|
||||||
|
const filePath = path.join(screenshotsDir, layoutFilename)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.screenshot({ path: filePath as `${string}.png`, type: 'png' })
|
||||||
|
console.log(`Screenshot saved for layout ${layout}:`, filePath)
|
||||||
|
screenshots.push(filePath)
|
||||||
|
} catch (e: any) {
|
||||||
|
const debugScreenshotErrorPath = path.resolve(`debug_screenshot_error_${layout}.png`) as `${string}.png`
|
||||||
|
await page.screenshot({ path: debugScreenshotErrorPath })
|
||||||
|
console.error(`Failed to capture screenshot for layout ${layout}:`, e)
|
||||||
|
console.error('Screenshot on screenshot error:', debugScreenshotErrorPath)
|
||||||
|
throw new Error(`Failed to capture screenshot for layout ${layout}: ` + (e.message || e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return screenshots
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadLayout(page: Page, layout: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('Trying to load layout:', layout)
|
||||||
|
|
||||||
|
// Try multiple selectors for the layout button
|
||||||
|
const layoutSelectors = [
|
||||||
|
'[data-name="load-chart-layout-dialog"]',
|
||||||
|
'[data-name="layouts-menu"]',
|
||||||
|
'[data-name="chart-layout-button"]',
|
||||||
|
'button[title*="Layout" i]',
|
||||||
|
'button[aria-label*="Layout" i]',
|
||||||
|
'[data-testid*="layout"]'
|
||||||
|
]
|
||||||
|
|
||||||
|
let layoutButton = null
|
||||||
|
for (const selector of layoutSelectors) {
|
||||||
|
try {
|
||||||
|
layoutButton = await page.waitForSelector(selector, { timeout: 3000 })
|
||||||
|
if (layoutButton) {
|
||||||
|
console.log('Found layout button with selector:', selector)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue to next selector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!layoutButton) {
|
||||||
|
// Try to find layout button by text content
|
||||||
|
const buttons = await page.$$('button, [role="button"]')
|
||||||
|
for (const btn of buttons) {
|
||||||
|
const text = await page.evaluate(el => {
|
||||||
|
const element = el as HTMLElement
|
||||||
|
return element.innerText || element.textContent || element.title || element.getAttribute('aria-label')
|
||||||
|
}, btn)
|
||||||
|
if (text && text.toLowerCase().includes('layout')) {
|
||||||
|
layoutButton = btn
|
||||||
|
console.log('Found layout button by text:', text)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutButton) {
|
||||||
|
await layoutButton.click()
|
||||||
|
console.log('Clicked layout button')
|
||||||
|
await new Promise(res => setTimeout(res, 1000))
|
||||||
|
|
||||||
|
// Look for search input or layout items
|
||||||
|
const searchSelectors = [
|
||||||
|
'input[name="search"]',
|
||||||
|
'input[placeholder*="search" i]',
|
||||||
|
'input[type="search"]',
|
||||||
|
'input[data-name="search"]'
|
||||||
|
]
|
||||||
|
|
||||||
|
let searchInput = null
|
||||||
|
for (const selector of searchSelectors) {
|
||||||
|
try {
|
||||||
|
searchInput = await page.waitForSelector(selector, { timeout: 3000 })
|
||||||
|
if (searchInput) break
|
||||||
|
} catch (e) {
|
||||||
|
// Continue to next selector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchInput) {
|
||||||
|
console.log('Found search input, typing layout name...')
|
||||||
|
await searchInput.type(layout, { delay: 50 })
|
||||||
|
await new Promise(res => setTimeout(res, 2000))
|
||||||
|
|
||||||
|
// Try to find and click the layout item
|
||||||
|
const layoutItemSelectors = [
|
||||||
|
'[data-name="chart-layout-list-item"]',
|
||||||
|
'[data-testid*="layout-item"]',
|
||||||
|
'.layout-item',
|
||||||
|
'[role="option"]'
|
||||||
|
]
|
||||||
|
|
||||||
|
let layoutItem = null
|
||||||
|
for (const selector of layoutItemSelectors) {
|
||||||
|
try {
|
||||||
|
const items = await page.$$(selector)
|
||||||
|
for (const item of items) {
|
||||||
|
const text = await page.evaluate(el => {
|
||||||
|
const element = el as HTMLElement
|
||||||
|
return element.innerText || element.textContent
|
||||||
|
}, item)
|
||||||
|
if (text && text.toLowerCase().includes(layout.toLowerCase())) {
|
||||||
|
layoutItem = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (layoutItem) break
|
||||||
|
} catch (e) {
|
||||||
|
// Continue to next selector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutItem) {
|
||||||
|
await layoutItem.click()
|
||||||
|
console.log('Clicked layout item:', layout)
|
||||||
|
} else {
|
||||||
|
console.log('Layout item not found, trying generic approach...')
|
||||||
|
await page.evaluate((layout) => {
|
||||||
|
const items = Array.from(document.querySelectorAll('*'))
|
||||||
|
const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase()))
|
||||||
|
if (item) (item as HTMLElement).click()
|
||||||
|
}, layout)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Search input not found, trying to find layout directly...')
|
||||||
|
await page.evaluate((layout) => {
|
||||||
|
const items = Array.from(document.querySelectorAll('*'))
|
||||||
|
const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase()))
|
||||||
|
if (item) (item as HTMLElement).click()
|
||||||
|
}, layout)
|
||||||
|
}
|
||||||
|
await new Promise(res => setTimeout(res, 4000))
|
||||||
|
console.log('Layout loaded:', layout)
|
||||||
|
} else {
|
||||||
|
console.log('Layout button not found, skipping layout loading')
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
const debugLayoutErrorPath = path.resolve('debug_layout_error.png') as `${string}.png`
|
||||||
|
await page.screenshot({ path: debugLayoutErrorPath })
|
||||||
|
console.error('TradingView layout not found or could not be loaded:', e)
|
||||||
|
console.log('Continuing without layout...')
|
||||||
|
// Don't throw error, just continue without layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tradingViewCapture = new TradingViewCapture()
|
||||||
134
package-lock.json
generated
134
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"@drift-labs/sdk": "^2.126.0-beta.14",
|
"@drift-labs/sdk": "^2.126.0-beta.14",
|
||||||
"@prisma/client": "^6.11.1",
|
"@prisma/client": "^6.11.1",
|
||||||
"@solana/web3.js": "^1.98.2",
|
"@solana/web3.js": "^1.98.2",
|
||||||
|
"bs58": "^6.0.0",
|
||||||
"next": "15.3.5",
|
"next": "15.3.5",
|
||||||
"openai": "^5.8.3",
|
"openai": "^5.8.3",
|
||||||
"prisma": "^6.11.1",
|
"prisma": "^6.11.1",
|
||||||
@@ -148,6 +149,15 @@
|
|||||||
"@solana/web3.js": "^1.68.0"
|
"@solana/web3.js": "^1.68.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@coral-xyz/anchor-30/node_modules/bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@coral-xyz/anchor-errors": {
|
"node_modules/@coral-xyz/anchor-errors": {
|
||||||
"version": "0.30.1",
|
"version": "0.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz",
|
||||||
@@ -156,6 +166,16 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@coral-xyz/anchor/node_modules/bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@coral-xyz/borsh": {
|
"node_modules/@coral-xyz/borsh": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.28.0.tgz",
|
||||||
@@ -294,6 +314,15 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@drift-labs/sdk/node_modules/bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@drift-labs/sdk/node_modules/nanoid": {
|
"node_modules/@drift-labs/sdk/node_modules/nanoid": {
|
||||||
"version": "3.3.4",
|
"version": "3.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||||
@@ -1509,6 +1538,15 @@
|
|||||||
"@solana/web3.js": "^1.95.5"
|
"@solana/web3.js": "^1.95.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@openbook-dex/openbook-v2/node_modules/bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.11.1.tgz",
|
||||||
@@ -1602,6 +1640,15 @@
|
|||||||
"node": ">=11"
|
"node": ">=11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@project-serum/anchor/node_modules/bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@project-serum/anchor/node_modules/camelcase": {
|
"node_modules/@project-serum/anchor/node_modules/camelcase": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
@@ -1807,6 +1854,15 @@
|
|||||||
"@solana/web3.js": "^1.68.0"
|
"@solana/web3.js": "^1.68.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pythnetwork/pyth-solana-receiver/node_modules/bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pythnetwork/solana-utils": {
|
"node_modules/@pythnetwork/solana-utils": {
|
||||||
"version": "0.4.5",
|
"version": "0.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/@pythnetwork/solana-utils/-/solana-utils-0.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/@pythnetwork/solana-utils/-/solana-utils-0.4.5.tgz",
|
||||||
@@ -2333,6 +2389,15 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@solana/web3.js/node_modules/bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@solana/web3.js/node_modules/eventemitter3": {
|
"node_modules/@solana/web3.js/node_modules/eventemitter3": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
@@ -2401,19 +2466,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@switchboard-xyz/common/node_modules/base-x": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="
|
|
||||||
},
|
|
||||||
"node_modules/@switchboard-xyz/common/node_modules/bs58": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
|
|
||||||
"dependencies": {
|
|
||||||
"base-x": "^5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@switchboard-xyz/common/node_modules/js-sha256": {
|
"node_modules/@switchboard-xyz/common/node_modules/js-sha256": {
|
||||||
"version": "0.11.1",
|
"version": "0.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz",
|
||||||
@@ -2439,19 +2491,6 @@
|
|||||||
"@switchboard-xyz/common": ">=3.0.0"
|
"@switchboard-xyz/common": ">=3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@switchboard-xyz/on-demand/node_modules/base-x": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="
|
|
||||||
},
|
|
||||||
"node_modules/@switchboard-xyz/on-demand/node_modules/bs58": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
|
|
||||||
"dependencies": {
|
|
||||||
"base-x": "^5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.11",
|
"version": "4.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
|
||||||
@@ -3841,6 +3880,15 @@
|
|||||||
"text-encoding-utf-8": "^1.0.2"
|
"text-encoding-utf-8": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/borsh/node_modules/bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
@@ -3864,13 +3912,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bs58": {
|
"node_modules/bs58": {
|
||||||
"version": "4.0.1",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
|
||||||
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
"integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base-x": "^3.0.2"
|
"base-x": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bs58/node_modules/base-x": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
@@ -6306,6 +6361,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
|
||||||
"integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ=="
|
"integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/jito-ts/node_modules/bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jito-ts/node_modules/superstruct": {
|
"node_modules/jito-ts/node_modules/superstruct": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz",
|
||||||
@@ -8173,6 +8237,16 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/solana-bankrun/node_modules/bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"@drift-labs/sdk": "^2.126.0-beta.14",
|
"@drift-labs/sdk": "^2.126.0-beta.14",
|
||||||
"@prisma/client": "^6.11.1",
|
"@prisma/client": "^6.11.1",
|
||||||
"@solana/web3.js": "^1.98.2",
|
"@solana/web3.js": "^1.98.2",
|
||||||
|
"bs58": "^6.0.0",
|
||||||
"next": "15.3.5",
|
"next": "15.3.5",
|
||||||
"openai": "^5.8.3",
|
"openai": "^5.8.3",
|
||||||
"prisma": "^6.11.1",
|
"prisma": "^6.11.1",
|
||||||
|
|||||||
BIN
screenshots/BTCUSD_ai_5_1752062666039.png
Normal file
BIN
screenshots/BTCUSD_ai_5_1752062666039.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
BIN
screenshots/SOLUSD_ai_5_1752062839851.png
Normal file
BIN
screenshots/SOLUSD_ai_5_1752062839851.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
11
scripts/convert-solana-key.js
Normal file
11
scripts/convert-solana-key.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const bs58 = require('bs58');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Paste your base58 private key string here
|
||||||
|
const base58 = process.argv[2];
|
||||||
|
if (!base58) {
|
||||||
|
console.error('Usage: node convert.js <base58key>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const arr = Array.from(bs58.decode(base58));
|
||||||
|
console.log(JSON.stringify(arr));
|
||||||
11
scripts/convert-solana-key.mjs
Normal file
11
scripts/convert-solana-key.mjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import bs58 from 'bs58';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
// Paste your base58 private key string here
|
||||||
|
const base58 = process.argv[2];
|
||||||
|
if (!base58) {
|
||||||
|
console.error('Usage: node convert-solana-key.mjs <base58key>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const arr = Array.from(bs58.decode(base58));
|
||||||
|
console.log(JSON.stringify(arr));
|
||||||
Reference in New Issue
Block a user