Merge development branch: improved UI, coin icons, and comprehensive documentation

- Add proper CoinGecko coin icons for BTC, ETH, SOL
- Clean up homepage layout and remove clutter
- Add comprehensive .github/copilot-instructions.md with full architecture documentation
- Include timeframe fixes, trading integration improvements, and Docker optimizations
- Maintain all trading functionality and AI analysis features
This commit is contained in:
mindesbunister
2025-07-17 12:01:15 +02:00
39 changed files with 3989 additions and 301 deletions

View File

@@ -1,3 +1,104 @@
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
This project is a Next.js 15 App Router application with TypeScript, Tailwind CSS, Prisma, and API routes. It is a trading bot dashboard with AI-powered analysis, auto-trading, trade execution, screenshot capture, and developer settings. Please generate code that is idiomatic for this stack and follows best practices for modern Next.js apps.
# AI-Powered Trading Bot Dashboard
This is a Next.js 15 App Router application with TypeScript, Tailwind CSS, and API routes. It's a production-ready trading bot with AI analysis, automated screenshot capture, and real-time trading execution via Drift Protocol and Jupiter DEX.
## Core Architecture
### Dual-Session Screenshot Automation
- **AI Layout**: `Z1TzpUrf` - RSI (top), EMAs, MACD (bottom)
- **DIY Layout**: `vWVvjLhP` - Stochastic RSI (top), VWAP, OBV (bottom)
- Parallel browser sessions for multi-layout capture in `lib/enhanced-screenshot.ts`
- TradingView automation with session persistence in `lib/tradingview-automation.ts`
### AI Analysis Pipeline
- OpenAI GPT-4o mini for cost-effective chart analysis (~$0.006 per analysis)
- Multi-layout comparison and consensus detection
- Professional trading setups with exact entry/exit levels and risk management
- Layout-specific indicator analysis (RSI vs Stochastic RSI, MACD vs OBV)
### Trading Integration
- **Drift Protocol**: Perpetual futures trading via `@drift-labs/sdk`
- **Jupiter DEX**: Spot trading on Solana
- Position management and P&L tracking
- Real-time account balance and collateral monitoring
## Critical Development Patterns
### Docker Environment
Use Docker for consistency: `npm run docker:dev` (port 9001) or `npm run docker:up` (port 9000)
- Multi-stage builds with browser automation optimizations
- Session persistence via volume mounts
- Chromium path: `/usr/bin/chromium`
### API Route Structure
```typescript
// Enhanced screenshot with progress tracking
POST /api/enhanced-screenshot
{
symbol: "SOLUSD",
timeframe: "240",
layouts: ["ai", "diy"],
analyze: true
}
// Returns: { screenshots, analysis, sessionId }
```
### Progress Tracking System
- `lib/progress-tracker.ts` manages real-time analysis progress
- SessionId-based tracking for multi-step operations
- Steps: init → auth → navigation → loading → capture → analysis
### Timeframe Mapping
Critical: Always use minute values first to avoid TradingView confusion
```typescript
'4h': ['240', '240m', '4h', '4H'] // 240 minutes FIRST, not "4h"
'1h': ['60', '60m', '1h', '1H'] // 60 minutes FIRST
```
### Component Architecture
- `components/AIAnalysisPanel.tsx` - Multi-timeframe analysis interface
- `components/Dashboard.tsx` - Main trading dashboard with real Drift positions
- `components/AdvancedTradingPanel.tsx` - Drift Protocol trading interface
- Layout: `app/layout.js` with gradient styling and navigation
## Environment Variables
```bash
OPENAI_API_KEY= # Required for AI analysis
TRADINGVIEW_EMAIL= # Required for automation
TRADINGVIEW_PASSWORD= # Required for automation
SOLANA_RPC_URL= # Drift trading
DRIFT_PRIVATE_KEY= # Drift trading (base58 encoded)
```
## Build & Development Commands
```bash
# Development (recommended)
npm run docker:dev # Port 9001, hot reload
npm run docker:logs # View container logs
npm run docker:exec # Shell access
# Production deployment
npm run docker:prod:up # Port 9000, optimized build
# Testing automation
node test-enhanced-screenshot.js # Test dual-session capture
./test-docker-comprehensive.sh # Full system test
```
## Code Style Guidelines
- Use `"use client"` for client components with state/effects
- Tailwind with gradient backgrounds and glassmorphism effects
- TypeScript interfaces for all trading parameters and API responses
- Error handling with detailed logging for browser automation
- Session persistence to avoid TradingView captchas
## Key Integration Points
- Session data: `.tradingview-session/` (volume mounted)
- Screenshots: `screenshots/` directory
- Progress tracking: EventEmitter-based real-time updates
- Database: Prisma with SQLite (file:./prisma/dev.db)
Generate code that follows these patterns and integrates seamlessly with the existing trading infrastructure.

View File

@@ -64,7 +64,7 @@
"value": "*",
"domain": ".tradingview.com",
"path": "/",
"expires": 1760180481,
"expires": 1760464150,
"httpOnly": false,
"secure": true,
"sameSite": "None"
@@ -79,24 +79,24 @@
"secure": false,
"sameSite": "Lax"
},
{
"name": "_sp_id.cf1a",
"value": ".1752404481.1.1752404482..f540bb6e-fa0b-496b-9640-1d4fec1e4c8e..656514fd-a0d5-4c9c-9763-be49bfa3bb6e.1752404481740.1",
"domain": ".tradingview.com",
"path": "/",
"expires": 1786964481.740595,
"httpOnly": false,
"secure": true,
"sameSite": "None"
},
{
"name": "sp",
"value": "476371e7-d9df-4cab-9f6c-9af104097490",
"domain": "snowplow-pixel.tradingview.com",
"path": "/",
"expires": 1783940482.306256,
"expires": 1784224154.521872,
"httpOnly": true,
"secure": true,
"sameSite": "None"
},
{
"name": "_sp_id.cf1a",
"value": ".1752404481.1.1752688150..f540bb6e-fa0b-496b-9640-1d4fec1e4c8e..656514fd-a0d5-4c9c-9763-be49bfa3bb6e.1752404481740.3",
"domain": ".tradingview.com",
"path": "/",
"expires": 1787248149.806463,
"httpOnly": false,
"secure": true,
"sameSite": "None"
}
]

View File

@@ -1,17 +1,11 @@
{
"localStorage": {
"cookie_dialog_tracked": "1",
"snowplowOutQueue_tv_cf_post2.expires": "1815476481741",
"tvlocalstorage.available": "true",
"featuretoggle_seed": "983912",
"first_visit_time": "1752404384075",
"snowplowOutQueue_tv_cf_post2": "[]",
"auto-show-email-for-signin": "1",
"snowplowOutQueue_tv_cf_post2.expires": "1815760150522",
"tvlocalstorage.available": "true",
"last-crosstab-monotonic-timestamp": "1752688195853",
"trial_availiable": "0",
"_grecaptcha": "09ANMylNBaIGTVsUxDDAB11tBaVKRevHvUG6E16KDP4nm97sYkmHshpfmxoAkcFfbj7mFb3zg4rncfzqc6A9g-20ErWdRAoBN59yOsTLvoV3Oc39otwCTzVeNmXMQoHwHs",
"last-crosstab-monotonic-timestamp": "1752404482390",
"last_username": "mindesbunister",
"signupSource": "auth page tvd"
"last_username": "mindesbunister"
},
"sessionStorage": {}
}

View File

@@ -0,0 +1,57 @@
# 🔄 Docker Container Restart Required
## Why Restart is Needed
You need to restart your Docker container because we made the following changes:
1. **Changed API imports**: Updated `/app/api/enhanced-screenshot/route.js` to use `enhanced-screenshot.ts` instead of `enhanced-screenshot-simple.ts`
2. **Added progress tracking**: The new implementation uses real-time progress tracking with EventEmitter
3. **New API endpoint**: Added `/app/api/progress/[sessionId]/stream/route.ts` for Server-Sent Events
4. **Updated service logic**: Modified core screenshot capture logic with progress tracking
## Restart Commands
### For Development Environment:
```bash
# Stop current containers
npm run docker:down
# Rebuild and start with development config
npm run docker:dev
# Or if you want to see logs immediately:
npm run docker:up:build
```
### For Production Environment:
```bash
# Stop production containers
npm run docker:prod:down
# Rebuild and start production
npm run docker:prod:build
npm run docker:prod:up
```
## What to Expect After Restart
**Real-time progress tracking**: Progress bar will update as each step completes
**Smooth animations**: CSS animations will work with the new progress data
**Live step updates**: Each step (init, auth, navigation, loading, capture, analysis) will show in real-time
**Better user experience**: No more waiting for the entire process to complete before seeing progress
## Troubleshooting
If animations still don't work after restart:
1. **Check browser console** for JavaScript errors
2. **Verify EventSource connection** in Network tab (should see `progress/[sessionId]/stream`)
3. **Check Docker logs**: `npm run docker:logs`
4. **Force rebuild**: `npm run docker:build:no-cache && npm run docker:up`
## Test the Fix
1. Go to `/analysis` page
2. Select a symbol (e.g., BTCUSD)
3. Click "Analyze"
4. You should see progress steps moving in real-time as they complete

59
SCREENSHOT_PATH_FIXES.md Normal file
View File

@@ -0,0 +1,59 @@
# 🔧 Screenshot Path & SSE Fixes
## Issues Fixed
### 1. Screenshot File Path Duplication
**Problem**: Screenshot paths were showing `/app/screenshots/app/screenshots/...` causing ENOENT errors.
**Root Cause**: The `takeScreenshot()` method returns a full absolute path, but `analyzeScreenshot()` was treating it as a filename and joining it with the screenshots directory again.
**Solution**: Updated both `analyzeScreenshot()` and `analyzeMultipleScreenshots()` methods to handle both full paths and filenames:
```typescript
// Check if it's already a full path or just a filename
if (path.isAbsolute(filenameOrPath)) {
// It's already a full path
imagePath = filenameOrPath
} else {
// It's just a filename, construct the full path
const screenshotsDir = path.join(process.cwd(), 'screenshots')
imagePath = path.join(screenshotsDir, filenameOrPath)
}
```
### 2. Next.js Async Params Issue
**Problem**: SSE endpoint was using `params.sessionId` without awaiting it, causing Next.js 15 error.
**Solution**: Updated the SSE endpoint to properly await params:
```typescript
// Before
{ params }: { params: { sessionId: string } }
const { sessionId } = params
// After
{ params }: { params: Promise<{ sessionId: string }> }
const { sessionId } = await params
```
## Files Modified
1. **`/lib/ai-analysis.ts`**:
- Fixed `analyzeScreenshot()` to handle full paths
- Fixed `analyzeMultipleScreenshots()` to handle full paths
- Updated variable references
2. **`/app/api/progress/[sessionId]/stream/route.ts`**:
- Added proper async/await for params in Next.js 15
## Testing
After these fixes:
✅ Screenshots should save and load correctly
✅ AI analysis should find screenshot files
✅ SSE progress tracking should work without Next.js warnings
✅ Real-time progress updates should function properly
## No Docker Restart Required
These are runtime fixes that don't require a Docker container restart. The application should work immediately after the files are updated.

85
app/api-test/page.tsx Normal file
View File

