diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fcc0e43 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/app/api/analyze/route.ts b/app/api/analyze/route.ts new file mode 100644 index 0000000..38d9009 --- /dev/null +++ b/app/api/analyze/route.ts @@ -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 }) + } +} diff --git a/app/api/auto-trading/route.ts b/app/api/auto-trading/route.ts new file mode 100644 index 0000000..1826bb3 --- /dev/null +++ b/app/api/auto-trading/route.ts @@ -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'] + }) +} diff --git a/app/api/screenshot/route.ts b/app/api/screenshot/route.ts new file mode 100644 index 0000000..a91facf --- /dev/null +++ b/app/api/screenshot/route.ts @@ -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 }) + } +} diff --git a/app/api/trading-history/route.ts b/app/api/trading-history/route.ts new file mode 100644 index 0000000..3c37871 --- /dev/null +++ b/app/api/trading-history/route.ts @@ -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 }) + } +} diff --git a/app/api/trading/route.ts b/app/api/trading/route.ts new file mode 100644 index 0000000..43ae72c --- /dev/null +++ b/app/api/trading/route.ts @@ -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 }) + } +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..ef59836 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: 'Inter', system-ui, sans-serif; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..375ce91 --- /dev/null +++ b/app/layout.tsx @@ -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 ( + + +
+ {children} +
+ + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..0c5d743 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,11 @@ +import AIAnalysisPanel from '../components/AIAnalysisPanel' +import Dashboard from '../components/Dashboard' + +export default function HomePage() { + return ( + <> + + + + ) +} diff --git a/app/screenshots/[filename]/route.ts b/app/screenshots/[filename]/route.ts new file mode 100644 index 0000000..1a00594 --- /dev/null +++ b/app/screenshots/[filename]/route.ts @@ -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 }) + } +} diff --git a/components/AIAnalysisPanel.tsx b/components/AIAnalysisPanel.tsx new file mode 100644 index 0000000..f287e11 --- /dev/null +++ b/components/AIAnalysisPanel.tsx @@ -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(null) + const [error, setError] = useState(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 ( +
+

AI Chart Analysis

+
+ setSymbol(e.target.value)} + placeholder="Symbol (e.g. BTCUSD)" + /> + + + +
+ {error && ( +
+ {error.includes('frame was detached') ? ( + <> + TradingView chart could not be loaded. Please check your symbol and layout, or try again.
+ (Technical: {error}) + + ) : error.includes('layout not found') ? ( + <> + TradingView layout not found. Please select a valid layout.
+ (Technical: {error}) + + ) : ( + error + )} +
+ )} + {loading && ( +
+ + Analyzing chart... +
+ )} + {result && ( +
+
Summary: {result.summary}
+
Sentiment: {result.marketSentiment}
+
Recommendation: {result.recommendation} ({result.confidence}%)
+
Support: {result.keyLevels?.support?.join(', ')}
+
Resistance: {result.keyLevels?.resistance?.join(', ')}
+
Reasoning: {result.reasoning}
+
+ )} +
+ ) +} diff --git a/components/AutoTradingPanel.tsx b/components/AutoTradingPanel.tsx new file mode 100644 index 0000000..f39dd0e --- /dev/null +++ b/components/AutoTradingPanel.tsx @@ -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('') + + 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 ( +
+

Auto-Trading Control

+
+ + +
+
Status: {status}
+ {message &&
{message}
} +
+ ) +} diff --git a/components/Dashboard-minimal.tsx b/components/Dashboard-minimal.tsx new file mode 100644 index 0000000..2212a09 --- /dev/null +++ b/components/Dashboard-minimal.tsx @@ -0,0 +1,11 @@ +"use client" +import React from 'react' + +export default function DashboardMinimal() { + return ( +
+

Trading Bot Dashboard

+

Welcome! Please select a feature from the menu.

+
+ ) +} diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx new file mode 100644 index 0000000..f66ce69 --- /dev/null +++ b/components/Dashboard.tsx @@ -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([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(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 ( +
+
+ + +
+
+
+

Open Positions

+ {loading ?
Loading...
: error ?
{error}
: ( + + + + + + + + + + + + {positions.map((pos, i) => ( + + + + + + + + ))} + +
SymbolSideSizeEntry PriceUnrealized PnL
{pos.symbol}{pos.side}{pos.size}{pos.entryPrice}{pos.unrealizedPnl}
+ )} +
+ +
+
+ ) +} diff --git a/components/DeveloperSettings.tsx b/components/DeveloperSettings.tsx new file mode 100644 index 0000000..a325504 --- /dev/null +++ b/components/DeveloperSettings.tsx @@ -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 ( +
+

Developer Settings

+ setEnv(e.target.value)} + /> + + {message &&
{message}
} +
+ ) +} diff --git a/components/TradingHistory.tsx b/components/TradingHistory.tsx new file mode 100644 index 0000000..a701482 --- /dev/null +++ b/components/TradingHistory.tsx @@ -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([]) + 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 ( +
+

Trading History

+ {loading ?
Loading...
: ( + + + + + + + + + + + + + {trades.map(trade => ( + + + + + + + + + ))} + +
SymbolSideAmountPriceStatusExecuted At
{trade.symbol}{trade.side}{trade.amount}{trade.price}{trade.status}{trade.executedAt}
+ )} +
+ ) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c57ef07 --- /dev/null +++ b/docker-compose.yml @@ -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"] diff --git a/lib/ai-analysis.ts b/lib/ai-analysis.ts new file mode 100644 index 0000000..2acc651 --- /dev/null +++ b/lib/ai-analysis.ts @@ -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 { + 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 { + 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() diff --git a/lib/auto-trading.ts b/lib/auto-trading.ts new file mode 100644 index 0000000..48b6cbe --- /dev/null +++ b/lib/auto-trading.ts @@ -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 = {} + + 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) { + 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 + }) +} diff --git a/lib/prisma.ts b/lib/prisma.ts new file mode 100644 index 0000000..2d4761c --- /dev/null +++ b/lib/prisma.ts @@ -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 diff --git a/lib/settings.ts b/lib/settings.ts new file mode 100644 index 0000000..c3cb4f7 --- /dev/null +++ b/lib/settings.ts @@ -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 { + 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 { + 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): Promise { + this.settings = { ...this.settings, ...updates } + await this.saveSettings() + return this.settings + } + + getSettings(): TradingSettings { + return this.settings + } + + async setSymbol(symbol: string): Promise { + await this.updateSettings({ symbol }) + } + + async setTimeframe(timeframe: string): Promise { + await this.updateSettings({ timeframe }) + } + + async setLayouts(layouts: string[]): Promise { + await this.updateSettings({ layouts }) + } + + async addLayout(layout: string): Promise { + if (!this.settings.layouts.includes(layout)) { + const layouts = [...this.settings.layouts, layout] + await this.updateSettings({ layouts }) + } + } + + async removeLayout(layout: string): Promise { + const layouts = this.settings.layouts.filter(l => l !== layout) + await this.updateSettings({ layouts }) + } +} + +export const settingsManager = SettingsManager.getInstance() diff --git a/lib/tradingview.ts b/lib/tradingview.ts new file mode 100644 index 0000000..75a8b3d --- /dev/null +++ b/lib/tradingview.ts @@ -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 { + 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() diff --git a/package-lock.json b/package-lock.json index 4cf5b4b..6327ccd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@drift-labs/sdk": "^2.126.0-beta.14", "@prisma/client": "^6.11.1", "@solana/web3.js": "^1.98.2", + "bs58": "^6.0.0", "next": "15.3.5", "openai": "^5.8.3", "prisma": "^6.11.1", @@ -148,6 +149,15 @@ "@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": { "version": "0.30.1", "resolved": "https://registry.npmjs.org/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz", @@ -156,6 +166,16 @@ "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": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.28.0.tgz", @@ -294,6 +314,15 @@ "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": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", @@ -1509,6 +1538,15 @@ "@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": { "version": "6.11.1", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.11.1.tgz", @@ -1602,6 +1640,15 @@ "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": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1807,6 +1854,15 @@ "@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": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/@pythnetwork/solana-utils/-/solana-utils-0.4.5.tgz", @@ -2333,6 +2389,15 @@ "@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": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -2401,19 +2466,6 @@ "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": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz", @@ -2439,19 +2491,6 @@ "@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": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", @@ -3841,6 +3880,15 @@ "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": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3864,13 +3912,20 @@ } }, "node_modules/bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", "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": { "version": "6.0.3", "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", "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": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", @@ -8173,6 +8237,16 @@ "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": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index f7757fd..ab13ee8 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@drift-labs/sdk": "^2.126.0-beta.14", "@prisma/client": "^6.11.1", "@solana/web3.js": "^1.98.2", + "bs58": "^6.0.0", "next": "15.3.5", "openai": "^5.8.3", "prisma": "^6.11.1", diff --git a/screenshots/BTCUSD_ai_5_1752062666039.png b/screenshots/BTCUSD_ai_5_1752062666039.png new file mode 100644 index 0000000..73545ae Binary files /dev/null and b/screenshots/BTCUSD_ai_5_1752062666039.png differ diff --git a/screenshots/SOLUSD_ai_5_1752062839851.png b/screenshots/SOLUSD_ai_5_1752062839851.png new file mode 100644 index 0000000..6ca612a Binary files /dev/null and b/screenshots/SOLUSD_ai_5_1752062839851.png differ diff --git a/scripts/convert-solana-key.js b/scripts/convert-solana-key.js new file mode 100644 index 0000000..3fdedf7 --- /dev/null +++ b/scripts/convert-solana-key.js @@ -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 '); + process.exit(1); +} +const arr = Array.from(bs58.decode(base58)); +console.log(JSON.stringify(arr)); diff --git a/scripts/convert-solana-key.mjs b/scripts/convert-solana-key.mjs new file mode 100644 index 0000000..c331483 --- /dev/null +++ b/scripts/convert-solana-key.mjs @@ -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 '); + process.exit(1); +} +const arr = Array.from(bs58.decode(base58)); +console.log(JSON.stringify(arr));