@@ -0,0 +1,85 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
export default function ChartAPITest() {
const chartContainerRef = useRef<HTMLDivElement>(null)
const [logs, setLogs] = useState<string[]>([])
const addLog = (message: string) => {
console.log(message)
setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`])
}
useEffect(() => {
if (!chartContainerRef.current) return
const testLightweightCharts = async () => {
try {
addLog('Importing lightweight-charts...')
const LightweightCharts = await import('lightweight-charts')
addLog('Import successful')
// Log what's available in the import
addLog('Available exports: ' + Object.keys(LightweightCharts).join(', '))
const { createChart } = LightweightCharts
addLog('createChart function available: ' + (typeof createChart))
// Create chart
const chart = createChart(chartContainerRef.current!, {
width: 600,
height: 300,
})
addLog('Chart created')
// Log chart methods
const chartMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(chart))
addLog('Chart methods: ' + chartMethods.join(', '))
// Try to find the correct method for adding series
if ('addCandlestickSeries' in chart) {
addLog('addCandlestickSeries method found!')
} else if ('addCandles' in chart) {
addLog('addCandles method found!')
} else if ('addSeries' in chart) {
addLog('addSeries method found!')
} else {
addLog('No obvious candlestick method found')
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
addLog(`Error: ${errorMessage}`)
console.error('Error:', error)
}
}
testLightweightCharts()
}, [])
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-2xl mb-4">Lightweight Charts API Test</h1>
<div className="mb-4">
<div
ref={chartContainerRef}
className="bg-gray-800 border border-gray-600 rounded"
style={{ width: '600px', height: '300px' }}
/>
</div>
<div>
<h2 className="text-white text-lg mb-2">API Investigation Logs</h2>
<div className="bg-gray-800 p-4 rounded max-h-96 overflow-y-auto">
{logs.map((log, index) => (
<div key={index} className="text-gray-300 text-sm font-mono mb-1">
{log}
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -1,19 +1,70 @@
import { NextResponse } from 'next/server'
import { enhancedScreenshotService } from '../../../lib/enhanced-screenshot-simple'
import { enhancedScreenshotService } from '../../../lib/enhanced-screenshot'
import { aiAnalysisService } from '../../../lib/ai-analysis'
import { progressTracker } from '../../../lib/progress-tracker'
export async function POST(request) {
try {
const body = await request.json()
const { symbol, layouts, timeframes, selectedLayouts, analyze = true } = body
const { symbol, layouts, timeframe, timeframes, selectedLayouts, analyze = true } = body
console.log('📊 Enhanced screenshot request:', { symbol, layouts, timeframes, selectedLayouts })
console.log('📊 Enhanced screenshot request:', { symbol, layouts, timeframe, timeframes, selectedLayouts })
// Generate unique session ID for progress tracking
const sessionId = `analysis_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
console.log('🔍 Created session ID:', sessionId)
// Create progress tracking session with initial steps
const initialSteps = [
{
id: 'init',
title: 'Initializing Analysis',
description: 'Starting AI-powered trading analysis...',
status: 'pending'
},
{
id: 'auth',
title: 'TradingView Authentication',
description: 'Logging into TradingView accounts',
status: 'pending'
},
{
id: 'navigation',
title: 'Chart Navigation',
description: 'Navigating to chart layouts',
status: 'pending'
},
{
id: 'loading',
title: 'Chart Data Loading',
description: 'Waiting for chart data and indicators',
status: 'pending'
},
{
id: 'capture',
title: 'Screenshot Capture',
description: 'Capturing high-quality screenshots',
status: 'pending'
},
{
id: 'analysis',
title: 'AI Analysis',
description: 'Analyzing screenshots with AI',
status: 'pending'
}
]
// Create the progress session
console.log('🔍 Creating progress session with steps:', initialSteps.length)
progressTracker.createSession(sessionId, initialSteps)
console.log('🔍 Progress session created successfully')
// Prepare configuration for screenshot service
const config = {
symbol: symbol || 'BTCUSD',
timeframe: timeframes?.[0] || '60', // Use first timeframe for now
timeframe: timeframe || timeframes?.[0] || '60', // Use single timeframe, fallback to first of array, then default
layouts: layouts || selectedLayouts || ['ai'],
sessionId, // Pass session ID for progress tracking
credentials: {
email: process.env.TRADINGVIEW_EMAIL,
password: process.env.TRADINGVIEW_PASSWORD
@@ -22,35 +73,32 @@ export async function POST(request) {
console.log('🔧 Using config:', config)
// Capture screenshots using the working service
const screenshots = await enhancedScreenshotService.captureWithLogin(config)
console.log('📸 Screenshots captured:', screenshots)
let screenshots = []
let analysis = null
// Perform AI analysis if requested and screenshots were captured
if (analyze && screenshots.length > 0) {
// Perform AI analysis if requested
if (analyze) {
try {
console.log('🤖 Starting AI analysis...')
// Extract just the filenames from full paths
const filenames = screenshots.map(path => path.split('/').pop())
if (filenames.length === 1) {
analysis = await aiAnalysisService.analyzeScreenshot(filenames[0])
} else {
analysis = await aiAnalysisService.analyzeMultipleScreenshots(filenames)
}
console.log('✅ AI analysis completed')
console.log('🤖 Starting automated capture and analysis...')
const result = await aiAnalysisService.captureAndAnalyzeWithConfig(config, sessionId)
screenshots = result.screenshots
analysis = result.analysis
console.log('✅ Automated capture and analysis completed')
} catch (analysisError) {
console.error('❌ AI analysis failed:', analysisError)
// Continue without analysis rather than failing the whole request
console.error('❌ Automated capture and analysis failed:', analysisError)
// Fall back to screenshot only
screenshots = await enhancedScreenshotService.captureWithLogin(config, sessionId)
}
} else {
// Capture screenshots only
screenshots = await enhancedScreenshotService.captureWithLogin(config, sessionId)
}
console.log('📸 Final screenshots:', screenshots)
const result = {
success: true,
sessionId, // Return session ID for progress tracking
timestamp: Date.now(),
symbol: config.symbol,
layouts: config.layouts,

View File

@@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from 'next/server'
import { progressTracker } from '../../../../../lib/progress-tracker'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const { sessionId } = await params
// Create a readable stream for Server-Sent Events
const encoder = new TextEncoder()
const stream = new ReadableStream({
start(controller) {
// Send initial progress if session exists
const initialProgress = progressTracker.getProgress(sessionId)
if (initialProgress) {
const data = `data: ${JSON.stringify(initialProgress)}\n\n`
controller.enqueue(encoder.encode(data))
}
// Listen for progress updates
const progressHandler = (progress: any) => {
const data = `data: ${JSON.stringify(progress)}\n\n`
controller.enqueue(encoder.encode(data))
}
// Listen for completion
const completeHandler = () => {
const data = `data: ${JSON.stringify({ type: 'complete' })}\n\n`
controller.enqueue(encoder.encode(data))
controller.close()
}
// Subscribe to events
progressTracker.on(`progress:${sessionId}`, progressHandler)
progressTracker.on(`progress:${sessionId}:complete`, completeHandler)
// Cleanup on stream close
request.signal.addEventListener('abort', () => {
progressTracker.off(`progress:${sessionId}`, progressHandler)
progressTracker.off(`progress:${sessionId}:complete`, completeHandler)
controller.close()
})
}
})
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Cache-Control'
}
})
}

52
app/api/progress/route.ts Normal file
View File

@@ -0,0 +1,52 @@
import { progressTracker } from '../../../lib/progress-tracker'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const sessionId = searchParams.get('sessionId')
if (!sessionId) {
return new Response('Session ID required', { status: 400 })
}
const stream = new ReadableStream({
start(controller) {
// Send initial progress if session exists
const currentProgress = progressTracker.getProgress(sessionId)
if (currentProgress) {
const data = `data: ${JSON.stringify(currentProgress)}\n\n`
controller.enqueue(new TextEncoder().encode(data))
}
// Listen for progress updates
const progressHandler = (progress: any) => {
const data = `data: ${JSON.stringify(progress)}\n\n`
controller.enqueue(new TextEncoder().encode(data))
}
const completeHandler = () => {
const data = `data: ${JSON.stringify({ type: 'complete' })}\n\n`
controller.enqueue(new TextEncoder().encode(data))
controller.close()
}
progressTracker.on(`progress:${sessionId}`, progressHandler)
progressTracker.on(`progress:${sessionId}:complete`, completeHandler)
// Cleanup on close
return () => {
progressTracker.off(`progress:${sessionId}`, progressHandler)
progressTracker.off(`progress:${sessionId}:complete`, completeHandler)
}
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control'
}
})
}

View File

@@ -7,6 +7,7 @@ export async function POST(request) {
symbol,
side,
amount,
amountUSD,
stopLoss,
takeProfit,
useRealDEX = false,
@@ -17,10 +18,16 @@ export async function POST(request) {
toCoin
} = body
// For Docker environment, use internal port 3000. For dev, use the host header
const host = request.headers.get('host') || 'localhost:3000'
const isDocker = process.env.DOCKER_ENV === 'true'
const baseUrl = isDocker ? 'http://localhost:3000' : `http://${host}`
console.log('🔄 Execute DEX trade request:', {
symbol,
side,
amount,
amountUSD,
stopLoss,
takeProfit,
useRealDEX,
@@ -64,13 +71,14 @@ export async function POST(request) {
console.log('🔍 Validating wallet balance before DEX trade...')
try {
const validationResponse = await fetch('http://localhost:3000/api/trading/validate', {
const validationResponse = await fetch(`${baseUrl}/api/trading/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
symbol,
side,
amount,
amountUSD,
tradingMode: 'SPOT',
fromCoin,
toCoin
@@ -194,7 +202,7 @@ export async function POST(request) {
// Add trade to history with clear spot swap indication
try {
// Use localhost for internal container communication
await fetch('http://localhost:3000/api/trading/history', {
await fetch(`${baseUrl}/api/trading/history`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({

View File

@@ -0,0 +1,311 @@
import { NextResponse } from 'next/server'
export async function POST(request) {
try {
const body = await request.json()
const {
symbol,
side,
amount,
leverage = 1,
stopLoss,
takeProfit,
useRealDEX = false
} = body
console.log('🔥 Drift Perpetuals trade request:', {
symbol,
side,
amount,
leverage,
stopLoss,
takeProfit,
useRealDEX
})
// Validate inputs
if (!symbol || !side || !amount) {
return NextResponse.json(
{
success: false,
error: 'Missing required fields: symbol, side, amount'
},
{ status: 400 }
)
}
if (!['BUY', 'SELL', 'LONG', 'SHORT'].includes(side.toUpperCase())) {
return NextResponse.json(
{
success: false,
error: 'Invalid side. Must be LONG/SHORT or BUY/SELL'
},
{ status: 400 }
)
}
if (amount <= 0) {
return NextResponse.json(
{
success: false,
error: 'Amount must be greater than 0'
},
{ status: 400 }
)
}
if (leverage < 1 || leverage > 10) {
return NextResponse.json(
{
success: false,
error: 'Leverage must be between 1x and 10x'
},
{ status: 400 }
)
}
if (!useRealDEX) {
// Simulation mode
console.log('🎮 Executing SIMULATED Drift perpetual trade')
const currentPrice = symbol === 'SOL' ? 166.75 : symbol === 'BTC' ? 121819 : 3041.66
const leveragedAmount = amount * leverage
const entryFee = leveragedAmount * 0.001 // 0.1% opening fee
const liquidationPrice = side.toUpperCase().includes('LONG') || side.toUpperCase() === 'BUY'
? currentPrice * (1 - 0.9 / leverage) // Approximate liquidation price
: currentPrice * (1 + 0.9 / leverage)
await new Promise(resolve => setTimeout(resolve, 1200))
return NextResponse.json({
success: true,
trade: {
txId: `drift_sim_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`,
orderId: `drift_order_${Date.now()}`,
symbol: symbol.toUpperCase(),
side: side.toUpperCase(),
positionSize: amount,
leverage: leverage,
leveragedAmount: leveragedAmount,
entryPrice: currentPrice,
liquidationPrice: liquidationPrice,
entryFee: entryFee,
timestamp: Date.now(),
status: 'OPEN',
platform: 'Drift Protocol (Simulation)',
stopLoss: stopLoss,
takeProfit: takeProfit,
monitoring: !!(stopLoss || takeProfit),
pnl: 0
},
message: `${side.toUpperCase()} perpetual position opened: $${amount} at ${leverage}x leverage - SIMULATED`
})
}
// Real Drift trading implementation
console.log('💰 Executing REAL Drift perpetual trade')
// Import Drift SDK components
const { DriftClient, initialize, MarketType, PositionDirection, OrderType } = await import('@drift-labs/sdk')
const { Connection, Keypair } = await import('@solana/web3.js')
const { Wallet } = await import('@coral-xyz/anchor')
// Initialize connection and wallet
const connection = new Connection(
process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
'confirmed'
)
if (!process.env.SOLANA_PRIVATE_KEY) {
return NextResponse.json({
success: false,
error: 'Drift trading not configured - missing SOLANA_PRIVATE_KEY'
}, { status: 400 })
}
const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY)
const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray))
const wallet = new Wallet(keypair)
console.log('🚀 Initializing Drift client...')
// Initialize Drift SDK
const env = 'mainnet-beta'
const sdkConfig = initialize({ env })
const driftClient = new DriftClient({
connection,
wallet,
programID: sdkConfig.DRIFT_PROGRAM_ID,
opts: {
commitment: 'confirmed',
},
})
await driftClient.subscribe()
try {
// Get market index for the symbol
const marketIndex = symbol === 'SOL' ? 0 : symbol === 'BTC' ? 1 : 0 // SOL-PERP is typically index 0
// Determine position direction
const direction = side.toUpperCase().includes('LONG') || side.toUpperCase() === 'BUY'
? PositionDirection.LONG
: PositionDirection.SHORT
// Calculate position size in base asset units
const currentPrice = 166.75 // Get from oracle in production
const baseAssetAmount = (amount * leverage) / currentPrice * 1e9 // Convert to lamports for SOL
console.log('📊 Trade parameters:', {
marketIndex,
direction: direction === PositionDirection.LONG ? 'LONG' : 'SHORT',
baseAssetAmount: baseAssetAmount.toString(),
leverage
})
// Place market order
const orderParams = {
orderType: OrderType.MARKET,
marketType: MarketType.PERP,
direction,
baseAssetAmount: Math.floor(baseAssetAmount),
marketIndex,
}
console.log('🎯 Placing Drift market order...')
const txSig = await driftClient.placeOrder(orderParams)
console.log('✅ Drift order placed:', txSig)
// Set up stop loss and take profit if specified
let stopLossOrderId = null
let takeProfitOrderId = null
if (stopLoss) {
try {
const stopLossParams = {
orderType: OrderType.LIMIT,
marketType: MarketType.PERP,
direction: direction === PositionDirection.LONG ? PositionDirection.SHORT : PositionDirection.LONG,
baseAssetAmount: Math.floor(baseAssetAmount),
price: stopLoss * 1e6, // Price in 6 decimal format
marketIndex,
triggerPrice: stopLoss * 1e6,
triggerCondition: direction === PositionDirection.LONG ? 'below' : 'above',
}
const slTxSig = await driftClient.placeOrder(stopLossParams)
stopLossOrderId = slTxSig
console.log('🛑 Stop loss order placed:', slTxSig)
} catch (slError) {
console.warn('⚠️ Stop loss order failed:', slError.message)
}
}
if (takeProfit) {
try {
const takeProfitParams = {
orderType: OrderType.LIMIT,
marketType: MarketType.PERP,
direction: direction === PositionDirection.LONG ? PositionDirection.SHORT : PositionDirection.LONG,
baseAssetAmount: Math.floor(baseAssetAmount),
price: takeProfit * 1e6, // Price in 6 decimal format
marketIndex,
triggerPrice: takeProfit * 1e6,
triggerCondition: direction === PositionDirection.LONG ? 'above' : 'below',
}
const tpTxSig = await driftClient.placeOrder(takeProfitParams)
takeProfitOrderId = tpTxSig
console.log('🎯 Take profit order placed:', tpTxSig)
} catch (tpError) {
console.warn('⚠️ Take profit order failed:', tpError.message)
}
}
// Calculate liquidation price
const liquidationPrice = direction === PositionDirection.LONG
? currentPrice * (1 - 0.9 / leverage)
: currentPrice * (1 + 0.9 / leverage)
const result = {
success: true,
trade: {
txId: txSig,
orderId: `drift_${Date.now()}`,
symbol: symbol.toUpperCase(),
side: direction === PositionDirection.LONG ? 'LONG' : 'SHORT',
positionSize: amount,
leverage: leverage,
leveragedAmount: amount * leverage,
entryPrice: currentPrice,
liquidationPrice: liquidationPrice,
entryFee: (amount * leverage) * 0.001,
timestamp: Date.now(),
status: 'PENDING',
platform: 'Drift Protocol',
dex: 'DRIFT_REAL',
stopLoss: stopLoss,
takeProfit: takeProfit,
stopLossOrderId: stopLossOrderId,
takeProfitOrderId: takeProfitOrderId,
monitoring: !!(stopLoss || takeProfit),
pnl: 0
},
message: `${direction === PositionDirection.LONG ? 'LONG' : 'SHORT'} perpetual position opened: $${amount} at ${leverage}x leverage`,
warnings: [
`⚠️ Liquidation risk at $${liquidationPrice.toFixed(4)}`,
'📊 Position requires active monitoring',
'💰 Real funds at risk'
]
}
return NextResponse.json(result)
} finally {
// Clean up
try {
await driftClient.unsubscribe()
} catch (e) {
console.warn('⚠️ Cleanup warning:', e.message)
}
}
} catch (error) {
console.error('❌ Drift perpetual trade execution error:', error)
return NextResponse.json(
{
success: false,
error: 'Internal server error',
message: `Failed to execute Drift perpetual trade: ${error.message}`,
details: error.message
},
{ status: 500 }
)
}
}
export async function GET() {
return NextResponse.json({
message: 'Drift Protocol Perpetuals Trading API',
endpoints: {
'POST /api/trading/execute-drift': 'Execute real perpetual trades via Drift Protocol',
},
status: 'Active',
features: [
'Real leveraged perpetual trading (1x-10x)',
'Long/Short positions with liquidation risk',
'Stop Loss & Take Profit orders',
'Real-time position tracking',
'Automatic margin management'
],
requirements: [
'SOLANA_PRIVATE_KEY environment variable',
'Sufficient USDC collateral in Drift account',
'Active Drift user account'
],
note: 'This API executes real trades with real money and liquidation risk. Use with caution.'
})
}

View File

@@ -0,0 +1,230 @@
import { NextResponse } from 'next/server'
import { jupiterDEXService } from '@/lib/jupiter-dex-service'
import { jupiterTriggerService } from '@/lib/jupiter-trigger-service'
export async function POST(request) {
try {
const body = await request.json()
const {
symbol,
side,
amount,
leverage = 1,
stopLoss,
takeProfit,
useRealDEX = false
} = body
console.log('🚀 Jupiter Leveraged Spot Trade request:', {
symbol,
side,
amount,
leverage,
stopLoss,
takeProfit,
useRealDEX
})
// Validate inputs
if (!symbol || !side || !amount) {
return NextResponse.json(
{
success: false,
error: 'Missing required fields: symbol, side, amount'
},
{ status: 400 }
)
}
if (!['BUY', 'SELL'].includes(side.toUpperCase())) {
return NextResponse.json(
{
success: false,
error: 'Invalid side. Must be BUY or SELL'
},
{ status: 400 }
)
}
if (amount <= 0) {
return NextResponse.json(
{
success: false,
error: 'Amount must be greater than 0'
},
{ status: 400 }
)
}
if (leverage < 1 || leverage > 10) {
return NextResponse.json(
{
success: false,
error: 'Leverage must be between 1x and 10x'
},
{ status: 400 }
)
}
if (!useRealDEX) {
// Simulation mode
console.log('🎮 Executing SIMULATED leveraged spot trade')
const currentPrice = symbol === 'SOL' ? 166.75 : symbol === 'BTC' ? 121819 : 3041.66
const leveragedAmount = amount * leverage
const estimatedTokens = side === 'BUY' ? leveragedAmount / currentPrice : leveragedAmount
await new Promise(resolve => setTimeout(resolve, 1200))
return NextResponse.json({
success: true,
trade: {
txId: `jupiter_leverage_sim_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`,
symbol: symbol.toUpperCase(),
side: side.toUpperCase(),
amount: leveragedAmount,
leverage: leverage,
originalAmount: amount,
estimatedTokens: estimatedTokens,
entryPrice: currentPrice,
timestamp: Date.now(),
status: 'FILLED',
platform: 'Jupiter DEX (Leveraged Spot)',
stopLoss: stopLoss,
takeProfit: takeProfit,
triggerOrders: stopLoss || takeProfit ? 'PENDING' : 'NONE'
},
message: `${side.toUpperCase()} ${leveragedAmount} USD worth of ${symbol} (${leverage}x leveraged spot trade) - SIMULATED`
})
}
// Real trading with Jupiter DEX + Trigger Orders
console.log('💰 Executing REAL leveraged spot trade via Jupiter DEX + Trigger Orders')
// Step 1: Execute the main trade with leveraged amount
const leveragedAmount = amount * leverage
const tradingPair = symbol === 'SOL' ? (side === 'BUY' ? 'USDC/SOL' : 'SOL/USDC') : 'SOL/USDC'
const tradeResult = await jupiterDEXService.executeTrade({
symbol,
side,
amount: leveragedAmount,
tradingPair,
quickSwap: false
})
if (!tradeResult.success) {
return NextResponse.json({
success: false,
error: `Main trade failed: ${tradeResult.error}`
}, { status: 400 })
}
console.log('✅ Main leveraged trade executed:', tradeResult.txId)
// Step 2: Calculate position size for trigger orders
const currentPrice = symbol === 'SOL' ? 166.75 : 3041.66 // Get from price API in production
const tokenAmount = side === 'BUY'
? leveragedAmount / currentPrice // If buying SOL, calculate SOL amount
: leveragedAmount // If selling, amount is already in the token
// Step 3: Create trigger orders for stop loss and take profit
let triggerResults = null
if (stopLoss || takeProfit) {
console.log('📋 Creating trigger orders for TP/SL...')
triggerResults = await jupiterTriggerService.createTradingOrders({
tokenSymbol: symbol,
amount: tokenAmount,
stopLoss: stopLoss,
takeProfit: takeProfit,
slippageBps: 50, // 0.5% slippage for trigger orders
expiredAt: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30 days expiry
})
if (triggerResults.success) {
console.log('✅ Trigger orders created:', {
stopLoss: triggerResults.stopLossOrder,
takeProfit: triggerResults.takeProfitOrder
})
} else {
console.warn('⚠️ Trigger orders failed:', triggerResults.error)
}
}
// Step 4: Return comprehensive result
const result = {
success: true,
trade: {
txId: tradeResult.txId,
orderId: tradeResult.orderId,
symbol: symbol.toUpperCase(),
side: side.toUpperCase(),
amount: leveragedAmount,
leverage: leverage,
originalAmount: amount,
tokenAmount: tokenAmount,
entryPrice: currentPrice,
timestamp: Date.now(),
status: 'FILLED',
platform: 'Jupiter DEX (Leveraged Spot)',
dex: 'JUPITER_DEX_REAL',
stopLoss: stopLoss,
takeProfit: takeProfit
},
triggerOrders: triggerResults ? {
stopLossOrderId: triggerResults.stopLossOrder,
takeProfitOrderId: triggerResults.takeProfitOrder,
status: triggerResults.success ? 'CREATED' : 'FAILED',
error: triggerResults.error
} : null,
message: `${side.toUpperCase()} $${leveragedAmount} worth of ${symbol} executed successfully`,
explanation: [
`🔥 Leveraged Spot Trade: Used ${leverage}x leverage to trade $${leveragedAmount} instead of $${amount}`,
`💰 Main Trade: ${side === 'BUY' ? 'Bought' : 'Sold'} ~${tokenAmount.toFixed(6)} ${symbol} via Jupiter DEX`,
stopLoss ? `🛑 Stop Loss: Trigger order created at $${stopLoss}` : null,
takeProfit ? `🎯 Take Profit: Trigger order created at $${takeProfit}` : null,
`📈 This gives you ${leverage}x exposure to ${symbol} price movements using spot trading`
].filter(Boolean)
}
return NextResponse.json(result)
} catch (error) {
console.error('❌ Leveraged spot trade execution error:', error)
return NextResponse.json(
{
success: false,
error: 'Internal server error',
message: 'Failed to execute leveraged spot trade. Please try again.'
},
{ status: 500 }
)
}
}
export async function GET() {
return NextResponse.json({
message: 'Jupiter Leveraged Spot Trading API',
description: 'Leveraged trading using Jupiter DEX spot swaps + Trigger Orders for TP/SL',
endpoints: {
'POST /api/trading/execute-leverage': 'Execute leveraged spot trades with trigger orders',
},
features: [
'Leveraged spot trading (1x-10x)',
'Direct wallet trading (no deposits needed)',
'Jupiter Trigger Orders for Stop Loss & Take Profit',
'Real-time execution via Jupiter DEX',
'Automatic position monitoring'
],
advantages: [
'✅ No fund deposits required (unlike Drift)',
'✅ Real leverage effect through increased position size',
'✅ Professional stop loss & take profit via Jupiter Triggers',
'✅ Best execution through Jupiter routing',
'✅ Low fees (0.03% for stables, 0.1% others)'
],
note: 'Uses Jupiter DEX for main trades and Jupiter Trigger API for stop loss/take profit orders.'
})
}

View File

@@ -106,6 +106,11 @@ export async function POST(request) {
const body = await request.json()
const { action, orderId, ...orderData } = body
// For Docker environment, use internal port 3000. For dev, use the host header
const host = request.headers.get('host') || 'localhost:3000'
const isDocker = process.env.DOCKER_ENV === 'true'
const baseUrl = isDocker ? 'http://localhost:3000' : `http://${host}`
if (action === 'add') {
// Load existing orders
const pendingOrders = loadPendingOrders()
@@ -187,7 +192,7 @@ export async function POST(request) {
try {
// Execute the trade by calling the trading API
const tradeResponse = await fetch('http://localhost:3000/api/trading', {
const tradeResponse = await fetch(`${baseUrl}/api/trading`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({

View File

@@ -3,14 +3,19 @@ import { NextResponse } from 'next/server'
export async function POST(request) {
try {
const body = await request.json()
const { symbol, side, amount, price, tradingMode = 'SPOT', fromCoin, toCoin } = body
const { symbol, side, amount, amountUSD, price, tradingMode = 'SPOT', fromCoin, toCoin } = body
console.log(`🔍 Validating trade: ${side} ${amount} ${symbol}`)
console.log(`🔍 Validating trade: ${side} ${amount} ${symbol} (USD: ${amountUSD})`)
// For Docker environment, use internal port 3000. For dev, use the host header
const host = request.headers.get('host') || 'localhost:3000'
const isDocker = process.env.DOCKER_ENV === 'true'
const baseUrl = isDocker ? 'http://localhost:3000' : `http://${host}`
// Fetch real wallet balance from the wallet API
let walletBalance
try {
const walletResponse = await fetch('http://localhost:3000/api/wallet/balance')
const walletResponse = await fetch(`${baseUrl}/api/wallet/balance`)
const walletData = await walletResponse.json()
if (walletData.success && walletData.wallet) {
@@ -42,15 +47,16 @@ export async function POST(request) {
if (tradingMode === 'SPOT') {
if (side.toUpperCase() === 'BUY') {
// For BUY orders, need USDC or USD equivalent
const tradePrice = price || 166.5 // Use provided price or current SOL price
requiredBalance = amount * tradePrice
// For BUY orders, use the USD amount directly (not amount * price)
requiredBalance = amountUSD || (amount * (price || 166.5))
requiredCurrency = 'USD'
availableBalance = walletBalance.usdValue
console.log(`💰 BUY validation: Need $${requiredBalance} USD, Have $${availableBalance}`)
} else {
// For SELL orders, need the actual token
// For SELL orders, need the actual token amount
requiredBalance = amount
requiredCurrency = fromCoin || symbol
requiredCurrency = fromCoin || symbol.replace('USD', '')
// Find the token balance
const tokenPosition = walletBalance.positions.find(pos =>
@@ -59,14 +65,16 @@ export async function POST(request) {
)
availableBalance = tokenPosition ? tokenPosition.amount : walletBalance.solBalance
console.log(`💰 SELL validation: Need ${requiredBalance} ${requiredCurrency}, Have ${availableBalance}`)
}
} else if (tradingMode === 'PERP') {
// For perpetuals, only need margin
const leverage = 10 // Default leverage
const tradePrice = price || 166.5
requiredBalance = (amount * tradePrice) / leverage
requiredBalance = (amountUSD || (amount * (price || 166.5))) / leverage
requiredCurrency = 'USD'
availableBalance = walletBalance.usdValue
console.log(`💰 PERP validation: Need $${requiredBalance} USD margin, Have $${availableBalance}`)
}
console.log(`💰 Balance check: Need ${requiredBalance} ${requiredCurrency}, Have ${availableBalance}`)

12
app/canvas-chart/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
'use client'
import React from 'react'
import SimpleTradingChart from '../../components/SimpleTradingChart'
export default function SimpleChartPage() {
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-3xl mb-6">Simple Canvas Chart Test</h1>
<SimpleTradingChart symbol="SOL/USDC" positions={[]} />
</div>
)
}

96
app/cdn-test/page.tsx Normal file
View File

@@ -0,0 +1,96 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
export default function StandaloneTest() {
const chartContainerRef = useRef<HTMLDivElement>(null)
const [status, setStatus] = useState('Starting...')
useEffect(() => {
const initChart = async () => {
try {
setStatus('Testing CDN version...')
// Try using the CDN version instead
if (typeof window !== 'undefined' && !(window as any).LightweightCharts) {
setStatus('Loading CDN script...')
const script = document.createElement('script')
script.src = 'https://unpkg.com/lightweight-charts@5.0.8/dist/lightweight-charts.standalone.production.js'
script.onload = () => {
setStatus('CDN loaded, creating chart...')
createChartWithCDN()
}
script.onerror = () => {
setStatus('CDN load failed')
}
document.head.appendChild(script)
} else if ((window as any).LightweightCharts) {
setStatus('CDN already loaded, creating chart...')
createChartWithCDN()
}
} catch (error) {
console.error('Error:', error)
setStatus(`Error: ${error}`)
}
}
const createChartWithCDN = () => {
try {
if (!chartContainerRef.current) {
setStatus('No container')
return
}
const { createChart, CandlestickSeries } = (window as any).LightweightCharts
setStatus('Creating chart with CDN...')
const chart = createChart(chartContainerRef.current, {
width: 800,
height: 400,
layout: {
background: { color: '#1a1a1a' },
textColor: '#ffffff',
},
})
setStatus('Adding series...')
const series = chart.addSeries(CandlestickSeries, {
upColor: '#26a69a',
downColor: '#ef5350',
})
setStatus('Setting data...')
series.setData([
{ time: '2025-07-14', open: 100, high: 105, low: 95, close: 102 },
{ time: '2025-07-15', open: 102, high: 107, low: 98, close: 104 },
{ time: '2025-07-16', open: 104, high: 109, low: 101, close: 106 },
])
setStatus('Chart created successfully!')
} catch (error) {
console.error('CDN chart error:', error)
setStatus(`CDN Error: ${error}`)
}
}
initChart()
}, [])
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-3xl mb-4">CDN Chart Test</h1>
<div className="text-green-400 text-lg mb-4">Status: {status}</div>
<div
ref={chartContainerRef}
className="border-2 border-blue-500 bg-gray-800"
style={{
width: '800px',
height: '400px',
}}
/>
</div>
)
}

111
app/chart-debug/page.tsx Normal file
View File

@@ -0,0 +1,111 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
export default function ChartDebug() {
const chartContainerRef = useRef<HTMLDivElement>(null)
const [logs, setLogs] = useState<string[]>([])
const [chartCreated, setChartCreated] = useState(false)
const addLog = (message: string) => {
console.log(message)
setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`])
}
useEffect(() => {
if (!chartContainerRef.current) {
addLog('Chart container ref not available')
return
}
const initChart = async () => {
try {
addLog('Starting chart initialization...')
// Import lightweight-charts
const LightweightCharts = await import('lightweight-charts')
addLog('Lightweight charts imported successfully')
const { createChart } = LightweightCharts
addLog('createChart extracted')
// Create chart with minimal options
const chart = createChart(chartContainerRef.current!, {
width: 600,
height: 300,
})
addLog('Chart created successfully')
setChartCreated(true)
// Add candlestick series with the correct v5 API
const candlestickSeries = chart.addCandlestickSeries({
upColor: '#26a69a',
downColor: '#ef5350',
borderDownColor: '#ef5350',
borderUpColor: '#26a69a',
wickDownColor: '#ef5350',
wickUpColor: '#26a69a',
})
addLog('Candlestick series added')
// Very simple test data
const testData = [
{ time: '2023-01-01', open: 100, high: 110, low: 95, close: 105 },
{ time: '2023-01-02', open: 105, high: 115, low: 100, close: 110 },
{ time: '2023-01-03', open: 110, high: 120, low: 105, close: 115 },
{ time: '2023-01-04', open: 115, high: 125, low: 110, close: 120 },
{ time: '2023-01-05', open: 120, high: 130, low: 115, close: 125 },
]
addLog(`Setting data with ${testData.length} points`)
candlestickSeries.setData(testData)
addLog('Data set successfully - chart should be visible now')
// Cleanup function
return () => {
addLog('Cleaning up chart')
chart.remove()
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
addLog(`Error: ${errorMessage}`)
console.error('Chart error:', error)
}
}
initChart()
}, [])
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-2xl mb-4">Chart Debug Test</h1>
<div className="mb-4">
<h2 className="text-white text-lg mb-2">Status</h2>
<div className="text-gray-400">
Chart Created: {chartCreated ? '✅ Yes' : '❌ No'}
</div>
</div>
<div className="mb-4">
<h2 className="text-white text-lg mb-2">Chart Container</h2>
<div
ref={chartContainerRef}
className="bg-gray-800 border border-gray-600 rounded"
style={{ width: '600px', height: '300px' }}
/>
</div>
<div>
<h2 className="text-white text-lg mb-2">Debug Logs</h2>
<div className="bg-gray-800 p-4 rounded max-h-60 overflow-y-auto">
{logs.map((log, index) => (
<div key={index} className="text-gray-300 text-sm font-mono">
{log}
</div>
))}
</div>
</div>
</div>
)
}

14
app/chart-test/page.tsx Normal file
View File

@@ -0,0 +1,14 @@
'use client'
import React from 'react'
import TradingChart from '../../components/TradingChart'
export default function SimpleChartTest() {
return (
<div className="min-h-screen bg-gray-900 p-4">
<div className="max-w-7xl mx-auto">
<h1 className="text-2xl font-bold text-white mb-6">Chart Test</h1>
<TradingChart />
</div>
</div>
)
}

198
app/chart-trading/page.tsx Normal file
View File

@@ -0,0 +1,198 @@
'use client'
import React, { useState, useEffect } from 'react'
import TradingChart from '../../components/TradingChart'
import CompactTradingPanel from '../../components/CompactTradingPanel'
import PositionsPanel from '../../components/PositionsPanel'
export default function ChartTradingPage() {
const [currentPrice, setCurrentPrice] = useState(166.21)
const [positions, setPositions] = useState([])
const [selectedSymbol, setSelectedSymbol] = useState('SOL')
useEffect(() => {
fetchPositions()
const interval = setInterval(fetchPositions, 10000) // Update every 10 seconds
return () => clearInterval(interval)
}, [])
const fetchPositions = async () => {
try {
const response = await fetch('/api/trading/positions')
const data = await response.json()
if (data.success) {
setPositions(data.positions || [])
}
} catch (error) {
console.error('Failed to fetch positions:', error)
}
}
const handleTrade = async (tradeData: any) => {
try {
console.log('Executing trade:', tradeData)
// For perpetual trades, use the execute-perp endpoint
const response = await fetch('/api/trading/execute-perp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tradeData)
})
const result = await response.json()
if (result.success) {
alert(`Trade executed successfully! ${result.message}`)
fetchPositions() // Refresh positions
} else {
alert(`Trade failed: ${result.error || result.message}`)
}
} catch (error) {
console.error('Trade execution error:', error)
alert('Trade execution failed. Please try again.')
}
}
const handlePriceUpdate = (price: number) => {
setCurrentPrice(price)
}
return (
<div className="h-screen bg-gray-900 flex flex-col">
{/* Top Bar */}
<div className="bg-gray-800 border-b border-gray-700 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-6">
<h1 className="text-xl font-bold text-white">Trading Terminal</h1>
{/* Symbol Selector */}
<div className="flex space-x-2">
{['SOL', 'BTC', 'ETH'].map(symbol => (
<button
key={symbol}
onClick={() => setSelectedSymbol(symbol)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
selectedSymbol === symbol
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{symbol}
</button>
))}
</div>
</div>
{/* Market Status */}
<div className="flex items-center space-x-4 text-sm">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span className="text-gray-300">Market Open</span>
</div>
<div className="text-gray-400">
Server Time: {new Date().toLocaleTimeString()}
</div>
</div>
</div>
</div>
{/* Main Trading Interface */}
<div className="flex-1 flex">
{/* Chart Area (70% width) */}
<div className="flex-1 p-4">
<TradingChart
symbol={selectedSymbol}
positions={positions}
onPriceUpdate={handlePriceUpdate}
/>
</div>
{/* Trading Panel (30% width) */}
<div className="w-96 border-l border-gray-700 p-4 space-y-4">
<CompactTradingPanel
symbol={selectedSymbol}
currentPrice={currentPrice}
onTrade={handleTrade}
/>
</div>
</div>
{/* Bottom Panel - Positions */}
<div className="border-t border-gray-700 bg-gray-800">
<div className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex space-x-6">
<button className="text-white font-medium border-b-2 border-blue-500 pb-2">
Positions ({positions.length})
</button>
<button className="text-gray-400 hover:text-white pb-2">
Orders
</button>
<button className="text-gray-400 hover:text-white pb-2">
History
</button>
</div>
{positions.length > 0 && (
<div className="text-sm text-gray-400">
Total P&L: <span className="text-green-400">+$0.00</span>
</div>
)}
</div>
{/* Positions Table */}
<div className="max-h-48 overflow-y-auto">
{positions.length === 0 ? (
<div className="text-center py-8 text-gray-400">
No open positions
</div>
) : (
<div className="space-y-2">
{positions.map((position: any) => (
<div
key={position.id}
className="bg-gray-900 rounded-lg p-4 flex items-center justify-between"
>
<div className="flex items-center space-x-4">
<div className={`w-3 h-3 rounded-full ${
position.side === 'BUY' ? 'bg-green-400' : 'bg-red-400'
}`}></div>
<div>
<div className="text-white font-medium">
{position.symbol} {position.side}
</div>
<div className="text-sm text-gray-400">
Size: {position.amount} Entry: ${position.entryPrice?.toFixed(2)}
</div>
</div>
</div>
<div className="text-right">
<div className="text-white font-medium">
${position.totalValue?.toFixed(2) || '0.00'}
</div>
<div className={`text-sm ${
(position.unrealizedPnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'
}`}>
{(position.unrealizedPnl || 0) >= 0 ? '+' : ''}${(position.unrealizedPnl || 0).toFixed(2)}
</div>
</div>
<div className="flex space-x-2">
<button className="px-3 py-1 bg-gray-700 text-gray-300 rounded text-sm hover:bg-gray-600">
Modify
</button>
<button className="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700">
Close
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
)
}

119
app/debug-chart/page.tsx Normal file
View File

@@ -0,0 +1,119 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
export default function DebugChart() {
const chartContainerRef = useRef<HTMLDivElement>(null)
const [logs, setLogs] = useState<string[]>([])
const [error, setError] = useState<string | null>(null)
const addLog = (message: string) => {
console.log(message)
setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`])
}
useEffect(() => {
addLog('Component mounted')
if (!chartContainerRef.current) {
addLog('ERROR: No chart container ref')
return
}
addLog('Chart container found')
const initChart = async () => {
try {
addLog('Starting chart initialization...')
addLog('Importing lightweight-charts...')
const LightweightChartsModule = await import('lightweight-charts')
addLog('Import successful')
addLog('Available exports: ' + Object.keys(LightweightChartsModule).join(', '))
const { createChart, ColorType, CrosshairMode } = LightweightChartsModule
addLog('Extracted createChart and other components')
addLog('Creating chart...')
const chart = createChart(chartContainerRef.current!, {
width: 600,
height: 300,
layout: {
textColor: '#ffffff',
background: { color: '#1a1a1a' },
},
})
addLog('Chart created successfully')
addLog('Adding candlestick series...')
const candlestickSeries = chart.addCandlestickSeries({
upColor: '#26a69a',
downColor: '#ef5350',
})
addLog('Series added successfully')
addLog('Generating test data...')
const data = [
{ time: '2025-07-10', open: 100, high: 110, low: 95, close: 105 },
{ time: '2025-07-11', open: 105, high: 115, low: 100, close: 110 },
{ time: '2025-07-12', open: 110, high: 120, low: 105, close: 115 },
{ time: '2025-07-13', open: 115, high: 125, low: 110, close: 118 },
{ time: '2025-07-14', open: 118, high: 128, low: 113, close: 122 },
{ time: '2025-07-15', open: 122, high: 132, low: 117, close: 125 },
{ time: '2025-07-16', open: 125, high: 135, low: 120, close: 130 },
]
addLog(`Generated ${data.length} data points`)
addLog('Setting data on series...')
candlestickSeries.setData(data)
addLog('Data set successfully - chart should be visible!')
addLog('Chart initialization complete')
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err)
const errorStack = err instanceof Error ? err.stack : 'No stack trace'
addLog(`ERROR: ${errorMessage}`)
console.error('Chart initialization error:', err)
setError(`${errorMessage}\n\nStack: ${errorStack}`)
}
}
initChart()
}, [])
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-2xl mb-4">Debug Chart Test</h1>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div>
<h2 className="text-white text-lg mb-2">Chart</h2>
<div
ref={chartContainerRef}
className="bg-gray-800 border border-gray-600 rounded"
style={{ width: '600px', height: '300px' }}
/>
</div>
<div>
<h2 className="text-white text-lg mb-2">Debug Logs</h2>
<div className="bg-gray-800 p-4 rounded h-80 overflow-y-auto">
{logs.map((log, index) => (
<div key={index} className="text-gray-300 text-sm font-mono mb-1">
{log}
</div>
))}
</div>
{error && (
<div className="mt-4 bg-red-900/20 border border-red-500 p-4 rounded">
<h3 className="text-red-400 font-semibold mb-2">Error Details:</h3>
<pre className="text-red-300 text-xs whitespace-pre-wrap">{error}</pre>
</div>
)}
</div>
</div>
</div>
)
}

72
app/direct-chart/page.tsx Normal file
View File

@@ -0,0 +1,72 @@
'use client'
import React, { useEffect } from 'react'
export default function DirectChart() {
useEffect(() => {
const container = document.getElementById('chart-container')
if (!container) return
const initChart = async () => {
try {
console.log('Starting direct chart...')
// Import with explicit .mjs extension
const chartModule = await import('lightweight-charts')
console.log('Module loaded:', chartModule)
const { createChart } = chartModule
console.log('Functions extracted')
const chart = createChart(container, {
width: 800,
height: 400,
layout: {
background: { color: '#1a1a1a' },
textColor: '#ffffff',
},
})
console.log('Chart created')
const series = chart.addCandlestickSeries({
upColor: '#26a69a',
downColor: '#ef5350',
})
console.log('Series added')
series.setData([
{ time: '2025-07-14', open: 100, high: 105, low: 95, close: 102 },
{ time: '2025-07-15', open: 102, high: 107, low: 98, close: 104 },
{ time: '2025-07-16', open: 104, high: 109, low: 101, close: 106 },
])
console.log('Data set - should be visible!')
} catch (error) {
console.error('Direct chart error:', error)
const statusDiv = document.getElementById('status')
if (statusDiv) {
statusDiv.textContent = `Error: ${error}`
statusDiv.className = 'text-red-400 text-lg mb-4'
}
}
}
// Add a small delay to ensure DOM is ready
setTimeout(initChart, 100)
}, [])
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-3xl mb-4">Direct DOM Chart</h1>
<div id="status" className="text-yellow-400 text-lg mb-4">Loading...</div>
<div
id="chart-container"
className="border-2 border-green-500 bg-gray-800"
style={{
width: '800px',
height: '400px',
}}
/>
</div>
)
}

View File

@@ -0,0 +1,77 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
export default function MinimalChartTest() {
const chartContainerRef = useRef<HTMLDivElement>(null)
const [status, setStatus] = useState('Starting...')
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!chartContainerRef.current) {
setStatus('No container ref')
return
}
const initChart = async () => {
try {
setStatus('Loading lightweight-charts...')
console.log('Starting chart init...')
const LightweightCharts = await import('lightweight-charts')
console.log('Lightweight charts loaded:', LightweightCharts)
setStatus('Charts library loaded')
const { createChart } = LightweightCharts
console.log('createChart:', typeof createChart)
setStatus('Creating chart...')
const chart = createChart(chartContainerRef.current!, {
width: 800,
height: 400,
})
console.log('Chart created:', chart)
setStatus('Chart created')
setStatus('Adding series...')
const series = chart.addCandlestickSeries({})
console.log('Series created:', series)
setStatus('Series added')
setStatus('Adding data...')
const data = [
{ time: '2025-01-01', open: 100, high: 110, low: 90, close: 105 },
{ time: '2025-01-02', open: 105, high: 115, low: 95, close: 110 },
{ time: '2025-01-03', open: 110, high: 120, low: 100, close: 115 },
]
series.setData(data)
console.log('Data set')
setStatus('Chart ready!')
} catch (err) {
console.error('Chart init error:', err)
const errorMsg = err instanceof Error ? err.message : String(err)
setError(errorMsg)
setStatus(`Error: ${errorMsg}`)
}
}
initChart()
}, [])
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-2xl mb-4">Minimal Chart Test</h1>
<div className="text-gray-300 mb-4">Status: {status}</div>
{error && (
<div className="text-red-400 mb-4 p-4 bg-red-900/20 rounded">
Error: {error}
</div>
)}
<div
ref={chartContainerRef}
className="bg-gray-800 border border-gray-600"
style={{ width: '800px', height: '400px' }}
/>
</div>
)
}

115
app/simple-chart/page.tsx Normal file
View File

@@ -0,0 +1,115 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
export default function SimpleChart() {
const chartContainerRef = useRef<HTMLDivElement>(null)
const [status, setStatus] = useState('Initializing...')
useEffect(() => {
if (!chartContainerRef.current) return
const initChart = async () => {
try {
setStatus('Loading lightweight-charts...')
console.log('Importing lightweight-charts...')
const LightweightCharts = await import('lightweight-charts')
console.log('Lightweight charts imported successfully')
setStatus('Creating chart...')
const { createChart, ColorType } = LightweightCharts
const chart = createChart(chartContainerRef.current!, {
layout: {
background: { type: ColorType.Solid, color: '#1a1a1a' },
textColor: '#ffffff',
},
width: chartContainerRef.current!.clientWidth || 800,
height: 400,
grid: {
vertLines: { color: 'rgba(42, 46, 57, 0.5)' },
horzLines: { color: 'rgba(42, 46, 57, 0.5)' },
},
})
setStatus('Adding candlestick series...')
console.log('Chart created, adding candlestick series...')
const candlestickSeries = chart.addCandlestickSeries({
upColor: '#26a69a',
downColor: '#ef5350',
borderDownColor: '#ef5350',
borderUpColor: '#26a69a',
wickDownColor: '#ef5350',
wickUpColor: '#26a69a',
})
// Generate sample data
const data = []
const baseTime = new Date(Date.now() - 100 * 60 * 1000) // 100 minutes ago
let price = 166.5
for (let i = 0; i < 100; i++) {
const currentTime = new Date(baseTime.getTime() + i * 60 * 1000) // 1 minute intervals
const timeString = currentTime.toISOString().split('T')[0] // YYYY-MM-DD format
const change = (Math.random() - 0.5) * 2 // Random price change
const open = price
const close = price + change
const high = Math.max(open, close) + Math.random() * 1
const low = Math.min(open, close) - Math.random() * 1
data.push({
time: timeString,
open: Number(open.toFixed(2)),
high: Number(high.toFixed(2)),
low: Number(low.toFixed(2)),
close: Number(close.toFixed(2)),
})
price = close
}
console.log('Setting chart data...', data.length, 'points')
candlestickSeries.setData(data)
setStatus('Chart loaded successfully!')
console.log('Chart created successfully!')
// Handle resize
const handleResize = () => {
if (chartContainerRef.current) {
chart.applyOptions({
width: chartContainerRef.current.clientWidth,
})
}
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
chart.remove()
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error('Error creating chart:', error)
setStatus(`Error: ${errorMessage}`)
}
}
initChart()
}, [])
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-2xl mb-4">Lightweight Charts Test</h1>
<div className="text-gray-400 mb-4">Status: {status}</div>
<div
ref={chartContainerRef}
className="bg-gray-800 rounded w-full h-96"
style={{ minHeight: '400px' }}
/>
</div>
)
}

95
app/simple-test/page.tsx Normal file
View File

@@ -0,0 +1,95 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
export default function SimpleTest() {
const chartContainerRef = useRef<HTMLDivElement>(null)
const [status, setStatus] = useState('Initializing...')
useEffect(() => {
const initChart = async () => {
try {
setStatus('Importing library...')
// Test if we can import the library
const chartModule = await import('lightweight-charts')
setStatus('Library imported')
// Test if we can extract functions
const { createChart } = chartModule
setStatus('Functions extracted')
if (!chartContainerRef.current) {
setStatus('No container element')
return
}
setStatus('Creating chart...')
// Create chart with explicit dimensions
const chart = createChart(chartContainerRef.current, {
width: 800,
height: 400,
layout: {
background: { color: '#1a1a1a' },
textColor: '#ffffff',
},
})
setStatus('Chart created')
// Add series
const series = chart.addCandlestickSeries({
upColor: '#00ff00',
downColor: '#ff0000',
})
setStatus('Series added')
// Add simple data
series.setData([
{ time: '2025-07-14', open: 100, high: 105, low: 95, close: 102 },
{ time: '2025-07-15', open: 102, high: 107, low: 98, close: 104 },
{ time: '2025-07-16', open: 104, high: 109, low: 101, close: 106 },
])
setStatus('Data set - Chart should be visible!')
} catch (error) {
console.error('Error:', error)
setStatus(`Error: ${error}`)
}
}
initChart()
}, [])
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-3xl mb-4">Simple Chart Test</h1>
<div className="text-green-400 text-lg mb-4">Status: {status}</div>
<div className="bg-red-500 p-2 mb-4 text-white">
This red border should help us see if the container is properly sized
</div>
<div
ref={chartContainerRef}
className="border-4 border-yellow-500 bg-gray-800"
style={{
width: '800px',
height: '400px',
display: 'block',
position: 'relative'
}}
>
<div className="text-white p-4">
Container content - this should be replaced by the chart
</div>
</div>
<div className="text-gray-400 mt-4">
Container ref: {chartContainerRef.current ? 'Available' : 'Not available'}
</div>
</div>
)
}

12
app/test-chart/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
'use client'
import React from 'react'
import WorkingTradingChart from '../../components/WorkingTradingChart'
export default function TestChartPage() {
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-3xl mb-6">Working Chart Test</h1>
<WorkingTradingChart symbol="SOL/USDC" positions={[]} />
</div>
)
}

16
app/test-trading/page.tsx Normal file
View File

@@ -0,0 +1,16 @@
'use client'
import React from 'react'
export default function SimpleTradingPage() {
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-3xl mb-6">Trading Dashboard</h1>
<div className="bg-gray-800 p-6 rounded-lg">
<h2 className="text-white text-xl mb-4">Chart Area</h2>
<div className="bg-gray-700 h-96 rounded flex items-center justify-center">
<span className="text-gray-300">Chart will load here</span>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,77 @@
'use client'
import React, { useEffect, useRef } from 'react'
export default function WorkingChart() {
const chartContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!chartContainerRef.current) return
const initChart = async () => {
try {
const { createChart } = await import('lightweight-charts')
const chart = createChart(chartContainerRef.current!, {
width: 800,
height: 400,
layout: {
textColor: '#ffffff',
background: { color: '#1a1a1a' },
},
})
const candlestickSeries = chart.addCandlestickSeries({
upColor: '#26a69a',
downColor: '#ef5350',
})
// Simple working data - last 30 days
const data = []
const today = new Date()
let price = 166.5
for (let i = 29; i >= 0; i--) {
const date = new Date(today)
date.setDate(date.getDate() - i)
const timeString = date.toISOString().split('T')[0]
const change = (Math.random() - 0.5) * 4
const open = price
const close = price + change
const high = Math.max(open, close) + Math.random() * 2
const low = Math.min(open, close) - Math.random() * 2
data.push({
time: timeString,
open: Number(open.toFixed(2)),
high: Number(high.toFixed(2)),
low: Number(low.toFixed(2)),
close: Number(close.toFixed(2)),
})
price = close
}
candlestickSeries.setData(data)
return () => {
chart.remove()
}
} catch (error) {
console.error('Chart error:', error)
}
}
initChart()
}, [])
return (
<div className="min-h-screen bg-gray-900 p-8">
<h1 className="text-white text-2xl mb-4">Working Chart Test</h1>
<div
ref={chartContainerRef}
className="bg-gray-800 border border-gray-600"
/>
</div>
)
}

View File

@@ -1,5 +1,5 @@
"use client"
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import TradeModal from './TradeModal'
import ScreenshotGallery from './ScreenshotGallery'
@@ -8,7 +8,9 @@ const timeframes = [
{ label: '1m', value: '1' },
{ label: '5m', value: '5' },
{ label: '15m', value: '15' },
{ label: '30m', value: '30' },
{ label: '1h', value: '60' },
{ label: '2h', value: '120' },
{ label: '4h', value: '240' },
{ label: '1d', value: 'D' },
{ label: '1w', value: 'W' },
@@ -60,6 +62,7 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
const [result, setResult] = useState<any>(null)
const [error, setError] = useState<string | null>(null)
const [progress, setProgress] = useState<AnalysisProgress | null>(null)
const [eventSource, setEventSource] = useState<EventSource | null>(null)
const [modalOpen, setModalOpen] = useState(false)
const [modalData, setModalData] = useState<any>(null)
const [enlargedScreenshot, setEnlargedScreenshot] = useState<string | null>(null)
@@ -77,6 +80,47 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
return String(value)
}
// Real-time progress tracking
const startProgressTracking = (sessionId: string) => {
// Close existing connection
if (eventSource) {
eventSource.close()
}
const es = new EventSource(`/api/progress/${sessionId}/stream`)
es.onmessage = (event) => {
try {
const progressData = JSON.parse(event.data)
if (progressData.type === 'complete') {
es.close()
setEventSource(null)
} else {
setProgress(progressData)
}
} catch (error) {
console.error('Error parsing progress data:', error)
}
}
es.onerror = (error) => {
console.error('EventSource error:', error)
es.close()
setEventSource(null)
}
setEventSource(es)
}
// Cleanup event source on unmount
React.useEffect(() => {
return () => {
if (eventSource) {
eventSource.close()
}
}
}, [eventSource])
const toggleLayout = (layout: string) => {
setSelectedLayouts(prev =>
prev.includes(layout)
@@ -93,104 +137,11 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
)
}
// Helper function to create initial progress steps
const createProgressSteps = (timeframes: string[], layouts: string[]): ProgressStep[] => {
const steps: ProgressStep[] = []
if (timeframes.length > 1) {
steps.push({
id: 'init',
title: 'Initializing Multi-Timeframe Analysis',
description: `Preparing to analyze ${timeframes.length} timeframes`,
status: 'pending'
})
} else {
steps.push({
id: 'init',
title: 'Initializing Analysis',
description: `Setting up screenshot service for ${layouts.join(', ')} layout(s)`,
status: 'pending'
})
}
// Helper function to create initial progress steps (no longer used - using real-time progress)
// const createProgressSteps = ...removed for real-time implementation
steps.push({
id: 'browser',
title: 'Starting Browser Sessions',
description: `Launching ${layouts.length} browser session(s)`,
status: 'pending'
})
steps.push({
id: 'auth',
title: 'TradingView Authentication',
description: 'Logging into TradingView accounts',
status: 'pending'
})
steps.push({
id: 'navigation',
title: 'Chart Navigation',
description: 'Navigating to chart layouts and timeframes',
status: 'pending'
})
steps.push({
id: 'loading',
title: 'Chart Data Loading',
description: 'Waiting for chart data and indicators to load',
status: 'pending'
})
steps.push({
id: 'capture',
title: 'Screenshot Capture',
description: 'Capturing high-quality chart screenshots',
status: 'pending'
})
steps.push({
id: 'analysis',
title: 'AI Analysis',
description: 'Analyzing screenshots with AI for trading insights',
status: 'pending'
})
return steps
}
// Helper function to update progress
const updateProgress = (stepId: string, status: ProgressStep['status'], details?: string) => {
setProgress(prev => {
if (!prev) return null
const updatedSteps = prev.steps.map(step => {
if (step.id === stepId) {
const updatedStep = {
...step,
status,
details: details || step.details
}
if (status === 'active' && !step.startTime) {
updatedStep.startTime = Date.now()
} else if ((status === 'completed' || status === 'error') && !step.endTime) {
updatedStep.endTime = Date.now()
}
return updatedStep
}
return step
})
const currentStepIndex = updatedSteps.findIndex(step => step.status === 'active')
return {
...prev,
steps: updatedSteps,
currentStep: currentStepIndex >= 0 ? currentStepIndex + 1 : prev.currentStep
}
})
}
// Helper function to update progress (no longer used - using real-time progress)
// const updateProgress = ...removed for real-time implementation
const performAnalysis = async (analysisSymbol = symbol, analysisTimeframes = selectedTimeframes) => {
if (loading || selectedLayouts.length === 0 || analysisTimeframes.length === 0) return
@@ -198,13 +149,51 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
setLoading(true)
setError(null)
setResult(null)
// Initialize progress tracking
const steps = createProgressSteps(analysisTimeframes, selectedLayouts)
// Set initial progress state to show animation immediately
setProgress({
currentStep: 0,
totalSteps: steps.length,
steps,
sessionId: 'initializing',
currentStep: 1,
totalSteps: 6,
steps: [
{
id: 'init',
title: 'Initializing Analysis',
description: 'Starting AI-powered trading analysis...',
status: 'active',
startTime: Date.now()
},
{
id: 'auth',
title: 'TradingView Authentication',
description: 'Logging into TradingView accounts',
status: 'pending'
},
{
id: 'navigation',
title: 'Chart Navigation',
description: 'Navigating to chart layouts',
status: 'pending'
},
{
id: 'loading',
title: 'Chart Data Loading',
description: 'Waiting for chart data and indicators',
status: 'pending'
},
{
id: 'capture',
title: 'Screenshot Capture',
description: 'Capturing high-quality screenshots',
status: 'pending'
},
{
id: 'analysis',
title: 'AI Analysis',
description: 'Analyzing screenshots with AI',
status: 'pending'
}
],
timeframeProgress: analysisTimeframes.length > 1 ? {
current: 0,
total: analysisTimeframes.length
@@ -212,14 +201,8 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
})
try {
updateProgress('init', 'active')
if (analysisTimeframes.length === 1) {
// Single timeframe analysis
await new Promise(resolve => setTimeout(resolve, 500)) // Brief pause for UI
updateProgress('init', 'completed')
updateProgress('browser', 'active', 'Starting browser session...')
// Single timeframe analysis with real-time progress
const response = await fetch('/api/enhanced-screenshot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -231,35 +214,16 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
})
})
// Since we can't track internal API progress in real-time, we'll simulate logical progression
await new Promise(resolve => setTimeout(resolve, 1000))
updateProgress('browser', 'completed')
updateProgress('auth', 'active', 'Authenticating with TradingView...')
await new Promise(resolve => setTimeout(resolve, 2000))
updateProgress('auth', 'completed')
updateProgress('navigation', 'active', `Navigating to ${analysisSymbol} chart...`)
await new Promise(resolve => setTimeout(resolve, 2000))
updateProgress('navigation', 'completed')
updateProgress('loading', 'active', 'Loading chart data and indicators...')
await new Promise(resolve => setTimeout(resolve, 3000))
updateProgress('loading', 'completed')
updateProgress('capture', 'active', 'Capturing screenshots...')
const data = await response.json()
if (!response.ok) {
updateProgress('capture', 'error', data.error || 'Screenshot capture failed')
throw new Error(data.error || 'Analysis failed')
}
updateProgress('capture', 'completed', `Captured ${data.screenshots?.length || 0} screenshot(s)`)
updateProgress('analysis', 'active', 'Running AI analysis...')
await new Promise(resolve => setTimeout(resolve, 1000))
updateProgress('analysis', 'completed', 'Analysis complete!')
// Start real-time progress tracking if sessionId is provided
if (data.sessionId) {
startProgressTracking(data.sessionId)
}
setResult(data)
@@ -269,31 +233,14 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
}
} else {
// Multiple timeframe analysis
await new Promise(resolve => setTimeout(resolve, 500))
updateProgress('init', 'completed', `Starting analysis for ${analysisTimeframes.length} timeframes`)
const results = []
for (let i = 0; i < analysisTimeframes.length; i++) {
const tf = analysisTimeframes[i]
const timeframeLabel = timeframes.find(t => t.value === tf)?.label || tf
// Update timeframe progress
setProgress(prev => prev ? {
...prev,
timeframeProgress: {
...prev.timeframeProgress!,
current: i + 1,
currentTimeframe: timeframeLabel
}
} : null)
console.log(`🧪 Analyzing timeframe: ${timeframeLabel}`)
if (i === 0) {
updateProgress('browser', 'active', `Processing ${timeframeLabel} - Starting browser...`)
}
const response = await fetch('/api/enhanced-screenshot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -305,21 +252,6 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
})
})
if (i === 0) {
updateProgress('browser', 'completed')
updateProgress('auth', 'active', `Processing ${timeframeLabel} - Authenticating...`)
await new Promise(resolve => setTimeout(resolve, 1000))
updateProgress('auth', 'completed')
}
updateProgress('navigation', 'active', `Processing ${timeframeLabel} - Navigating to chart...`)
await new Promise(resolve => setTimeout(resolve, 1000))
updateProgress('loading', 'active', `Processing ${timeframeLabel} - Loading chart data...`)
await new Promise(resolve => setTimeout(resolve, 1500))
updateProgress('capture', 'active', `Processing ${timeframeLabel} - Capturing screenshots...`)
const result = await response.json()
results.push({
timeframe: tf,
@@ -327,18 +259,26 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
success: response.ok,
result
})
// Start progress tracking for the first timeframe session
if (i === 0 && result.sessionId) {
startProgressTracking(result.sessionId)
}
updateProgress('analysis', 'active', `Processing ${timeframeLabel} - Running AI analysis...`)
// Update timeframe progress manually for multi-timeframe
setProgress(prev => prev ? {
...prev,
timeframeProgress: {
current: i + 1,
total: analysisTimeframes.length,
currentTimeframe: timeframeLabel
}
} : null)
// Small delay between requests
await new Promise(resolve => setTimeout(resolve, 2000))
await new Promise(resolve => setTimeout(resolve, 1000))
}
updateProgress('navigation', 'completed')
updateProgress('loading', 'completed')
updateProgress('capture', 'completed', `Captured screenshots for all ${analysisTimeframes.length} timeframes`)
updateProgress('analysis', 'completed', `Completed analysis for all timeframes!`)
const multiResult = {
type: 'multi_timeframe',
symbol: analysisSymbol,
@@ -488,17 +428,29 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
// Trade execution API call
const executeTrade = async (tradeData: any) => {
try {
// Use real DEX trading for manual trades
const response = await fetch('/api/trading/execute-dex', {
// Determine if this is a leveraged position or spot trade
const leverage = parseFloat(tradeData.leverage) || 1
const isLeveraged = leverage > 1
// Route to appropriate API based on leverage
const apiEndpoint = isLeveraged ? '/api/trading/execute-drift' : '/api/trading/execute-dex'
const tradingMode = isLeveraged ? 'PERP' : 'SPOT'
console.log(`🎯 Executing ${tradingMode} trade with ${leverage}x leverage via ${apiEndpoint}`)
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
symbol: tradeData.symbol || symbol,
side: 'BUY', // Could be derived from analysis
amount: parseFloat(tradeData.positionSize) || parseFloat(tradeData.size),
amountUSD: parseFloat(tradeData.amountUSD || tradeData.positionSize || tradeData.size),
leverage: leverage,
stopLoss: parseFloat(tradeData.sl),
takeProfit: parseFloat(tradeData.tp1), // Use TP1 as primary target
useRealDEX: true, // Enable real trading for manual execution
tradingMode: tradingMode,
tradingPair: `${tradeData.symbol || symbol}/USDC`,
quickSwap: false
})
@@ -507,17 +459,30 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
const result = await response.json()
if (response.ok && result.success) {
// Show detailed success message for DEX execution
let message = `✅ Real DEX Trade executed successfully!\n\n`
// Show detailed success message based on trading type
const leverage = parseFloat(tradeData.leverage) || 1
const isLeveraged = leverage > 1
const tradeType = isLeveraged ? 'Leveraged Perpetual Position' : 'Spot Trade'
const platform = isLeveraged ? 'Drift Protocol' : 'Jupiter DEX'
let message = `${tradeType} executed successfully!\n\n`
message += `📊 Transaction ID: ${result.trade?.txId || result.txId}\n`
message += `💰 Symbol: ${tradeData.symbol || symbol}\n`
message += `📈 Size: ${tradeData.positionSize || tradeData.size}\n`
message += `🏪 DEX: ${result.trade?.dex || 'Jupiter'}\n`
message += `📈 Size: ${tradeData.positionSize || tradeData.size} USDC\n`
if (isLeveraged) {
message += `⚡ Leverage: ${leverage}x (via increased position size)\n`
message += `<EFBFBD> Actual Trade Size: $${(parseFloat(tradeData.positionSize || tradeData.size) * leverage).toFixed(2)}\n`
}
message += `<EFBFBD>🏪 Platform: ${platform}\n`
if (tradeData.sl) message += `🛑 Stop Loss: $${tradeData.sl}\n`
if (tradeData.tp1) message += `🎯 Take Profit: $${tradeData.tp1}\n`
if (tradeData.sl) message += `🛑 Stop Loss: $${tradeData.sl} (Jupiter Trigger Order)\n`
if (tradeData.tp1) message += `🎯 Take Profit: $${tradeData.tp1} (Jupiter Trigger Order)\n`
if (result.trade?.monitoring) {
if (result.triggerOrders?.status === 'CREATED') {
message += `\n🔄 Trigger Orders: ACTIVE\n`
if (result.triggerOrders.stopLossOrderId) message += `🛑 SL Order: ${result.triggerOrders.stopLossOrderId.substring(0, 8)}...\n`
if (result.triggerOrders.takeProfitOrderId) message += `🎯 TP Order: ${result.triggerOrders.takeProfitOrderId.substring(0, 8)}...\n`
} else if (result.trade?.monitoring || result.position) {
message += `\n🔄 Position monitoring: ACTIVE`
}
@@ -532,6 +497,8 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
alert(`❌ Trade Failed: Insufficient Balance\n\nPlease ensure you have enough tokens in your wallet.\n\nError: ${errorMsg}`)
} else if (errorMsg.includes('Real Jupiter Perpetuals trading not yet implemented')) {
alert(`❌ Real Trading Not Available\n\nReal Jupiter Perpetuals trading is still in development. This trade will be simulated instead.\n\nTo use real spot trading, reduce the leverage to 1x.`)
} else if (errorMsg.includes('Trigger API error') || errorMsg.includes('trigger orders failed')) {
alert(`⚠️ Trade Executed, But Trigger Orders Failed\n\nYour main trade was successful, but stop loss/take profit orders could not be created.\n\nError: ${errorMsg}\n\nPlease monitor your position manually.`)
} else {
alert(`❌ Trade Failed\n\nError: ${errorMsg}`)
}
@@ -579,16 +546,16 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
<label className="block text-xs font-medium text-gray-400 mb-2">Quick Timeframe Presets</label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
<button
onClick={() => setSelectedTimeframes(['5', '15', '60'])}
onClick={() => setSelectedTimeframes(['5', '15', '30'])}
className="py-2 px-3 rounded-lg text-xs font-medium bg-purple-600/20 text-purple-300 hover:bg-purple-600/30 transition-all"
>
🕒 Scalping (5m, 15m, 1h)
🕒 Scalping (5m, 15m, 30m)
</button>
<button
onClick={() => setSelectedTimeframes(['60', '240', 'D'])}
onClick={() => setSelectedTimeframes(['60', '120', '240'])}
className="py-2 px-3 rounded-lg text-xs font-medium bg-blue-600/20 text-blue-300 hover:bg-blue-600/30 transition-all"
>
📊 Day Trading (1h, 4h, 1d)
📊 Day Trading (1h, 2h, 4h)
</button>
<button
onClick={() => setSelectedTimeframes(['240', 'D', 'W'])}
@@ -978,7 +945,23 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
</div>
<div className="grid gap-4">
{result.results.map((timeframeResult: any, index: number) => (
{result.results
.sort((a: any, b: any) => {
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
const timeframeOrder: {[key: string]: number} = {
'5': 1, '5m': 1,
'15': 2, '15m': 2,
'30': 3, '30m': 3,
'60': 4, '1h': 4,
'120': 5, '2h': 5,
'240': 6, '4h': 6,
'D': 7, '1D': 7
}
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
return orderA - orderB
})
.map((timeframeResult: any, index: number) => (
<div key={index} className={`p-4 rounded-lg border ${
timeframeResult.success
? 'bg-green-500/5 border-green-500/30'
@@ -1459,9 +1442,43 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
{/* Multi-timeframe Screenshot Gallery */}
{result && result.type === 'multi_timeframe' && result.results && (
<ScreenshotGallery
screenshots={result.results.filter((r: any) => r.success && r.result.screenshots).flatMap((r: any) => r.result.screenshots)}
screenshots={result.results
.filter((r: any) => r.success && r.result.screenshots)
.sort((a: any, b: any) => {
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
const timeframeOrder: {[key: string]: number} = {
'5': 1, '5m': 1,
'15': 2, '15m': 2,
'30': 3, '30m': 3,
'60': 4, '1h': 4,
'120': 5, '2h': 5,
'240': 6, '4h': 6,
'D': 7, '1D': 7
}
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
return orderA - orderB
})
.flatMap((r: any) => r.result.screenshots)}
symbol={symbol}
timeframes={result.results.filter((r: any) => r.success).map((r: any) => r.timeframeLabel)}
timeframes={result.results
.filter((r: any) => r.success)
.sort((a: any, b: any) => {
// Sort by timeframe order: 5m, 15m, 30m, 1h, 2h, 4h, 1D
const timeframeOrder: {[key: string]: number} = {
'5': 1, '5m': 1,
'15': 2, '15m': 2,
'30': 3, '30m': 3,
'60': 4, '1h': 4,
'120': 5, '2h': 5,
'240': 6, '4h': 6,
'D': 7, '1D': 7
}
const orderA = timeframeOrder[a.timeframe] || timeframeOrder[a.timeframeLabel] || 999
const orderB = timeframeOrder[b.timeframe] || timeframeOrder[b.timeframeLabel] || 999
return orderA - orderB
})
.map((r: any) => r.timeframeLabel)}
enlargedImage={enlargedScreenshot}
onImageClick={handleScreenshotClick}
onClose={() => setEnlargedScreenshot(null)}

View File

@@ -34,6 +34,56 @@ export default function ScreenshotGallery({
if (screenshots.length === 0) return null
// Utility function to convert timeframe to sortable number
const timeframeToMinutes = (timeframe: string): number => {
const tf = timeframe.toLowerCase()
if (tf.includes('5m') || tf === '5') return 5
if (tf.includes('15m') || tf === '15') return 15
if (tf.includes('30m') || tf === '30') return 30
if (tf.includes('1h') || tf === '60') return 60
if (tf.includes('2h') || tf === '120') return 120
if (tf.includes('4h') || tf === '240') return 240
if (tf.includes('1d') || tf === 'D') return 1440
// Default fallback
return parseInt(tf) || 999
}
// Extract timeframe from filename
const extractTimeframeFromFilename = (filename: string) => {
const match = filename.match(/_(\d+|D)_/)
if (!match) return 'Unknown'
const tf = match[1]
if (tf === 'D') return '1D'
if (tf === '5') return '5m'
if (tf === '15') return '15m'
if (tf === '30') return '30m'
if (tf === '60') return '1h'
if (tf === '120') return '2h'
if (tf === '240') return '4h'
return `${tf}m`
}
// Create sorted screenshot data with timeframes
const screenshotData = screenshots.map((screenshot, index) => {
const screenshotUrl = typeof screenshot === 'string'
? screenshot
: (screenshot as any)?.url || String(screenshot)
const filename = screenshotUrl.split('/').pop() || ''
const timeframe = timeframes[index] || extractTimeframeFromFilename(filename)
return {
screenshot,
screenshotUrl,
filename,
timeframe,
index,
sortOrder: timeframeToMinutes(timeframe)
}
})
// Sort by timeframe (smallest to largest)
const sortedData = screenshotData.sort((a, b) => a.sortOrder - b.sortOrder)
// Helper function to format screenshot URL
const formatScreenshotUrl = (screenshot: string | any) => {
// Handle both string URLs and screenshot objects
@@ -56,36 +106,17 @@ export default function ScreenshotGallery({
Chart Screenshots
</h4>
<div className="text-xs text-gray-400">
{screenshots.length} captured Click to enlarge
{sortedData.length} captured Click to enlarge
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{screenshots.map((screenshot, index) => {
// Handle both string URLs and screenshot objects
const screenshotUrl = typeof screenshot === 'string'
? screenshot
: (screenshot as any)?.url || String(screenshot)
const filename = screenshotUrl.split('/').pop() || ''
// Extract timeframe from filename (e.g., SOLUSD_5_ai_timestamp.png -> "5m")
const extractTimeframeFromFilename = (filename: string) => {
const match = filename.match(/_(\d+|D)_/)
if (!match) return 'Unknown'
const tf = match[1]
if (tf === 'D') return '1D'
if (tf === '5') return '5m'
if (tf === '15') return '15m'
if (tf === '60') return '1h'
if (tf === '240') return '4h'
return `${tf}m`
}
const timeframe = timeframes[index] || extractTimeframeFromFilename(filename)
const imageUrl = formatScreenshotUrl(screenshot)
{sortedData.map((item, displayIndex) => {
const imageUrl = formatScreenshotUrl(item.screenshot)
return (
<div
key={index}
key={displayIndex}
className="group relative bg-gray-800/30 rounded-lg overflow-hidden border border-gray-700 hover:border-purple-500/50 transition-all cursor-pointer transform hover:scale-[1.02]"
onClick={() => onImageClick(imageUrl)}
>
@@ -93,7 +124,7 @@ export default function ScreenshotGallery({
<div className="aspect-video bg-gray-800 flex items-center justify-center relative">
<img
src={imageUrl}
alt={`${symbol} - ${timeframe} chart`}
alt={`${symbol} - ${item.timeframe} chart`}
className="w-full h-full object-cover"
onError={(e: any) => {
const target = e.target as HTMLImageElement
@@ -106,7 +137,7 @@ export default function ScreenshotGallery({
<div className="text-center">
<div className="text-3xl mb-2">📊</div>
<div className="text-sm">Chart Preview</div>
<div className="text-xs text-gray-500">{filename}</div>
<div className="text-xs text-gray-500">{item.filename}</div>
</div>
</div>
@@ -125,7 +156,7 @@ export default function ScreenshotGallery({
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium text-white">{symbol}</div>
<div className="text-xs text-purple-300">{timeframe} Timeframe</div>
<div className="text-xs text-purple-300">{item.timeframe} Timeframe</div>
</div>
<div className="text-xs text-gray-400">
Click to view

View File

@@ -0,0 +1,178 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
interface SimpleCandlestickData {
time: string
open: number
high: number
low: number
close: number
}
interface SimpleTradingChartProps {
symbol?: string
positions?: any[]
}
export default function SimpleTradingChart({ symbol = 'SOL/USDC', positions = [] }: SimpleTradingChartProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [data, setData] = useState<SimpleCandlestickData[]>([])
const [error, setError] = useState<string | null>(null)
// Generate sample data
useEffect(() => {
const generateData = () => {
const data: SimpleCandlestickData[] = []
const basePrice = 166.5
let currentPrice = basePrice
const today = new Date()
for (let i = 29; i >= 0; i--) {
const date = new Date(today)
date.setDate(date.getDate() - i)
const timeString = date.toISOString().split('T')[0]
const change = (Math.random() - 0.5) * 4
const open = currentPrice
const close = currentPrice + change
const high = Math.max(open, close) + Math.random() * 2
const low = Math.min(open, close) - Math.random() * 2
data.push({
time: timeString,
open: Number(open.toFixed(2)),
high: Number(high.toFixed(2)),
low: Number(low.toFixed(2)),
close: Number(close.toFixed(2)),
})
currentPrice = close
}
return data
}
setData(generateData())
}, [])
// Draw the chart on canvas
useEffect(() => {
if (!canvasRef.current || data.length === 0) return
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
if (!ctx) return
// Set canvas size
canvas.width = 800
canvas.height = 400
// Clear canvas
ctx.fillStyle = '#1a1a1a'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Calculate chart dimensions
const padding = 50
const chartWidth = canvas.width - 2 * padding
const chartHeight = canvas.height - 2 * padding
// Find price range
const prices = data.flatMap(d => [d.open, d.high, d.low, d.close])
const minPrice = Math.min(...prices)
const maxPrice = Math.max(...prices)
const priceRange = maxPrice - minPrice
// Helper functions
const getX = (index: number) => padding + (index / (data.length - 1)) * chartWidth
const getY = (price: number) => padding + ((maxPrice - price) / priceRange) * chartHeight
// Draw grid
ctx.strokeStyle = 'rgba(42, 46, 57, 0.5)'
ctx.lineWidth = 1
// Horizontal grid lines
for (let i = 0; i <= 5; i++) {
const y = padding + (i / 5) * chartHeight
ctx.beginPath()
ctx.moveTo(padding, y)
ctx.lineTo(padding + chartWidth, y)
ctx.stroke()
}
// Vertical grid lines
for (let i = 0; i <= 10; i++) {
const x = padding + (i / 10) * chartWidth
ctx.beginPath()
ctx.moveTo(x, padding)
ctx.lineTo(x, padding + chartHeight)
ctx.stroke()
}
// Draw candlesticks
const candleWidth = Math.max(2, chartWidth / data.length * 0.8)
data.forEach((candle, index) => {
const x = getX(index)
const openY = getY(candle.open)
const closeY = getY(candle.close)
const highY = getY(candle.high)
const lowY = getY(candle.low)
const isGreen = candle.close > candle.open
const color = isGreen ? '#26a69a' : '#ef5350'
// Draw wick
ctx.strokeStyle = color
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(x, highY)
ctx.lineTo(x, lowY)
ctx.stroke()
// Draw body
ctx.fillStyle = color
const bodyTop = Math.min(openY, closeY)
const bodyHeight = Math.abs(closeY - openY)
ctx.fillRect(x - candleWidth / 2, bodyTop, candleWidth, Math.max(bodyHeight, 1))
})
// Draw price labels
ctx.fillStyle = '#ffffff'
ctx.font = '12px Arial'
ctx.textAlign = 'right'
for (let i = 0; i <= 5; i++) {
const price = maxPrice - (i / 5) * priceRange
const y = padding + (i / 5) * chartHeight
ctx.fillText(price.toFixed(2), padding - 10, y + 4)
}
// Draw title
ctx.fillStyle = '#ffffff'
ctx.font = 'bold 16px Arial'
ctx.textAlign = 'left'
ctx.fillText(symbol, padding, 30)
}, [data, symbol])
if (error) {
return (
<div className="bg-gray-900 rounded-lg p-6 h-[600px] flex items-center justify-center">
<div className="text-red-400">Chart Error: {error}</div>
</div>
)
}
return (
<div className="bg-gray-900 rounded-lg p-6">
<canvas
ref={canvasRef}
className="border border-gray-700 rounded"
style={{ width: '100%', maxWidth: '800px', height: '400px' }}
/>
<div className="mt-4 text-sm text-gray-400">
Simple canvas-based candlestick chart {data.length} data points
</div>
</div>
)
}

261
components/TradingChart.tsx Normal file
View File

@@ -0,0 +1,261 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
interface Position {
id: string
symbol: string
side: 'LONG' | 'SHORT'
size: number
entryPrice: number
stopLoss?: number
takeProfit?: number
pnl: number
pnlPercentage: number
}
interface TradingChartProps {
symbol?: string
positions?: Position[]
onPriceUpdate?: (price: number) => void
}
export default function TradingChart({ symbol = 'SOL/USDC', positions = [], onPriceUpdate }: TradingChartProps) {
const chartContainerRef = useRef<HTMLDivElement>(null)
const chart = useRef<any>(null)
const candlestickSeries = useRef<any>(null)
const positionLines = useRef<any[]>([])
const [isLoading, setIsLoading] = useState(true)
// Initialize chart with dynamic import
useEffect(() => {
if (!chartContainerRef.current) return
const initChart = async () => {
try {
// Dynamic import to avoid SSR issues
const LightweightCharts = await import('lightweight-charts')
const { createChart, ColorType, CrosshairMode, LineStyle } = LightweightCharts
chart.current = createChart(chartContainerRef.current!, {
layout: {
background: { type: ColorType.Solid, color: '#1a1a1a' },
textColor: '#ffffff',
},
width: chartContainerRef.current!.clientWidth,
height: 600,
grid: {
vertLines: { color: 'rgba(42, 46, 57, 0.5)' },
horzLines: { color: 'rgba(42, 46, 57, 0.5)' },
},
crosshair: {
mode: CrosshairMode.Normal,
},
rightPriceScale: {
borderColor: 'rgba(197, 203, 206, 0.8)',
},
timeScale: {
borderColor: 'rgba(197, 203, 206, 0.8)',
},
})
// Create candlestick series
candlestickSeries.current = chart.current.addCandlestickSeries({
upColor: '#26a69a',
downColor: '#ef5350',
borderDownColor: '#ef5350',
borderUpColor: '#26a69a',
wickDownColor: '#ef5350',
wickUpColor: '#26a69a',
})
// Generate sample data
console.log('Generating sample data...')
const data = generateSampleData()
console.log('Sample data generated:', data.length, 'points')
console.log('First few data points:', data.slice(0, 3))
console.log('Setting chart data...')
candlestickSeries.current.setData(data)
console.log('Chart data set successfully')
// Call onPriceUpdate with the latest price if provided
if (onPriceUpdate && data.length > 0) {
const latestPrice = data[data.length - 1].close
onPriceUpdate(latestPrice)
}
// Add position overlays
console.log('Adding position overlays...')
addPositionOverlays(LineStyle)
console.log('Position overlays added')
console.log('Chart initialization complete')
setIsLoading(false)
// Handle resize
const handleResize = () => {
if (chart.current && chartContainerRef.current) {
chart.current.applyOptions({
width: chartContainerRef.current.clientWidth,
})
}
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
if (chart.current) {
chart.current.remove()
}
}
} catch (error) {
console.error('Failed to initialize chart:', error)
console.error('Error details:', {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
})
setIsLoading(false)
}
}
const addPositionOverlays = (LineStyle: any) => {
if (!chart.current) return
// Clear existing lines
positionLines.current.forEach(line => {
if (line && chart.current) {
chart.current.removePriceLine(line)
}
})
positionLines.current = []
// Add new position lines
positions.forEach(position => {
// Entry price line
const entryLine = chart.current.addPriceLine({
price: position.entryPrice,
color: '#2196F3',
lineWidth: 2,
lineStyle: LineStyle.Solid,
axisLabelVisible: true,
title: `Entry: $${position.entryPrice.toFixed(2)}`,
})
positionLines.current.push(entryLine)
// Stop loss line
if (position.stopLoss) {
const slLine = chart.current.addPriceLine({
price: position.stopLoss,
color: '#f44336',
lineWidth: 2,
lineStyle: LineStyle.Dashed,
axisLabelVisible: true,
title: `SL: $${position.stopLoss.toFixed(2)}`,
})
positionLines.current.push(slLine)
}
// Take profit line
if (position.takeProfit) {
const tpLine = chart.current.addPriceLine({
price: position.takeProfit,
color: '#4caf50',
lineWidth: 2,
lineStyle: LineStyle.Dashed,
axisLabelVisible: true,
title: `TP: $${position.takeProfit.toFixed(2)}`,
})
positionLines.current.push(tpLine)
}
})
}
const generateSampleData = () => {
const data = []
const basePrice = 166.5
let currentPrice = basePrice
const baseDate = new Date()
for (let i = 0; i < 100; i++) {
// Generate data for the last 100 days, one point per day
const currentTime = new Date(baseDate.getTime() - (99 - i) * 24 * 60 * 60 * 1000)
const timeString = currentTime.toISOString().split('T')[0] // YYYY-MM-DD format
const volatility = 0.02
const change = (Math.random() - 0.5) * volatility * currentPrice
const open = currentPrice
const close = currentPrice + change
const high = Math.max(open, close) + Math.random() * 0.01 * currentPrice
const low = Math.min(open, close) - Math.random() * 0.01 * currentPrice
data.push({
time: timeString,
open: Number(open.toFixed(2)),
high: Number(high.toFixed(2)),
low: Number(low.toFixed(2)),
close: Number(close.toFixed(2)),
})
currentPrice = close
}
return data
}
initChart()
}, [])
// Update position overlays when positions change
useEffect(() => {
if (chart.current && !isLoading && positions.length > 0) {
import('lightweight-charts').then(({ LineStyle }) => {
// Re-add position overlays (this is a simplified version)
// In a full implementation, you'd want to properly manage line updates
})
}
}, [positions, isLoading])
if (isLoading) {
return (
<div className="h-[600px] bg-gray-900 rounded-lg flex items-center justify-center">
<div className="text-white">Loading chart...</div>
</div>
)
}
return (
<div className="relative">
{/* Chart Header */}
<div className="absolute top-4 left-4 z-10 bg-gray-800/80 rounded-lg px-3 py-2">
<div className="flex items-center space-x-4">
<div className="text-white font-bold text-lg">{symbol}</div>
<div className="text-green-400 font-semibold">$166.21</div>
<div className="text-green-400 text-sm">+1.42%</div>
</div>
</div>
{/* Position Info */}
{positions.length > 0 && (
<div className="absolute top-4 right-4 z-10 bg-gray-800/80 rounded-lg px-3 py-2">
<div className="text-white text-sm">
{positions.map(position => (
<div key={position.id} className="flex items-center space-x-2">
<div className={`w-2 h-2 rounded-full ${
position.side === 'LONG' ? 'bg-green-400' : 'bg-red-400'
}`}></div>
<span>{position.side} {position.size}</span>
<span className={position.pnl >= 0 ? 'text-green-400' : 'text-red-400'}>
{position.pnl >= 0 ? '+' : ''}${position.pnl.toFixed(2)}
</span>
</div>
))}
</div>
</div>
)}
{/* Chart Container */}
<div ref={chartContainerRef} className="w-full h-[600px] bg-gray-900 rounded-lg" />
</div>
)
}

View File

@@ -0,0 +1,218 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
interface CandlestickData {
time: string
open: number
high: number
low: number
close: number
}
interface TradingChartProps {
symbol?: string
positions?: any[]
}
export default function WorkingTradingChart({ symbol = 'SOL/USDC', positions = [] }: TradingChartProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [status, setStatus] = useState('Initializing...')
const [error, setError] = useState<string | null>(null)
useEffect(() => {
try {
setStatus('Generating data...')
// Generate sample candlestick data
const data: CandlestickData[] = []
const basePrice = 166.5
let currentPrice = basePrice
const today = new Date()
for (let i = 29; i >= 0; i--) {
const date = new Date(today)
date.setDate(date.getDate() - i)
const volatility = 0.02
const change = (Math.random() - 0.5) * volatility * currentPrice
const open = currentPrice
const close = currentPrice + change
const high = Math.max(open, close) + Math.random() * 0.01 * currentPrice
const low = Math.min(open, close) - Math.random() * 0.01 * currentPrice
data.push({
time: date.toISOString().split('T')[0],
open: Number(open.toFixed(2)),
high: Number(high.toFixed(2)),
low: Number(low.toFixed(2)),
close: Number(close.toFixed(2)),
})
currentPrice = close
}
setStatus('Drawing chart...')
drawChart(data)
setStatus('Chart ready!')
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err)
setError(errorMessage)
setStatus(`Error: ${errorMessage}`)
console.error('Chart error:', err)
}
}, [])
const drawChart = (data: CandlestickData[]) => {
const canvas = canvasRef.current
if (!canvas) {
throw new Error('Canvas element not found')
}
const ctx = canvas.getContext('2d')
if (!ctx) {
throw new Error('Could not get 2D context')
}
// Set canvas size
canvas.width = 800
canvas.height = 400
// Clear canvas
ctx.fillStyle = '#1a1a1a'
ctx.fillRect(0, 0, canvas.width, canvas.height)
if (data.length === 0) {
ctx.fillStyle = '#ffffff'
ctx.font = '16px Arial'
ctx.textAlign = 'center'
ctx.fillText('No data available', canvas.width / 2, canvas.height / 2)
return
}
// Calculate price range
const prices = data.flatMap(d => [d.open, d.high, d.low, d.close])
const minPrice = Math.min(...prices)
const maxPrice = Math.max(...prices)
const priceRange = maxPrice - minPrice
const padding = priceRange * 0.1
// Chart dimensions
const chartLeft = 60
const chartRight = canvas.width - 40
const chartTop = 40
const chartBottom = canvas.height - 60
const chartWidth = chartRight - chartLeft
const chartHeight = chartBottom - chartTop
// Draw grid lines
ctx.strokeStyle = '#333333'
ctx.lineWidth = 1
// Horizontal grid lines (price levels)
for (let i = 0; i <= 5; i++) {
const y = chartTop + (chartHeight / 5) * i
ctx.beginPath()
ctx.moveTo(chartLeft, y)
ctx.lineTo(chartRight, y)
ctx.stroke()
// Price labels
const price = maxPrice + padding - ((maxPrice + padding - (minPrice - padding)) / 5) * i
ctx.fillStyle = '#888888'
ctx.font = '12px Arial'
ctx.textAlign = 'right'
ctx.fillText(price.toFixed(2), chartLeft - 10, y + 4)
}
// Vertical grid lines (time)
const timeStep = Math.max(1, Math.floor(data.length / 6))
for (let i = 0; i < data.length; i += timeStep) {
const x = chartLeft + (chartWidth / (data.length - 1)) * i
ctx.beginPath()
ctx.moveTo(x, chartTop)
ctx.lineTo(x, chartBottom)
ctx.stroke()
// Time labels
ctx.fillStyle = '#888888'
ctx.font = '12px Arial'
ctx.textAlign = 'center'
ctx.fillText(data[i].time.split('-')[1] + '/' + data[i].time.split('-')[2], x, chartBottom + 20)
}
// Draw candlesticks
const candleWidth = Math.max(2, chartWidth / data.length * 0.6)
data.forEach((candle, index) => {
const x = chartLeft + (chartWidth / (data.length - 1)) * index
const openY = chartBottom - ((candle.open - (minPrice - padding)) / (maxPrice + padding - (minPrice - padding))) * chartHeight
const closeY = chartBottom - ((candle.close - (minPrice - padding)) / (maxPrice + padding - (minPrice - padding))) * chartHeight
const highY = chartBottom - ((candle.high - (minPrice - padding)) / (maxPrice + padding - (minPrice - padding))) * chartHeight
const lowY = chartBottom - ((candle.low - (minPrice - padding)) / (maxPrice + padding - (minPrice - padding))) * chartHeight
const isGreen = candle.close > candle.open
ctx.strokeStyle = isGreen ? '#26a69a' : '#ef5350'
ctx.fillStyle = isGreen ? '#26a69a' : '#ef5350'
ctx.lineWidth = 1
// Draw wick
ctx.beginPath()
ctx.moveTo(x, highY)
ctx.lineTo(x, lowY)
ctx.stroke()
// Draw body
const bodyTop = Math.min(openY, closeY)
const bodyHeight = Math.abs(closeY - openY)
if (bodyHeight < 1) {
// Doji - draw a line
ctx.beginPath()
ctx.moveTo(x - candleWidth / 2, openY)
ctx.lineTo(x + candleWidth / 2, openY)
ctx.stroke()
} else {
ctx.fillRect(x - candleWidth / 2, bodyTop, candleWidth, bodyHeight)
}
})
// Draw chart border
ctx.strokeStyle = '#555555'
ctx.lineWidth = 1
ctx.strokeRect(chartLeft, chartTop, chartWidth, chartHeight)
// Draw title
ctx.fillStyle = '#ffffff'
ctx.font = 'bold 16px Arial'
ctx.textAlign = 'left'
ctx.fillText(symbol, chartLeft, 25)
// Draw current price
const currentPrice = data[data.length - 1].close
ctx.fillStyle = '#26a69a'
ctx.font = 'bold 14px Arial'
ctx.textAlign = 'right'
ctx.fillText(`$${currentPrice.toFixed(2)}`, chartRight, 25)
}
return (
<div className="w-full">
<div className="mb-4">
<div className="text-white text-sm">Status: <span className="text-green-400">{status}</span></div>
{error && (
<div className="text-red-400 text-sm mt-2">Error: {error}</div>
)}
</div>
<div className="bg-gray-800 rounded-lg p-4">
<canvas
ref={canvasRef}
className="w-full h-auto border border-gray-600 rounded"
style={{ maxWidth: '100%', height: 'auto' }}
/>
</div>
</div>
)
}

View File

@@ -2,7 +2,8 @@ services:
app:
container_name: trader_dev
build:
target: development # Use development target for faster builds
context: .
dockerfile: Dockerfile
args:
- BUILDKIT_INLINE_CACHE=1
- NODE_VERSION=20.11.1
@@ -50,7 +51,7 @@ services:
# Port mapping for development
ports:
- "9000:3000"
- "9001:3000"
# X11 and display configuration for manual CAPTCHA solving
privileged: true

View File

@@ -3,6 +3,7 @@ import fs from 'fs/promises'
import path from 'path'
import { enhancedScreenshotService, ScreenshotConfig } from './enhanced-screenshot'
import { TradingViewCredentials } from './tradingview-automation'
import { progressTracker } from './progress-tracker'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
@@ -70,16 +71,61 @@ export interface AnalysisResult {
}
export class AIAnalysisService {
async analyzeScreenshot(filename: string): Promise<AnalysisResult | null> {
async analyzeScreenshot(filenameOrPath: string): Promise<AnalysisResult | null> {
try {
const screenshotsDir = path.join(process.cwd(), 'screenshots')
const imagePath = path.join(screenshotsDir, filename)
let imagePath: string
// Check if it's already a full path or just a filename
if (path.isAbsolute(filenameOrPath)) {
// It's already a full path
imagePath = filenameOrPath
} else {
// It's just a filename, construct the full path
const screenshotsDir = path.join(process.cwd(), 'screenshots')
imagePath = path.join(screenshotsDir, filenameOrPath)
}
// Read image file
const imageBuffer = await fs.readFile(imagePath)
const base64Image = imageBuffer.toString('base64')
const prompt = `You are now a professional trading assistant. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
⚠️ CRITICAL RSI READING INSTRUCTION: The RSI indicator shows a numerical value AND a line position. IGNORE the number if it conflicts with the visual line position. If the RSI line appears in the top area of the indicator (above the 70 horizontal line), report it as OVERBOUGHT regardless of what number is displayed.
**CRITICAL: FIRST IDENTIFY THE LAYOUT TYPE**
Before analyzing any indicators, you MUST determine which layout you are looking at:
**AI Layout identification:**
- Has RSI at the TOP of the chart
- Has MACD at the BOTTOM of the chart
- Has EMAs (9, 20, 50, 200) visible on the main chart
- Does NOT have VWAP or OBV
**DIY Layout identification:**
- Has Stochastic RSI at the TOP of the chart
- Has OBV (On-Balance Volume) at the BOTTOM of the chart
- Has VWAP (thick line) visible on the main chart
- Does NOT have regular RSI or MACD
**LAYOUT-SPECIFIC INDICATOR INFORMATION:**
If this is an AI Layout screenshot, it contains:
- TOP: RSI indicator (overbought above 70, oversold below 30)
- MIDDLE (on chart): SVP, ATR Bands, EMA 9, EMA 20, EMA 50, EMA 200
- BOTTOM: MACD indicator (NOT AT TOP - this is at the bottom of the chart)
* MACD has two lines: MACD line (usually blue/faster) and Signal line (usually red/slower)
* Bullish crossover = MACD line crosses ABOVE signal line (upward momentum)
* Bearish crossover = MACD line crosses BELOW signal line (downward momentum)
* Histogram bars: Green = bullish momentum, Red = bearish momentum
* Zero line: Above = overall bullish trend, Below = overall bearish trend
If this is a DIY Module Layout screenshot, it contains:
- TOP: Stochastic RSI indicator
- MIDDLE (on chart): VWAP, Smart Money Concepts by Algo
- BOTTOM: OBV (On-Balance Volume) indicator
**TRADING ANALYSIS REQUIREMENTS:**
1. **TIMEFRAME RISK ASSESSMENT**: Based on the timeframe shown in the screenshot, adjust risk accordingly:
@@ -104,9 +150,23 @@ export class AIAnalysisService {
4. **CONFIRMATION TRIGGERS**: Exact signals to wait for:
- Specific candle patterns, indicator crosses, volume confirmations
- RSI behavior: "If RSI crosses above 70 while price is under resistance → wait"
- RSI/Stoch RSI behavior:
* MANDATORY: State if RSI is "OVERBOUGHT" (line above 70), "OVERSOLD" (line below 30), or "NEUTRAL" (between 30-70)
* Do NOT say "above 50 line" - only report overbought/oversold/neutral status
* If RSI line appears in upper area of indicator box, it's likely overbought regardless of number
- VWAP: "If price retakes VWAP with bullish momentum → consider invalidation"
- OBV: "If OBV starts climbing while price stays flat → early exit or reconsider bias"
- MACD: Analyze MACD crossovers at the BOTTOM indicator panel.
* Bullish crossover = MACD line (faster line) crosses ABOVE signal line (slower line) - indicates upward momentum
* Bearish crossover = MACD line crosses BELOW signal line - indicates downward momentum
* Histogram: Green bars = increasing bullish momentum, Red bars = increasing bearish momentum
* Report specific crossover direction and current momentum state
- EMA alignment: Check 9/20/50/200 EMA positioning and price relationship
- Smart Money Concepts: Identify supply/demand zones and market structure
5. **LAYOUT-SPECIFIC ANALYSIS**:
- AI Layout: Focus on RSI momentum (MUST identify overbought/oversold status), EMA alignment, MACD signals, and ATR bands for volatility
- DIY Layout: Emphasize VWAP positioning, Stoch RSI oversold/overbought levels, OBV volume confirmation, and Smart Money Concepts structure
Examine the chart and identify:
- Current price action and trend direction
@@ -118,6 +178,7 @@ Examine the chart and identify:
Provide your analysis in this exact JSON format (replace values with your analysis):
{
"layoutDetected": "AI Layout|DIY Layout",
"summary": "Objective technical analysis with timeframe risk assessment and specific trading setup",
"marketSentiment": "BULLISH|BEARISH|NEUTRAL",
"keyLevels": {
@@ -153,10 +214,14 @@ Provide your analysis in this exact JSON format (replace values with your analys
"riskToReward": "1:2",
"confirmationTrigger": "Specific signal: Bearish engulfing candle on rejection from VWAP zone with RSI under 50",
"indicatorAnalysis": {
"rsi": "Specific RSI level and precise interpretation with action triggers",
"vwap": "VWAP relationship to price with exact invalidation levels",
"obv": "Volume analysis with specific behavioral expectations",
"macd": "MACD signal line crosses and momentum analysis"
"rsi": "ONLY if AI Layout detected: RSI status - MUST be 'OVERBOUGHT' (above 70 line), 'OVERSOLD' (below 30 line), or 'NEUTRAL' (30-70). Do NOT reference 50 line position.",
"stochRsi": "ONLY if DIY Layout detected: Stochastic RSI oversold/overbought conditions - check both %K and %D lines",
"vwap": "ONLY if DIY Layout detected: VWAP relationship to price with exact invalidation levels",
"obv": "ONLY if DIY Layout detected: OBV volume analysis with specific behavioral expectations",
"macd": "ONLY if AI Layout detected: MACD analysis - The MACD is located at the BOTTOM of the chart. Analyze: 1) Histogram bars (green = bullish momentum, red = bearish), 2) Signal line crossover (MACD line crossing ABOVE signal line = bullish crossover, BELOW = bearish crossover), 3) Zero line position. Report specific crossover direction and current momentum state.",
"emaAlignment": "If AI Layout: EMA 9/20/50/200 positioning and price relationship - note stack order and price position",
"atrBands": "If AI Layout: ATR bands for volatility and support/resistance",
"smartMoney": "If DIY Layout: Smart Money Concepts supply/demand zones and structure"
},
"timeframeRisk": {
"assessment": "Risk level based on detected timeframe",
@@ -245,14 +310,23 @@ Return only the JSON object with your technical analysis.`
}
}
async analyzeMultipleScreenshots(filenames: string[]): Promise<AnalysisResult | null> {
async analyzeMultipleScreenshots(filenamesOrPaths: string[]): Promise<AnalysisResult | null> {
try {
const screenshotsDir = path.join(process.cwd(), 'screenshots')
// Read all image files and convert to base64
const images = await Promise.all(
filenames.map(async (filename) => {
const imagePath = path.join(screenshotsDir, filename)
filenamesOrPaths.map(async (filenameOrPath) => {
let imagePath: string
// Check if it's already a full path or just a filename
if (path.isAbsolute(filenameOrPath)) {
// It's already a full path
imagePath = filenameOrPath
} else {
// It's just a filename, construct the full path
const screenshotsDir = path.join(process.cwd(), 'screenshots')
imagePath = path.join(screenshotsDir, filenameOrPath)
}
const imageBuffer = await fs.readFile(imagePath)
const base64Image = imageBuffer.toString('base64')
return {
@@ -265,15 +339,40 @@ Return only the JSON object with your technical analysis.`
})
)
const layoutInfo = filenames.map(f => {
if (f.includes('_ai_')) return 'AI Layout'
const layoutInfo = filenamesOrPaths.map(f => {
const filename = path.basename(f) // Extract filename from path
if (filename.includes('_ai_')) return 'AI Layout'
if (f.includes('_diy_') || f.includes('_Diy module_')) return 'DIY Module Layout'
return 'Unknown Layout'
}).join(' and ')
const prompt = `You are now a professional trading assistant. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
I'm providing you with ${filenames.length} TradingView chart screenshots from different layouts: ${layoutInfo}.
I'm providing you with ${filenamesOrPaths.length} TradingView chart screenshots from different layouts: ${layoutInfo}.
⚠️ CRITICAL RSI READING INSTRUCTION: The RSI indicator shows a numerical value AND a line position. IGNORE the number if it conflicts with the visual line position. If the RSI line appears in the top area of the indicator (above the 70 horizontal line), report it as OVERBOUGHT regardless of what number is displayed.
**LAYOUT-SPECIFIC INDICATOR INFORMATION:**
**AI Layout Structure:**
- TOP: RSI indicator (14-period) - Look for EXACT numerical value displayed and visual position relative to 30/50/70 levels
- MIDDLE (on chart): SVP, ATR Bands, EMA 9 (yellow), EMA 20 (orange), EMA 50 (blue), EMA 200 (red)
- BOTTOM: MACD indicator with signal line and histogram
**DIY Module Layout Structure:**
- TOP: Stochastic RSI indicator - Check both %K and %D lines relative to 20/50/80 levels
- MIDDLE (on chart): VWAP (thick line), Smart Money Concepts by Algo (supply/demand zones)
- BOTTOM: OBV (On-Balance Volume) indicator showing volume flow
**CRITICAL: ACCURATE INDICATOR READING:**
- RSI: IGNORE the numerical value if it conflicts with visual position. The RSI line position on the chart is what matters:
* If RSI line is visually ABOVE the 70 horizontal line = OVERBOUGHT (regardless of number shown)
* If RSI line is visually BELOW the 30 horizontal line = OVERSOLD (regardless of number shown)
* If RSI line is between 30-70 = NEUTRAL zone
* Example: If number shows "56.61" but line appears above 70 level, report as "RSI OVERBOUGHT at 70+ level"
- MACD: Check histogram bars (green/red) and signal line crossovers
- EMA Alignment: Note price position relative to each EMA and EMA stack order
- VWAP: Identify if price is above/below VWAP and by how much
**TRADING ANALYSIS REQUIREMENTS:**
@@ -299,12 +398,23 @@ I'm providing you with ${filenames.length} TradingView chart screenshots from di
4. **CONFIRMATION TRIGGERS**: Exact signals to wait for:
- Specific candle patterns, indicator crosses, volume confirmations
- RSI behavior: "If RSI crosses above 70 while price is under resistance → wait"
- RSI/Stoch RSI behavior: "If RSI crosses above 70 while price is under resistance → wait"
* CRITICAL: Read RSI visually - if the line appears above 70 level regardless of numerical display, treat as overbought
* If RSI line appears below 30 level visually, treat as oversold regardless of number shown
- VWAP: "If price retakes VWAP with bullish momentum → consider invalidation"
- OBV: "If OBV starts climbing while price stays flat → early exit or reconsider bias"
- MACD: Analyze MACD crossovers at the BOTTOM indicator panel.
* Bullish crossover = MACD line (faster line) crosses ABOVE signal line (slower line) - indicates upward momentum
* Bearish crossover = MACD line crosses BELOW signal line - indicates downward momentum
* Histogram: Green bars = increasing bullish momentum, Red bars = increasing bearish momentum
* Report specific crossover direction and current momentum state
- EMA alignment: Check 9/20/50/200 EMA positioning and price relationship
- Smart Money Concepts: Identify supply/demand zones and market structure
5. **CROSS-LAYOUT ANALYSIS**:
- Compare insights from different chart layouts for confirmations/contradictions
- AI Layout insights: RSI momentum + EMA alignment + MACD signals
- DIY Layout insights: VWAP positioning + Stoch RSI + OBV volume + Smart Money structure
- Use multiple perspectives to increase confidence
- Note which layout provides clearest signals
@@ -348,10 +458,14 @@ I'm providing you with ${filenames.length} TradingView chart screenshots from di
"riskToReward": "1:2.5",
"confirmationTrigger": "Specific signal: Bearish engulfing candle on rejection from VWAP zone with RSI under 50",
"indicatorAnalysis": {
"rsi": "Specific RSI level and precise interpretation with action triggers",
"vwap": "VWAP relationship to price with exact invalidation levels",
"obv": "Volume analysis with specific behavioral expectations",
"macd": "MACD signal line crosses and momentum analysis",
"rsi": "AI Layout: RSI status - MUST be 'OVERBOUGHT' (above 70 line), 'OVERSOLD' (below 30 line), or 'NEUTRAL' (30-70). Do NOT reference 50 line position.",
"stochRsi": "DIY Layout: Stochastic RSI oversold/overbought conditions",
"vwap": "DIY Layout: VWAP relationship to price with exact invalidation levels",
"obv": "DIY Layout: OBV volume analysis with specific behavioral expectations",
"macd": "AI Layout: MACD signal line crosses and histogram momentum analysis - green/red bars and signal line position",
"emaAlignment": "AI Layout: EMA 9/20/50/200 positioning and price relationship - note stack order and price position",
"atrBands": "AI Layout: ATR bands for volatility and support/resistance",
"smartMoney": "DIY Layout: Smart Money Concepts supply/demand zones and structure",
"crossLayoutConsensus": "Detailed comparison of how different layouts confirm or contradict signals"
},
"layoutComparison": {
@@ -384,7 +498,7 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
}
]
console.log(`🤖 Sending ${filenames.length} screenshots to OpenAI for multi-layout analysis...`)
console.log(`🤖 Sending ${filenamesOrPaths.length} screenshots to OpenAI for multi-layout analysis...`)
const response = await openai.chat.completions.create({
model: "gpt-4o-mini", // Cost-effective model with vision capabilities
@@ -491,10 +605,12 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
screenshots: string[]
analysis: AnalysisResult | null
}> {
const { sessionId } = config
try {
console.log(`Starting automated capture with config for ${config.symbol} ${config.timeframe}`)
// Capture screenshots using enhanced service
// Capture screenshots using enhanced service (this will handle its own progress)
const screenshots = await enhancedScreenshotService.captureWithLogin(config)
if (screenshots.length === 0) {
@@ -503,6 +619,11 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
console.log(`${screenshots.length} screenshot(s) captured`)
// Add AI analysis step to progress if sessionId exists
if (sessionId) {
progressTracker.updateStep(sessionId, 'analysis', 'active', 'Running AI analysis on screenshots...')
}
let analysis: AnalysisResult | null = null
if (screenshots.length === 1) {
@@ -514,11 +635,20 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
}
if (!analysis) {
if (sessionId) {
progressTracker.updateStep(sessionId, 'analysis', 'error', 'AI analysis failed to generate results')
}
throw new Error('Failed to analyze screenshots')
}
console.log(`Analysis completed for ${config.symbol} ${config.timeframe}`)
if (sessionId) {
progressTracker.updateStep(sessionId, 'analysis', 'completed', 'AI analysis completed successfully!')
// Mark session as complete
setTimeout(() => progressTracker.deleteSession(sessionId), 1000)
}
return {
screenshots,
analysis
@@ -526,6 +656,20 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
} catch (error) {
console.error('Automated capture and analysis with config failed:', error)
if (sessionId) {
// Find the active step and mark it as error
const progress = progressTracker.getProgress(sessionId)
if (progress) {
const activeStep = progress.steps.find(step => step.status === 'active')
if (activeStep) {
progressTracker.updateStep(sessionId, activeStep.id, 'error', error instanceof Error ? error.message : 'Unknown error')
}
}
// Clean up session
setTimeout(() => progressTracker.deleteSession(sessionId), 5000)
}
return {
screenshots: [],
analysis: null

View File

@@ -3,12 +3,14 @@ import fs from 'fs/promises'
import path from 'path'
import puppeteer from 'puppeteer'
import { Browser, Page } from 'puppeteer'
import { progressTracker, ProgressStep } from './progress-tracker'
export interface ScreenshotConfig {
symbol: string
timeframe: string
layouts?: string[] // Multiple chart layouts if needed
credentials?: TradingViewCredentials // Optional if using .env
sessionId?: string // For progress tracking
}
// Layout URL mappings for direct navigation
@@ -28,6 +30,13 @@ export class EnhancedScreenshotService {
console.log('📋 Config:', config)
const screenshotFiles: string[] = []
const { sessionId } = config
console.log('🔍 Enhanced Screenshot Service received sessionId:', sessionId)
// Progress tracking (session already created in API)
if (sessionId) {
progressTracker.updateStep(sessionId, 'init', 'active', 'Starting browser sessions...')
}
try {
// Ensure screenshots directory exists
@@ -39,8 +48,13 @@ export class EnhancedScreenshotService {
console.log(`\n🔄 Starting parallel capture of ${layoutsToCapture.length} layouts...`)
if (sessionId) {
progressTracker.updateStep(sessionId, 'init', 'completed', `Started ${layoutsToCapture.length} browser sessions`)
progressTracker.updateStep(sessionId, 'auth', 'active', 'Authenticating with TradingView...')
}
// Create parallel session promises for true dual-session approach
const sessionPromises = layoutsToCapture.map(async (layout) => {
const sessionPromises = layoutsToCapture.map(async (layout, index) => {
const layoutKey = layout.toLowerCase()
let layoutSession: TradingViewAutomation | null = null
@@ -71,6 +85,9 @@ export class EnhancedScreenshotService {
const isLoggedIn = await layoutSession.isLoggedIn()
if (!isLoggedIn) {
console.log(`🔐 Logging in to ${layout} session...`)
if (sessionId && index === 0) {
progressTracker.updateStep(sessionId, 'auth', 'active', `Logging into ${layout} session...`)
}
const loginSuccess = await layoutSession.smartLogin(config.credentials)
if (!loginSuccess) {
throw new Error(`Failed to login to ${layout} session`)
@@ -79,6 +96,12 @@ export class EnhancedScreenshotService {
console.log(`${layout} session already logged in`)
}
// Update auth progress when first session completes auth
if (sessionId && index === 0) {
progressTracker.updateStep(sessionId, 'auth', 'completed', 'TradingView authentication successful')
progressTracker.updateStep(sessionId, 'navigation', 'active', `Navigating to ${config.symbol} chart...`)
}
// Navigate directly to the specific layout URL with symbol and timeframe
const directUrl = `https://www.tradingview.com/chart/${layoutUrl}/?symbol=${config.symbol}&interval=${config.timeframe}`
console.log(`🌐 ${layout.toUpperCase()}: Navigating directly to ${directUrl}`)
@@ -132,6 +155,12 @@ export class EnhancedScreenshotService {
console.log(`${layout.toUpperCase()}: Successfully navigated to layout`)
// Update navigation progress when first session completes navigation
if (sessionId && index === 0) {
progressTracker.updateStep(sessionId, 'navigation', 'completed', 'Chart navigation successful')
progressTracker.updateStep(sessionId, 'loading', 'active', 'Loading chart data and indicators...')
}
// Progressive loading strategy: shorter initial wait, then chart-specific wait
console.log(`${layout.toUpperCase()}: Initial page stabilization (2s)...`)
await new Promise(resolve => setTimeout(resolve, 2000))
@@ -171,6 +200,12 @@ export class EnhancedScreenshotService {
await new Promise(resolve => setTimeout(resolve, 3000))
}
// Update loading progress when first session completes loading
if (sessionId && index === 0) {
progressTracker.updateStep(sessionId, 'loading', 'completed', 'Chart data loaded successfully')
progressTracker.updateStep(sessionId, 'capture', 'active', 'Capturing screenshots...')
}
// Take screenshot with better error handling
const filename = `${config.symbol}_${config.timeframe}_${layout}_${timestamp}.png`
console.log(`📸 Taking ${layout} screenshot: ${filename}`)
@@ -237,11 +272,27 @@ export class EnhancedScreenshotService {
}
})
if (sessionId) {
progressTracker.updateStep(sessionId, 'capture', 'completed', `Captured ${screenshotFiles.length}/${layoutsToCapture.length} screenshots`)
}
console.log(`\n🎯 Parallel capture completed: ${screenshotFiles.length}/${layoutsToCapture.length} screenshots`)
return screenshotFiles
} catch (error) {
console.error('Enhanced parallel screenshot capture failed:', error)
if (sessionId) {
// Mark the current active step as error
const progress = progressTracker.getProgress(sessionId)
if (progress) {
const activeStep = progress.steps.find(step => step.status === 'active')
if (activeStep) {
progressTracker.updateStep(sessionId, activeStep.id, 'error', error instanceof Error ? error.message : 'Unknown error')
}
}
}
throw error
}
}

View File

@@ -0,0 +1,490 @@
import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js'
import fetch from 'cross-fetch'
export interface TriggerOrder {
orderId: string
inputMint: string
outputMint: string
makingAmount: string
takingAmount: string
targetPrice: number
side: 'BUY' | 'SELL'
orderType: 'STOP_LOSS' | 'TAKE_PROFIT' | 'LIMIT'
status: 'PENDING' | 'EXECUTED' | 'CANCELLED' | 'EXPIRED'
createdAt: number
executedAt?: number
txId?: string
requestId?: string
}
class JupiterTriggerService {
private connection: Connection
private keypair: Keypair | null = null
private activeOrders: TriggerOrder[] = []
// Token mint addresses
private tokens = {
SOL: 'So11111111111111111111111111111111111111112', // Wrapped SOL
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
}
constructor() {
const rpcUrl = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'
this.connection = new Connection(rpcUrl, 'confirmed')
this.initializeWallet()
}
private initializeWallet() {
try {
if (process.env.SOLANA_PRIVATE_KEY) {
const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY)
this.keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray))
console.log('✅ Jupiter Trigger wallet initialized:', this.keypair.publicKey.toString())
} else {
console.warn('⚠️ No SOLANA_PRIVATE_KEY found for Jupiter Trigger')
}
} catch (error) {
console.error('❌ Failed to initialize Jupiter Trigger wallet:', error)
}
}
/**
* Create a stop loss order
* When current price drops to stopPrice, sell the token
*/
async createStopLossOrder(params: {
tokenSymbol: string
amount: number // Amount of tokens to sell
stopPrice: number // Price at which to trigger the sale
slippageBps?: number // Optional slippage (default 0 for exact execution)
expiredAt?: number // Optional expiry timestamp
}): Promise<{
success: boolean
orderId?: string
requestId?: string
transaction?: string
error?: string
}> {
if (!this.keypair) {
return { success: false, error: 'Wallet not initialized' }
}
try {
const { tokenSymbol, amount, stopPrice, slippageBps = 0, expiredAt } = params
console.log('🛑 Creating stop loss order:', params)
// Determine mint addresses
const inputMint = tokenSymbol === 'SOL' ? this.tokens.SOL : this.tokens.USDC
const outputMint = tokenSymbol === 'SOL' ? this.tokens.USDC : this.tokens.SOL
// Calculate amounts
const makingAmount = tokenSymbol === 'SOL'
? Math.floor(amount * 1_000_000_000).toString() // SOL has 9 decimals
: Math.floor(amount * 1_000_000).toString() // USDC has 6 decimals
const takingAmount = tokenSymbol === 'SOL'
? Math.floor(amount * stopPrice * 1_000_000).toString() // Convert to USDC
: Math.floor(amount / stopPrice * 1_000_000_000).toString() // Convert to SOL
const orderParams: any = {
inputMint,
outputMint,
maker: this.keypair.publicKey.toString(),
payer: this.keypair.publicKey.toString(),
params: {
makingAmount,
takingAmount,
},
computeUnitPrice: "auto"
}
// Add optional parameters
if (slippageBps > 0) {
orderParams.params.slippageBps = slippageBps.toString()
}
if (expiredAt) {
orderParams.params.expiredAt = expiredAt.toString()
}
// Create the trigger order
const response = await fetch('https://api.jup.ag/trigger/v1/createOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(orderParams)
})
if (!response.ok) {
const error = await response.json()
throw new Error(`Trigger API error: ${error.error || response.status}`)
}
const result = await response.json()
// Store the order locally
const order: TriggerOrder = {
orderId: result.order,
inputMint,
outputMint,
makingAmount,
takingAmount,
targetPrice: stopPrice,
side: 'SELL',
orderType: 'STOP_LOSS',
status: 'PENDING',
createdAt: Date.now(),
requestId: result.requestId
}
this.activeOrders.push(order)
console.log('✅ Stop loss order created:', result.order)
return {
success: true,
orderId: result.order,
requestId: result.requestId,
transaction: result.transaction
}
} catch (error: any) {
console.error('❌ Failed to create stop loss order:', error)
return { success: false, error: error.message }
}
}
/**
* Create a take profit order
* When current price rises to targetPrice, sell the token
*/
async createTakeProfitOrder(params: {
tokenSymbol: string
amount: number // Amount of tokens to sell
targetPrice: number // Price at which to trigger the sale
slippageBps?: number // Optional slippage (default 0 for exact execution)
expiredAt?: number // Optional expiry timestamp
}): Promise<{
success: boolean
orderId?: string
requestId?: string
transaction?: string
error?: string
}> {
if (!this.keypair) {
return { success: false, error: 'Wallet not initialized' }
}
try {
const { tokenSymbol, amount, targetPrice, slippageBps = 0, expiredAt } = params
console.log('🎯 Creating take profit order:', params)
// Determine mint addresses
const inputMint = tokenSymbol === 'SOL' ? this.tokens.SOL : this.tokens.USDC
const outputMint = tokenSymbol === 'SOL' ? this.tokens.USDC : this.tokens.SOL
// Calculate amounts
const makingAmount = tokenSymbol === 'SOL'
? Math.floor(amount * 1_000_000_000).toString() // SOL has 9 decimals
: Math.floor(amount * 1_000_000).toString() // USDC has 6 decimals
const takingAmount = tokenSymbol === 'SOL'
? Math.floor(amount * targetPrice * 1_000_000).toString() // Convert to USDC
: Math.floor(amount / targetPrice * 1_000_000_000).toString() // Convert to SOL
const orderParams: any = {
inputMint,
outputMint,
maker: this.keypair.publicKey.toString(),
payer: this.keypair.publicKey.toString(),
params: {
makingAmount,
takingAmount,
},
computeUnitPrice: "auto"
}
// Add optional parameters
if (slippageBps > 0) {
orderParams.params.slippageBps = slippageBps.toString()
}
if (expiredAt) {
orderParams.params.expiredAt = expiredAt.toString()
}
// Create the trigger order
const response = await fetch('https://api.jup.ag/trigger/v1/createOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(orderParams)
})
if (!response.ok) {
const error = await response.json()
throw new Error(`Trigger API error: ${error.error || response.status}`)
}
const result = await response.json()
// Store the order locally
const order: TriggerOrder = {
orderId: result.order,
inputMint,
outputMint,
makingAmount,
takingAmount,
targetPrice: targetPrice,
side: 'SELL',
orderType: 'TAKE_PROFIT',
status: 'PENDING',
createdAt: Date.now(),
requestId: result.requestId
}
this.activeOrders.push(order)
console.log('✅ Take profit order created:', result.order)
return {
success: true,
orderId: result.order,
requestId: result.requestId,
transaction: result.transaction
}
} catch (error: any) {
console.error('❌ Failed to create take profit order:', error)
return { success: false, error: error.message }
}
}
/**
* Execute (sign and send) a trigger order transaction
*/
async executeOrder(transaction: string, requestId: string): Promise<{
success: boolean
txId?: string
error?: string
}> {
if (!this.keypair) {
return { success: false, error: 'Wallet not initialized' }
}
try {
console.log('⚡ Executing trigger order transaction')
// Deserialize and sign transaction
const transactionBuf = Buffer.from(transaction, 'base64')
const versionedTransaction = VersionedTransaction.deserialize(transactionBuf)
versionedTransaction.sign([this.keypair])
// Send via Jupiter's execute endpoint
const response = await fetch('https://api.jup.ag/trigger/v1/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestId,
transaction: Buffer.from(versionedTransaction.serialize()).toString('base64')
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(`Execute API error: ${error.error || response.status}`)
}
const result = await response.json()
console.log('✅ Trigger order executed:', result.txId)
// Update local order status
const order = this.activeOrders.find(o => o.requestId === requestId)
if (order) {
order.status = 'PENDING'
order.txId = result.txId
}
return {
success: true,
txId: result.txId
}
} catch (error: any) {
console.error('❌ Failed to execute trigger order:', error)
return { success: false, error: error.message }
}
}
/**
* Cancel a trigger order
*/
async cancelOrder(orderId: string): Promise<{
success: boolean
txId?: string
error?: string
}> {
if (!this.keypair) {
return { success: false, error: 'Wallet not initialized' }
}
try {
console.log('❌ Cancelling trigger order:', orderId)
const response = await fetch('https://api.jup.ag/trigger/v1/cancelOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
order: orderId,
owner: this.keypair.publicKey.toString(),
computeUnitPrice: "auto"
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(`Cancel API error: ${error.error || response.status}`)
}
const result = await response.json()
// Sign and send the cancel transaction
const transactionBuf = Buffer.from(result.transaction, 'base64')
const versionedTransaction = VersionedTransaction.deserialize(transactionBuf)
versionedTransaction.sign([this.keypair])
const txId = await this.connection.sendTransaction(versionedTransaction)
const confirmation = await this.connection.confirmTransaction(txId, 'confirmed')
if (confirmation.value.err) {
throw new Error(`Cancel transaction failed: ${confirmation.value.err}`)
}
// Update local order status
const order = this.activeOrders.find(o => o.orderId === orderId)
if (order) {
order.status = 'CANCELLED'
}
console.log('✅ Trigger order cancelled:', txId)
return {
success: true,
txId
}
} catch (error: any) {
console.error('❌ Failed to cancel trigger order:', error)
return { success: false, error: error.message }
}
}
/**
* Get active trigger orders for the wallet
*/
async getTriggerOrders(): Promise<TriggerOrder[]> {
if (!this.keypair) {
return []
}
try {
const response = await fetch(`https://api.jup.ag/trigger/v1/getTriggerOrders?wallet=${this.keypair.publicKey.toString()}&active=true`)
if (!response.ok) {
throw new Error(`Get orders API error: ${response.status}`)
}
const result = await response.json()
console.log('📊 Retrieved trigger orders:', result.orders?.length || 0)
return result.orders || []
} catch (error: any) {
console.error('❌ Failed to get trigger orders:', error)
return this.activeOrders // Fallback to local orders
}
}
/**
* Create both stop loss and take profit orders for a trade
*/
async createTradingOrders(params: {
tokenSymbol: string
amount: number
stopLoss?: number
takeProfit?: number
slippageBps?: number
expiredAt?: number
}): Promise<{
success: boolean
stopLossOrder?: string
takeProfitOrder?: string
transactions?: string[]
error?: string
}> {
const { tokenSymbol, amount, stopLoss, takeProfit, slippageBps, expiredAt } = params
const results: any = { success: true, transactions: [] }
try {
// Create stop loss order
if (stopLoss) {
const slResult = await this.createStopLossOrder({
tokenSymbol,
amount,
stopPrice: stopLoss,
slippageBps,
expiredAt
})
if (slResult.success) {
results.stopLossOrder = slResult.orderId
results.transactions.push(slResult.transaction)
} else {
console.warn('⚠️ Failed to create stop loss order:', slResult.error)
}
}
// Create take profit order
if (takeProfit) {
const tpResult = await this.createTakeProfitOrder({
tokenSymbol,
amount,
targetPrice: takeProfit,
slippageBps,
expiredAt
})
if (tpResult.success) {
results.takeProfitOrder = tpResult.orderId
results.transactions.push(tpResult.transaction)
} else {
console.warn('⚠️ Failed to create take profit order:', tpResult.error)
}
}
return results
} catch (error: any) {
console.error('❌ Failed to create trading orders:', error)
return { success: false, error: error.message }
}
}
getLocalOrders(): TriggerOrder[] {
return this.activeOrders
}
isConfigured(): boolean {
return this.keypair !== null
}
}
export const jupiterTriggerService = new JupiterTriggerService()
export default JupiterTriggerService

115
lib/progress-tracker.ts Normal file
View File

@@ -0,0 +1,115 @@
import { EventEmitter } from 'events'
export type ProgressStatus = 'pending' | 'active' | 'completed' | 'error'
export interface ProgressStep {
id: string
title: string
description: string
status: ProgressStatus
startTime?: number
endTime?: number
details?: string
}
export interface AnalysisProgress {
sessionId: string
currentStep: number
totalSteps: number
steps: ProgressStep[]
timeframeProgress?: {
current: number
total: number
currentTimeframe?: string
}
}
class ProgressTracker extends EventEmitter {
private sessions: Map<string, AnalysisProgress> = new Map()
createSession(sessionId: string, steps: ProgressStep[]): AnalysisProgress {
const progress: AnalysisProgress = {
sessionId,
currentStep: 0,
totalSteps: steps.length,
steps: steps.map(step => ({ ...step, status: 'pending' }))
}
this.sessions.set(sessionId, progress)
this.emit(`progress:${sessionId}`, progress)
return progress
}
updateStep(sessionId: string, stepId: string, status: ProgressStatus, details?: string): void {
console.log(`🔍 Progress Update: ${sessionId} -> ${stepId} -> ${status}${details ? ` (${details})` : ''}`)
const progress = this.sessions.get(sessionId)
if (!progress) {
console.log(`🔍 Warning: No session found for ${sessionId}`)
return
}
const updatedSteps = progress.steps.map(step => {
if (step.id === stepId) {
const updatedStep = {
...step,
status,
details: details || step.details
}
if (status === 'active' && !step.startTime) {
updatedStep.startTime = Date.now()
} else if ((status === 'completed' || status === 'error') && !step.endTime) {
updatedStep.endTime = Date.now()
}
return updatedStep
}
return step
})
const currentStepIndex = updatedSteps.findIndex(step => step.status === 'active')
const updatedProgress: AnalysisProgress = {
...progress,
steps: updatedSteps,
currentStep: currentStepIndex >= 0 ? currentStepIndex + 1 : progress.currentStep
}
this.sessions.set(sessionId, updatedProgress)
console.log(`🔍 Emitting progress event for ${sessionId}, currentStep: ${updatedProgress.currentStep}`)
this.emit(`progress:${sessionId}`, updatedProgress)
}
updateTimeframeProgress(sessionId: string, current: number, total: number, currentTimeframe?: string): void {
const progress = this.sessions.get(sessionId)
if (!progress) return
const updatedProgress: AnalysisProgress = {
...progress,
timeframeProgress: {
current,
total,
currentTimeframe
}
}
this.sessions.set(sessionId, updatedProgress)
this.emit(`progress:${sessionId}`, updatedProgress)
}
getProgress(sessionId: string): AnalysisProgress | undefined {
return this.sessions.get(sessionId)
}
deleteSession(sessionId: string): void {
this.sessions.delete(sessionId)
this.emit(`progress:${sessionId}:complete`)
}
// Get all active sessions (for debugging)
getActiveSessions(): string[] {
return Array.from(this.sessions.keys())
}
}
export const progressTracker = new ProgressTracker()

109
test-drift-funds.mjs Normal file
View File

@@ -0,0 +1,109 @@
import { Connection, PublicKey } from '@solana/web3.js';
async function testDriftAccount() {
try {
console.log('🔍 Testing Drift account access...');
const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
const accountPDA = '7LonnWut5i3h36xyMA5jbwnGFbnzXUPY2dsPfNaSsrTk';
console.log('📡 Connecting to Solana...');
const accountInfo = await connection.getAccountInfo(new PublicKey(accountPDA));
if (!accountInfo) {
console.log('❌ Drift account not found');
return;
}
console.log('✅ Drift account found!');
console.log(`📊 Account data size: ${accountInfo.data.length} bytes`);
console.log(`💰 Account lamports: ${accountInfo.lamports}`);
console.log(`👤 Owner: ${accountInfo.owner.toBase58()}`);
// Try to parse balance data from multiple known offsets
const data = accountInfo.data;
console.log(`\n🔍 Scanning for USDC balance...`);
// Try multiple offsets where USDC balance might be stored
const offsets = [106, 114, 122, 130, 138, 146, 154];
for (const offset of offsets) {
try {
const balance = data.readBigInt64LE(offset);
const value = Number(balance) / 1_000_000; // USDC has 6 decimals
if (value > 0 && value < 1000000) { // Reasonable range
console.log(` Offset ${offset}: $${value.toFixed(6)}`);
}
} catch (e) {
// Skip invalid offsets
}
}
console.log(`\n🔍 Scanning for SOL position...`);
// Try multiple offsets for SOL position
const solOffsets = [432, 440, 448, 456, 464, 472];
for (const offset of solOffsets) {
try {
const position = data.readBigInt64LE(offset);
const amount = Number(position) / 1_000_000_000; // SOL has 9 decimals
if (Math.abs(amount) > 0.001 && Math.abs(amount) < 1000) { // Reasonable range
console.log(` Offset ${offset}: ${amount.toFixed(6)} SOL`);
}
} catch (e) {
// Skip invalid offsets
}
}
// Find the best USDC balance (likely the $23 you mentioned)
let bestUsdcValue = 0;
let bestSolAmount = 0;
for (const offset of offsets) {
try {
const balance = data.readBigInt64LE(offset);
const value = Number(balance) / 1_000_000; // USDC has 6 decimals
if (value > bestUsdcValue && value < 1000000) { // Take the highest reasonable value
bestUsdcValue = value;
}
} catch (e) {
// Skip invalid offsets
}
}
for (const offset of solOffsets) {
try {
const position = data.readBigInt64LE(offset);
const amount = Number(position) / 1_000_000_000; // SOL has 9 decimals
if (Math.abs(amount) > Math.abs(bestSolAmount) && Math.abs(amount) < 1000) {
bestSolAmount = amount;
}
} catch (e) {
// Skip invalid offsets
}
}
console.log(`\n💵 Best parsed balances:`);
console.log(` USDC: $${bestUsdcValue.toFixed(6)}`);
console.log(` SOL: ${bestSolAmount.toFixed(6)} SOL`);
if (bestUsdcValue > 20) {
console.log(`\n🎉 Found your $${bestUsdcValue.toFixed(2)} USDC!`);
}
if (bestUsdcValue > 1 || Math.abs(bestSolAmount) > 0.01) {
console.log('\n✅ Sufficient funds for leveraged trading!');
console.log('🎯 Ready to implement real Drift perpetual trading');
console.log(`📊 With $${bestUsdcValue.toFixed(2)} you can open positions up to $${(bestUsdcValue * 10).toFixed(2)} with 10x leverage`);
} else {
console.log('\n⚠ Limited funds detected');
}
} catch (error) {
console.error('❌ Test failed:', error.message);
}
}
testDriftAccount();

43
test-trade-validation.js Normal file
View File

@@ -0,0 +1,43 @@
// Quick test to verify the trade validation fix
const testTradeValidation = async () => {
console.log('🧪 Testing trade validation with fixed amountUSD...')
// Use port 9000 for Docker or 3001 for local dev
const apiUrl = process.env.DOCKER_MODE ? 'http://localhost:9000' : 'http://localhost:3001'
const tradeData = {
symbol: 'USDCUSD',
side: 'BUY',
amount: 5,
amountUSD: 5, // This should be passed through correctly now
useRealDEX: false, // Use simulation for testing
tradingPair: 'USDCUSD/USDC'
}
console.log('🚀 Sending test trade to:', apiUrl)
console.log('🚀 Trade data:', tradeData)
try {
const response = await fetch(`${apiUrl}/api/trading/execute-dex`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tradeData)
})
const result = await response.json()
console.log('📊 Response status:', response.status)
console.log('📊 Response body:', result)
if (response.ok) {
console.log('✅ Trade validation fix is working!')
} else {
console.log('❌ Trade validation still has issues:', result.message)
}
} catch (error) {
console.error('❌ Test failed:', error)
}
}
testTradeValidation()