Compare commits
8 Commits
1a7bdb4109
...
74b0087f17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74b0087f17 | ||
|
|
2bdf9e2b41 | ||
|
|
bd49c65867 | ||
|
|
ba354c609d | ||
|
|
56409b1161 | ||
|
|
6232c457ad | ||
|
|
186cb6355c | ||
|
|
1b0d92d6ad |
@@ -74,9 +74,14 @@ RUN chmod +x node_modules/.bin/*
|
|||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Copy startup script
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
# Set environment variables for Puppeteer
|
# Set environment variables for Puppeteer
|
||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
|
ENV DOCKER_ENV=true
|
||||||
|
|
||||||
# Start the app (default to development mode)
|
# Start the app with cleanup handlers
|
||||||
CMD ["npm", "run", "dev:docker"]
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
|
|||||||
276
app/api/batch-analysis/route.js
Normal file
276
app/api/batch-analysis/route.js
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
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
|
||||||
|
|
||||||
|
console.log('📊 Batch analysis request:', { symbol, layouts, timeframes, selectedLayouts })
|
||||||
|
|
||||||
|
// Validate inputs
|
||||||
|
if (!symbol || !timeframes || !Array.isArray(timeframes) || timeframes.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Invalid request: symbol and timeframes array required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique session ID for progress tracking
|
||||||
|
const sessionId = `batch_analysis_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
console.log('🔍 Created batch analysis session ID:', sessionId)
|
||||||
|
|
||||||
|
// Create progress tracking session with initial steps
|
||||||
|
const initialSteps = [
|
||||||
|
{
|
||||||
|
id: 'init',
|
||||||
|
title: 'Initializing Batch Analysis',
|
||||||
|
description: 'Starting multi-timeframe 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 screenshots for ${timeframes.length} timeframes`,
|
||||||
|
status: 'pending'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'analysis',
|
||||||
|
title: 'AI Analysis',
|
||||||
|
description: 'Analyzing all screenshots with AI',
|
||||||
|
status: 'pending'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Create the progress session
|
||||||
|
progressTracker.createSession(sessionId, initialSteps)
|
||||||
|
|
||||||
|
// Prepare base configuration
|
||||||
|
const baseConfig = {
|
||||||
|
symbol: symbol || 'BTCUSD',
|
||||||
|
layouts: layouts || selectedLayouts || ['ai', 'diy'],
|
||||||
|
sessionId,
|
||||||
|
credentials: {
|
||||||
|
email: process.env.TRADINGVIEW_EMAIL,
|
||||||
|
password: process.env.TRADINGVIEW_PASSWORD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔧 Base config:', baseConfig)
|
||||||
|
|
||||||
|
let allScreenshots = []
|
||||||
|
const screenshotResults = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
// STEP 1: Collect ALL screenshots from ALL timeframes FIRST
|
||||||
|
console.log(`🔄 Starting batch screenshot collection for ${timeframes.length} timeframes...`)
|
||||||
|
|
||||||
|
progressTracker.updateStep(sessionId, 'init', 'active', 'Starting batch screenshot collection...')
|
||||||
|
|
||||||
|
for (let i = 0; i < timeframes.length; i++) {
|
||||||
|
const timeframe = timeframes[i]
|
||||||
|
const timeframeLabel = getTimeframeLabel(timeframe)
|
||||||
|
|
||||||
|
console.log(`📸 Collecting screenshots for ${symbol} ${timeframeLabel} (${i + 1}/${timeframes.length})`)
|
||||||
|
|
||||||
|
// Update progress for current timeframe
|
||||||
|
progressTracker.updateStep(sessionId, 'capture', 'active',
|
||||||
|
`Capturing ${timeframeLabel} screenshots (${i + 1}/${timeframes.length})`
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
...baseConfig,
|
||||||
|
timeframe: timeframe,
|
||||||
|
sessionId: i === 0 ? sessionId : undefined // Only track progress for first timeframe
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture screenshots WITHOUT analysis
|
||||||
|
const screenshots = await enhancedScreenshotService.captureWithLogin(config)
|
||||||
|
|
||||||
|
if (screenshots && screenshots.length > 0) {
|
||||||
|
console.log(`✅ Captured ${screenshots.length} screenshots for ${timeframeLabel}`)
|
||||||
|
|
||||||
|
// Store screenshots with metadata
|
||||||
|
const screenshotData = {
|
||||||
|
timeframe: timeframe,
|
||||||
|
timeframeLabel: timeframeLabel,
|
||||||
|
screenshots: screenshots,
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
|
||||||
|
screenshotResults.push(screenshotData)
|
||||||
|
allScreenshots.push(...screenshots)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ No screenshots captured for ${timeframeLabel}`)
|
||||||
|
screenshotResults.push({
|
||||||
|
timeframe: timeframe,
|
||||||
|
timeframeLabel: timeframeLabel,
|
||||||
|
screenshots: [],
|
||||||
|
success: false,
|
||||||
|
error: 'No screenshots captured'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (timeframeError) {
|
||||||
|
console.error(`❌ Error capturing ${timeframeLabel}:`, timeframeError)
|
||||||
|
screenshotResults.push({
|
||||||
|
timeframe: timeframe,
|
||||||
|
timeframeLabel: timeframeLabel,
|
||||||
|
screenshots: [],
|
||||||
|
success: false,
|
||||||
|
error: timeframeError.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between captures
|
||||||
|
if (i < timeframes.length - 1) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📊 Batch screenshot collection completed: ${allScreenshots.length} total screenshots`)
|
||||||
|
progressTracker.updateStep(sessionId, 'capture', 'completed', `Captured ${allScreenshots.length} total screenshots`)
|
||||||
|
|
||||||
|
// STEP 2: Send ALL screenshots to AI for comprehensive analysis
|
||||||
|
let analysis = null
|
||||||
|
|
||||||
|
if (analyze && allScreenshots.length > 0) {
|
||||||
|
console.log(`🤖 Starting comprehensive AI analysis on ${allScreenshots.length} screenshots...`)
|
||||||
|
progressTracker.updateStep(sessionId, 'analysis', 'active', 'Running comprehensive AI analysis...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (allScreenshots.length === 1) {
|
||||||
|
analysis = await aiAnalysisService.analyzeScreenshot(allScreenshots[0])
|
||||||
|
} else {
|
||||||
|
analysis = await aiAnalysisService.analyzeMultipleScreenshots(allScreenshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (analysis) {
|
||||||
|
console.log('✅ Comprehensive AI analysis completed')
|
||||||
|
progressTracker.updateStep(sessionId, 'analysis', 'completed', 'AI analysis completed successfully!')
|
||||||
|
} else {
|
||||||
|
throw new Error('AI analysis returned null')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (analysisError) {
|
||||||
|
console.error('❌ AI analysis failed:', analysisError)
|
||||||
|
progressTracker.updateStep(sessionId, 'analysis', 'error', `AI analysis failed: ${analysisError.message}`)
|
||||||
|
|
||||||
|
// Don't fail the entire request - return screenshots without analysis
|
||||||
|
analysis = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 3: Format comprehensive results
|
||||||
|
const result = {
|
||||||
|
success: true,
|
||||||
|
type: 'batch_analysis',
|
||||||
|
sessionId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
symbol: symbol,
|
||||||
|
timeframes: timeframes,
|
||||||
|
layouts: baseConfig.layouts,
|
||||||
|
summary: `Batch analysis completed for ${timeframes.length} timeframes`,
|
||||||
|
totalScreenshots: allScreenshots.length,
|
||||||
|
screenshotResults: screenshotResults,
|
||||||
|
allScreenshots: allScreenshots.map(path => ({
|
||||||
|
url: `/screenshots/${path.split('/').pop()}`,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})),
|
||||||
|
analysis: analysis, // Comprehensive analysis of ALL screenshots
|
||||||
|
message: `Successfully captured ${allScreenshots.length} screenshots${analysis ? ' with comprehensive AI analysis' : ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up session
|
||||||
|
setTimeout(() => progressTracker.deleteSession(sessionId), 2000)
|
||||||
|
|
||||||
|
// Trigger post-analysis cleanup in development mode
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
try {
|
||||||
|
const { default: aggressiveCleanup } = await import('../../../lib/aggressive-cleanup')
|
||||||
|
// Run cleanup in background, don't block the response
|
||||||
|
aggressiveCleanup.runPostAnalysisCleanup().catch(console.error)
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('Error triggering post-batch-analysis cleanup:', cleanupError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(result)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Batch analysis failed:', error)
|
||||||
|
progressTracker.updateStep(sessionId, 'analysis', 'error', `Batch analysis failed: ${error.message}`)
|
||||||
|
setTimeout(() => progressTracker.deleteSession(sessionId), 5000)
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Batch analysis failed',
|
||||||
|
message: error.message,
|
||||||
|
sessionId: sessionId
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Batch analysis API error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Batch analysis failed',
|
||||||
|
message: error.message
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get timeframe label
|
||||||
|
function getTimeframeLabel(timeframe) {
|
||||||
|
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' },
|
||||||
|
{ label: '1M', value: 'M' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return timeframes.find(t => t.value === timeframe)?.label || timeframe
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Batch Analysis API - use POST method for multi-timeframe analysis',
|
||||||
|
endpoints: {
|
||||||
|
POST: '/api/batch-analysis - Run multi-timeframe analysis with parameters'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
34
app/api/cleanup/route.js
Normal file
34
app/api/cleanup/route.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// API endpoint to manually trigger cleanup
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
try {
|
||||||
|
console.log('🧹 Manual cleanup triggered via API...')
|
||||||
|
|
||||||
|
// Import and trigger cleanup
|
||||||
|
const { aggressiveCleanup } = await import('../../../lib/startup')
|
||||||
|
await aggressiveCleanup.cleanupOrphanedProcesses()
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Cleanup completed successfully'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in manual cleanup:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
// Return cleanup status
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Cleanup endpoint is active',
|
||||||
|
endpoints: {
|
||||||
|
'POST /api/cleanup': 'Trigger manual cleanup',
|
||||||
|
'GET /api/cleanup': 'Check cleanup status'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -113,6 +113,17 @@ export async function POST(request) {
|
|||||||
message: `Successfully captured ${screenshots.length} screenshot(s)${analysis ? ' with AI analysis' : ''}`
|
message: `Successfully captured ${screenshots.length} screenshot(s)${analysis ? ' with AI analysis' : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger post-analysis cleanup in development mode
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
try {
|
||||||
|
const { default: aggressiveCleanup } = await import('../../../lib/aggressive-cleanup')
|
||||||
|
// Run cleanup in background, don't block the response
|
||||||
|
aggressiveCleanup.runPostAnalysisCleanup().catch(console.error)
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('Error triggering post-analysis cleanup:', cleanupError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(result)
|
return NextResponse.json(result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Enhanced screenshot API error:', error)
|
console.error('Enhanced screenshot API error:', error)
|
||||||
|
|||||||
75
app/api/price/route.js
Normal file
75
app/api/price/route.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET(request) {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const symbol = searchParams.get('symbol') || 'BTCUSD'
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Map symbols to CoinGecko IDs
|
||||||
|
const symbolMap = {
|
||||||
|
'BTCUSD': 'bitcoin',
|
||||||
|
'ETHUSD': 'ethereum',
|
||||||
|
'SOLUSD': 'solana',
|
||||||
|
'SUIUSD': 'sui',
|
||||||
|
'ADAUSD': 'cardano',
|
||||||
|
'DOGEUSD': 'dogecoin',
|
||||||
|
'XRPUSD': 'ripple',
|
||||||
|
'AVAXUSD': 'avalanche-2',
|
||||||
|
'LINKUSD': 'chainlink',
|
||||||
|
'MATICUSD': 'matic-network'
|
||||||
|
}
|
||||||
|
|
||||||
|
const coinId = symbolMap[symbol.toUpperCase()] || 'bitcoin'
|
||||||
|
|
||||||
|
// Fetch from CoinGecko API
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`CoinGecko API error: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
const price = data[coinId]?.usd
|
||||||
|
|
||||||
|
if (!price) {
|
||||||
|
throw new Error('Price not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
symbol: symbol.toUpperCase(),
|
||||||
|
price: price,
|
||||||
|
source: 'coingecko'
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Price fetch error:', error)
|
||||||
|
|
||||||
|
// Return fallback prices for testing
|
||||||
|
const fallbackPrices = {
|
||||||
|
'BTCUSD': 100000,
|
||||||
|
'ETHUSD': 4000,
|
||||||
|
'SOLUSD': 200,
|
||||||
|
'SUIUSD': 4.5,
|
||||||
|
'ADAUSD': 1.2,
|
||||||
|
'DOGEUSD': 0.4,
|
||||||
|
'XRPUSD': 2.5,
|
||||||
|
'AVAXUSD': 45,
|
||||||
|
'LINKUSD': 20,
|
||||||
|
'MATICUSD': 1.1
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
symbol: symbol.toUpperCase(),
|
||||||
|
price: fallbackPrices[symbol.toUpperCase()] || 100,
|
||||||
|
source: 'fallback',
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import './globals.css'
|
import './globals.css'
|
||||||
import Navigation from '../components/Navigation.tsx'
|
import Navigation from '../components/Navigation.tsx'
|
||||||
|
|
||||||
|
// Initialize cleanup system
|
||||||
|
import '../lib/startup'
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Trading Bot Dashboard',
|
title: 'Trading Bot Dashboard',
|
||||||
description: 'AI-powered trading bot with automated analysis and execution',
|
description: 'AI-powered trading bot with automated analysis and execution',
|
||||||
|
|||||||
26
cleanup-chromium.sh
Executable file
26
cleanup-chromium.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Cleanup script to kill zombie Chromium processes
|
||||||
|
# This should be run periodically or when the application shuts down
|
||||||
|
|
||||||
|
echo "🧹 Cleaning up zombie Chromium processes..."
|
||||||
|
|
||||||
|
# Kill all defunct chromium processes
|
||||||
|
pkill -f "chromium.*defunct" 2>/dev/null
|
||||||
|
|
||||||
|
# Kill any remaining chromium processes in the container
|
||||||
|
if [ -n "$DOCKER_ENV" ]; then
|
||||||
|
echo "Running in Docker environment, cleaning up container processes..."
|
||||||
|
# In Docker, we need to be more careful about process cleanup
|
||||||
|
ps aux | grep '[c]hromium' | grep -v grep | awk '{print $2}' | xargs -r kill -TERM 2>/dev/null
|
||||||
|
sleep 2
|
||||||
|
ps aux | grep '[c]hromium' | grep -v grep | awk '{print $2}' | xargs -r kill -KILL 2>/dev/null
|
||||||
|
else
|
||||||
|
echo "Running in host environment, cleaning up host processes..."
|
||||||
|
pkill -f "chromium" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up any temporary puppeteer profiles
|
||||||
|
rm -rf /tmp/puppeteer_dev_chrome_profile-* 2>/dev/null
|
||||||
|
|
||||||
|
echo "✅ Cleanup completed"
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import TradeModal from './TradeModal'
|
import TradeModal from './TradeModal'
|
||||||
import ScreenshotGallery from './ScreenshotGallery'
|
import ScreenshotGallery from './ScreenshotGallery'
|
||||||
|
import PositionCalculator from './PositionCalculator'
|
||||||
|
import PriceFetcher from '../lib/price-fetcher'
|
||||||
|
|
||||||
const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim())
|
const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim())
|
||||||
const timeframes = [
|
const timeframes = [
|
||||||
@@ -65,6 +67,7 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
const [enlargedScreenshot, setEnlargedScreenshot] = useState<string | null>(null)
|
const [enlargedScreenshot, setEnlargedScreenshot] = useState<string | null>(null)
|
||||||
const [tradeModalOpen, setTradeModalOpen] = useState(false)
|
const [tradeModalOpen, setTradeModalOpen] = useState(false)
|
||||||
const [tradeModalData, setTradeModalData] = useState<any>(null)
|
const [tradeModalData, setTradeModalData] = useState<any>(null)
|
||||||
|
const [currentPrice, setCurrentPrice] = useState<number>(0)
|
||||||
|
|
||||||
// Helper function to safely render any value
|
// Helper function to safely render any value
|
||||||
const safeRender = (value: any): string => {
|
const safeRender = (value: any): string => {
|
||||||
@@ -118,6 +121,25 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
}
|
}
|
||||||
}, [eventSource])
|
}, [eventSource])
|
||||||
|
|
||||||
|
// Fetch current price when symbol changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fetchPrice = async () => {
|
||||||
|
try {
|
||||||
|
const price = await PriceFetcher.getCurrentPrice(symbol)
|
||||||
|
setCurrentPrice(price)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching price:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPrice()
|
||||||
|
|
||||||
|
// Set up periodic price updates every 30 seconds
|
||||||
|
const interval = setInterval(fetchPrice, 30000)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [symbol])
|
||||||
|
|
||||||
const toggleLayout = (layout: string) => {
|
const toggleLayout = (layout: string) => {
|
||||||
setSelectedLayouts(prev =>
|
setSelectedLayouts(prev =>
|
||||||
prev.includes(layout)
|
prev.includes(layout)
|
||||||
@@ -229,68 +251,57 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
onAnalysisComplete(data.analysis, analysisSymbol)
|
onAnalysisComplete(data.analysis, analysisSymbol)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Multiple timeframe analysis
|
// Multiple timeframe analysis - use batch analysis endpoint
|
||||||
const results = []
|
console.log(`🧪 Starting batch analysis for ${analysisTimeframes.length} timeframes`)
|
||||||
|
|
||||||
for (let i = 0; i < analysisTimeframes.length; i++) {
|
const response = await fetch('/api/batch-analysis', {
|
||||||
const tf = analysisTimeframes[i]
|
method: 'POST',
|
||||||
const timeframeLabel = timeframes.find(t => t.value === tf)?.label || tf
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
console.log(`🧪 Analyzing timeframe: ${timeframeLabel}`)
|
symbol: analysisSymbol,
|
||||||
|
timeframes: analysisTimeframes,
|
||||||
const response = await fetch('/api/enhanced-screenshot', {
|
layouts: selectedLayouts,
|
||||||
method: 'POST',
|
analyze: true
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
symbol: analysisSymbol,
|
|
||||||
timeframe: tf,
|
|
||||||
layouts: selectedLayouts,
|
|
||||||
analyze: true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
results.push({
|
|
||||||
timeframe: tf,
|
|
||||||
timeframeLabel,
|
|
||||||
success: response.ok,
|
|
||||||
result
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Start progress tracking for the first timeframe session
|
const data = await response.json()
|
||||||
if (i === 0 && result.sessionId) {
|
|
||||||
startProgressTracking(result.sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
const multiResult = {
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Batch analysis failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start real-time progress tracking if sessionId is provided
|
||||||
|
if (data.sessionId) {
|
||||||
|
startProgressTracking(data.sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert batch analysis result to compatible format
|
||||||
|
const batchResult = {
|
||||||
type: 'multi_timeframe',
|
type: 'multi_timeframe',
|
||||||
symbol: analysisSymbol,
|
symbol: analysisSymbol,
|
||||||
summary: `Analyzed ${results.length} timeframes for ${analysisSymbol}`,
|
summary: data.summary,
|
||||||
results
|
analysis: data.analysis, // Comprehensive analysis of ALL screenshots
|
||||||
|
totalScreenshots: data.totalScreenshots,
|
||||||
|
results: data.screenshotResults?.map((sr: any) => ({
|
||||||
|
timeframe: sr.timeframe,
|
||||||
|
timeframeLabel: sr.timeframeLabel,
|
||||||
|
success: sr.success,
|
||||||
|
result: {
|
||||||
|
screenshots: sr.screenshots?.map((path: string) => ({
|
||||||
|
url: `/screenshots/${path.split('/').pop()}`,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})) || [],
|
||||||
|
analysis: sr.timeframe === analysisTimeframes[0] ? data.analysis : null // Only show comprehensive analysis on first timeframe
|
||||||
|
}
|
||||||
|
})) || []
|
||||||
}
|
}
|
||||||
|
|
||||||
setResult(multiResult)
|
setResult(batchResult)
|
||||||
|
|
||||||
// Call the callback with the first successful analysis result if provided
|
// Call the callback with the comprehensive analysis result if provided
|
||||||
if (onAnalysisComplete) {
|
if (onAnalysisComplete && data.analysis) {
|
||||||
const firstSuccessfulResult = results.find(r => r.success && r.result?.analysis)
|
onAnalysisComplete(data.analysis, analysisSymbol)
|
||||||
if (firstSuccessfulResult) {
|
|
||||||
onAnalysisComplete(firstSuccessfulResult.result.analysis, analysisSymbol)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1375,6 +1386,39 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Position Calculator - Always Show */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="card card-gradient">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-xl font-bold text-white flex items-center">
|
||||||
|
<span className="w-8 h-8 bg-gradient-to-br from-green-400 to-emerald-600 rounded-lg flex items-center justify-center mr-3">
|
||||||
|
📊
|
||||||
|
</span>
|
||||||
|
Position Calculator
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center space-x-2 text-sm text-gray-400">
|
||||||
|
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||||
|
<span>Live Calculator</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PositionCalculator
|
||||||
|
analysis={result?.analysis || null}
|
||||||
|
currentPrice={
|
||||||
|
result?.analysis?.entry?.price ||
|
||||||
|
result?.analysis?.entry ||
|
||||||
|
(typeof result?.analysis?.entry === 'string' ? parseFloat(result?.analysis?.entry.replace(/[^0-9.-]+/g, '')) : 0) ||
|
||||||
|
currentPrice ||
|
||||||
|
0
|
||||||
|
}
|
||||||
|
symbol={symbol}
|
||||||
|
onPositionChange={(position) => {
|
||||||
|
console.log('Position calculation updated:', position)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{result && !result.analysis && result.screenshots && (
|
{result && !result.analysis && result.screenshots && (
|
||||||
<div className="mt-6 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
<div className="mt-6 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||||
<h3 className="text-lg font-bold text-yellow-400 mb-2">Screenshots Captured</h3>
|
<h3 className="text-lg font-bold text-yellow-400 mb-2">Screenshots Captured</h3>
|
||||||
|
|||||||
503
components/PositionCalculator.tsx
Normal file
503
components/PositionCalculator.tsx
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
// Dynamic Position Calculator Component
|
||||||
|
"use client"
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
interface PositionCalculatorProps {
|
||||||
|
analysis?: any // The AI analysis results
|
||||||
|
currentPrice?: number
|
||||||
|
symbol?: string
|
||||||
|
onPositionChange?: (position: PositionCalculation) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PositionCalculation {
|
||||||
|
investmentAmount: number
|
||||||
|
leverage: number
|
||||||
|
positionSize: number
|
||||||
|
entryPrice: number
|
||||||
|
stopLoss: number
|
||||||
|
takeProfit: number
|
||||||
|
liquidationPrice: number
|
||||||
|
maxLoss: number
|
||||||
|
maxProfit: number
|
||||||
|
riskRewardRatio: number
|
||||||
|
marginRequired: number
|
||||||
|
tradingFee: number
|
||||||
|
netInvestment: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PositionCalculator({
|
||||||
|
analysis,
|
||||||
|
currentPrice = 0,
|
||||||
|
symbol = 'BTCUSD',
|
||||||
|
onPositionChange
|
||||||
|
}: PositionCalculatorProps) {
|
||||||
|
const [investmentAmount, setInvestmentAmount] = useState<number>(100)
|
||||||
|
const [leverage, setLeverage] = useState<number>(10)
|
||||||
|
const [positionType, setPositionType] = useState<'long' | 'short'>('long')
|
||||||
|
const [calculation, setCalculation] = useState<PositionCalculation | null>(null)
|
||||||
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||||
|
const [marketPrice, setMarketPrice] = useState<number>(currentPrice)
|
||||||
|
|
||||||
|
// Trading parameters
|
||||||
|
const [tradingFee, setTradingFee] = useState<number>(0.1) // 0.1% fee
|
||||||
|
const [maintenanceMargin, setMaintenanceMargin] = useState<number>(0.5) // 0.5% maintenance margin
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
console.log('📊 PositionCalculator mounted with:', { analysis, currentPrice, symbol })
|
||||||
|
|
||||||
|
// Auto-detect position type from analysis
|
||||||
|
useEffect(() => {
|
||||||
|
if (analysis?.recommendation) {
|
||||||
|
const rec = analysis.recommendation.toLowerCase()
|
||||||
|
if (rec.includes('buy') || rec.includes('long') || rec.includes('bullish')) {
|
||||||
|
setPositionType('long')
|
||||||
|
} else if (rec.includes('sell') || rec.includes('short') || rec.includes('bearish')) {
|
||||||
|
setPositionType('short')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [analysis])
|
||||||
|
|
||||||
|
// Fetch current market price if not provided
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPrice = async () => {
|
||||||
|
console.log('🔍 Fetching price for:', symbol, 'currentPrice:', currentPrice)
|
||||||
|
|
||||||
|
if (currentPrice > 0) {
|
||||||
|
console.log('✅ Using provided currentPrice:', currentPrice)
|
||||||
|
setMarketPrice(currentPrice)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/price?symbol=${symbol}`)
|
||||||
|
const data = await response.json()
|
||||||
|
console.log('📊 Price API response:', data)
|
||||||
|
|
||||||
|
if (data.price) {
|
||||||
|
console.log('✅ Setting market price to:', data.price)
|
||||||
|
setMarketPrice(data.price)
|
||||||
|
} else {
|
||||||
|
console.error('❌ No price in API response')
|
||||||
|
setMarketPrice(symbol.includes('BTC') ? 100000 : symbol.includes('ETH') ? 4000 : 100)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to fetch price:', error)
|
||||||
|
// Fallback to a reasonable default for testing
|
||||||
|
const fallbackPrice = symbol.includes('BTC') ? 100000 : symbol.includes('ETH') ? 4000 : 100
|
||||||
|
console.log('🔄 Using fallback price:', fallbackPrice)
|
||||||
|
setMarketPrice(fallbackPrice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPrice()
|
||||||
|
}, [currentPrice, symbol])
|
||||||
|
|
||||||
|
// Calculate position metrics
|
||||||
|
const calculatePosition = () => {
|
||||||
|
let priceToUse = marketPrice || currentPrice
|
||||||
|
console.log('📊 Calculating position with:', { priceToUse, marketPrice, currentPrice, investmentAmount, leverage })
|
||||||
|
|
||||||
|
// Use fallback price if no price is available
|
||||||
|
if (!priceToUse || priceToUse <= 0) {
|
||||||
|
priceToUse = symbol.includes('BTC') ? 117000 : symbol.includes('ETH') ? 4000 : symbol.includes('SOL') ? 200 : 100
|
||||||
|
console.log('🔄 Using fallback price:', priceToUse)
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionSize = investmentAmount * leverage
|
||||||
|
const marginRequired = investmentAmount
|
||||||
|
const fee = positionSize * (tradingFee / 100)
|
||||||
|
const netInvestment = investmentAmount + fee
|
||||||
|
|
||||||
|
console.log('📈 Position calculation:', { positionSize, marginRequired, fee, netInvestment })
|
||||||
|
|
||||||
|
// Get AI analysis targets if available
|
||||||
|
let entryPrice = priceToUse
|
||||||
|
let stopLoss = 0
|
||||||
|
let takeProfit = 0
|
||||||
|
|
||||||
|
if (analysis && analysis.analysis) {
|
||||||
|
console.log('🤖 Using AI analysis:', analysis.analysis)
|
||||||
|
// Try to extract entry, stop loss, and take profit from AI analysis
|
||||||
|
const analysisText = analysis.analysis.toLowerCase()
|
||||||
|
|
||||||
|
// Look for entry price
|
||||||
|
const entryMatch = analysisText.match(/entry[:\s]*[\$]?(\d+\.?\d*)/i)
|
||||||
|
if (entryMatch) {
|
||||||
|
entryPrice = parseFloat(entryMatch[1])
|
||||||
|
console.log('📍 Found entry price:', entryPrice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for stop loss
|
||||||
|
const stopMatch = analysisText.match(/stop[:\s]*[\$]?(\d+\.?\d*)/i)
|
||||||
|
if (stopMatch) {
|
||||||
|
stopLoss = parseFloat(stopMatch[1])
|
||||||
|
console.log('🛑 Found stop loss:', stopLoss)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for take profit
|
||||||
|
const profitMatch = analysisText.match(/(?:take profit|target)[:\s]*[\$]?(\d+\.?\d*)/i)
|
||||||
|
if (profitMatch) {
|
||||||
|
takeProfit = parseFloat(profitMatch[1])
|
||||||
|
console.log('🎯 Found take profit:', takeProfit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no specific targets found, use percentage-based defaults
|
||||||
|
if (!stopLoss) {
|
||||||
|
stopLoss = positionType === 'long'
|
||||||
|
? entryPrice * 0.95 // 5% stop loss for long
|
||||||
|
: entryPrice * 1.05 // 5% stop loss for short
|
||||||
|
console.log('🔄 Using default stop loss:', stopLoss)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!takeProfit) {
|
||||||
|
takeProfit = positionType === 'long'
|
||||||
|
? entryPrice * 1.10 // 10% take profit for long
|
||||||
|
: entryPrice * 0.90 // 10% take profit for short
|
||||||
|
console.log('🔄 Using default take profit:', takeProfit)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('📊 No AI analysis, using defaults')
|
||||||
|
// Default targets if no analysis
|
||||||
|
stopLoss = positionType === 'long'
|
||||||
|
? priceToUse * 0.95
|
||||||
|
: priceToUse * 1.05
|
||||||
|
takeProfit = positionType === 'long'
|
||||||
|
? priceToUse * 1.10
|
||||||
|
: priceToUse * 0.90
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate liquidation price
|
||||||
|
const liquidationPrice = positionType === 'long'
|
||||||
|
? entryPrice * (1 - (1 / leverage) + (maintenanceMargin / 100))
|
||||||
|
: entryPrice * (1 + (1 / leverage) - (maintenanceMargin / 100))
|
||||||
|
|
||||||
|
// Calculate PnL
|
||||||
|
const stopLossChange = positionType === 'long'
|
||||||
|
? (stopLoss - entryPrice) / entryPrice
|
||||||
|
: (entryPrice - stopLoss) / entryPrice
|
||||||
|
const takeProfitChange = positionType === 'long'
|
||||||
|
? (takeProfit - entryPrice) / entryPrice
|
||||||
|
: (entryPrice - takeProfit) / entryPrice
|
||||||
|
|
||||||
|
const maxLoss = positionSize * Math.abs(stopLossChange)
|
||||||
|
const maxProfit = positionSize * Math.abs(takeProfitChange)
|
||||||
|
const riskRewardRatio = maxProfit / maxLoss
|
||||||
|
|
||||||
|
const result: PositionCalculation = {
|
||||||
|
investmentAmount,
|
||||||
|
leverage,
|
||||||
|
positionSize,
|
||||||
|
entryPrice,
|
||||||
|
stopLoss,
|
||||||
|
takeProfit,
|
||||||
|
liquidationPrice,
|
||||||
|
maxLoss,
|
||||||
|
maxProfit,
|
||||||
|
riskRewardRatio,
|
||||||
|
marginRequired,
|
||||||
|
tradingFee: fee,
|
||||||
|
netInvestment
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Position calculation result:', result)
|
||||||
|
setCalculation(result)
|
||||||
|
onPositionChange?.(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate when parameters change
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('🔄 Recalculating position...')
|
||||||
|
calculatePosition()
|
||||||
|
}, [investmentAmount, leverage, positionType, currentPrice, analysis, tradingFee, maintenanceMargin, marketPrice])
|
||||||
|
|
||||||
|
// Force initial calculation
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('🚀 Position Calculator initialized, forcing calculation...')
|
||||||
|
calculatePosition()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number, decimals: number = 2) => {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
minimumFractionDigits: decimals,
|
||||||
|
maximumFractionDigits: decimals
|
||||||
|
}).format(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatPrice = (price: number) => {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'decimal',
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
}).format(price)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-br from-gray-800/50 to-gray-900/50 border border-gray-700 rounded-lg p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-xl font-bold text-white flex items-center">
|
||||||
|
<span className="w-6 h-6 bg-gradient-to-br from-green-400 to-green-600 rounded-lg flex items-center justify-center mr-3 text-sm">
|
||||||
|
📊
|
||||||
|
</span>
|
||||||
|
Position Calculator
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||||
|
className="text-sm text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{showAdvanced ? '🔽 Hide Advanced' : '🔼 Show Advanced'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current Price Display */}
|
||||||
|
<div className="mb-6 p-4 bg-gray-800/30 rounded-lg border border-gray-600">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm text-gray-400">Current Market Price</div>
|
||||||
|
<div className="text-lg font-bold text-white">
|
||||||
|
{symbol}: ${formatPrice(marketPrice || currentPrice || (symbol.includes('BTC') ? 117000 : symbol.includes('ETH') ? 4000 : symbol.includes('SOL') ? 200 : 100))}
|
||||||
|
{(!marketPrice && !currentPrice) && (
|
||||||
|
<span className="text-xs text-yellow-400 ml-2">(fallback)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{analysis?.recommendation && (
|
||||||
|
<div className="mt-2 text-sm text-cyan-400">
|
||||||
|
AI Recommendation: {analysis.recommendation}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Controls */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
{/* Investment Amount */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Investment Amount ($)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={investmentAmount}
|
||||||
|
onChange={(e) => setInvestmentAmount(Number(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Position Type */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Position Type
|
||||||
|
{analysis?.recommendation && (
|
||||||
|
<span className="ml-2 text-xs text-cyan-400">(Auto-detected from analysis)</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setPositionType('long')}
|
||||||
|
className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||||
|
positionType === 'long'
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📈 Long
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPositionType('short')}
|
||||||
|
className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||||
|
positionType === 'short'
|
||||||
|
? 'bg-red-600 text-white'
|
||||||
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📉 Short
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leverage Slider */}
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Leverage: {leverage}x
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={leverage}
|
||||||
|
onChange={(e) => setLeverage(Number(e.target.value))}
|
||||||
|
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to right, #10B981 0%, #10B981 ${leverage}%, #374151 ${leverage}%, #374151 100%)`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||||||
|
<span>1x</span>
|
||||||
|
<span>25x</span>
|
||||||
|
<span>50x</span>
|
||||||
|
<span>100x</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced Settings */}
|
||||||
|
{showAdvanced && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6 p-4 bg-gray-800/30 rounded-lg border border-gray-600">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Trading Fee (%)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={tradingFee}
|
||||||
|
onChange={(e) => setTradingFee(Number(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Maintenance Margin (%)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={maintenanceMargin}
|
||||||
|
onChange={(e) => setMaintenanceMargin(Number(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
min="0.1"
|
||||||
|
max="5"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Calculation Results */}
|
||||||
|
{calculation && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Position Summary */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div className="text-sm text-gray-400 mb-1">Position Size</div>
|
||||||
|
<div className="text-lg font-bold text-white">
|
||||||
|
{formatCurrency(calculation.positionSize)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div className="text-sm text-gray-400 mb-1">Entry Price</div>
|
||||||
|
<div className="text-lg font-bold text-white">
|
||||||
|
${formatPrice(calculation.entryPrice)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div className="text-sm text-gray-400 mb-1">Margin Required</div>
|
||||||
|
<div className="text-lg font-bold text-white">
|
||||||
|
{formatCurrency(calculation.marginRequired)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Risk Metrics */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-red-900/20 border border-red-700 rounded-lg p-4">
|
||||||
|
<div className="text-sm text-red-400 mb-2">🚨 Risk Metrics</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Stop Loss:</span>
|
||||||
|
<span className="text-red-400 font-bold">${formatPrice(calculation.stopLoss)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Max Loss:</span>
|
||||||
|
<span className="text-red-400 font-bold">{formatCurrency(calculation.maxLoss)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Liquidation:</span>
|
||||||
|
<span className="text-red-500 font-bold">${formatPrice(calculation.liquidationPrice)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-green-900/20 border border-green-700 rounded-lg p-4">
|
||||||
|
<div className="text-sm text-green-400 mb-2">💰 Profit Potential</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Take Profit:</span>
|
||||||
|
<span className="text-green-400 font-bold">${formatPrice(calculation.takeProfit)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Max Profit:</span>
|
||||||
|
<span className="text-green-400 font-bold">{formatCurrency(calculation.maxProfit)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Risk/Reward:</span>
|
||||||
|
<span className="text-green-400 font-bold">1:{calculation.riskRewardRatio.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fee Breakdown */}
|
||||||
|
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div className="text-sm text-gray-400 mb-2">💸 Fee Breakdown</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Trading Fee:</span>
|
||||||
|
<span className="text-yellow-400">{formatCurrency(calculation.tradingFee)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Net Investment:</span>
|
||||||
|
<span className="text-white font-bold">{formatCurrency(calculation.netInvestment)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">Leverage:</span>
|
||||||
|
<span className="text-blue-400 font-bold">{leverage}x</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Risk Warning */}
|
||||||
|
{leverage > 50 && (
|
||||||
|
<div className="bg-red-900/30 border border-red-600 rounded-lg p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-red-400 text-lg">⚠️</span>
|
||||||
|
<span className="text-red-400 font-bold">High Leverage Warning</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-red-300 text-sm mt-2">
|
||||||
|
Using {leverage}x leverage is extremely risky. A small price movement against your position could result in liquidation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #10B981;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #065F46;
|
||||||
|
box-shadow: 0 0 0 1px #065F46;
|
||||||
|
}
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #10B981;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #065F46;
|
||||||
|
box-shadow: 0 0 0 1px #065F46;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -44,23 +44,50 @@ export default function ScreenshotGallery({
|
|||||||
if (tf.includes('2h') || tf === '120') return 120
|
if (tf.includes('2h') || tf === '120') return 120
|
||||||
if (tf.includes('4h') || tf === '240') return 240
|
if (tf.includes('4h') || tf === '240') return 240
|
||||||
if (tf.includes('1d') || tf === 'D') return 1440
|
if (tf.includes('1d') || tf === 'D') return 1440
|
||||||
|
if (tf.includes('1w') || tf === 'W') return 10080
|
||||||
|
if (tf.includes('1M') || tf === 'M') return 43200
|
||||||
// Default fallback
|
// Default fallback
|
||||||
return parseInt(tf) || 999
|
return parseInt(tf) || 999
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract timeframe from filename
|
// Extract timeframe from filename
|
||||||
const extractTimeframeFromFilename = (filename: string) => {
|
const extractTimeframeFromFilename = (filename: string) => {
|
||||||
const match = filename.match(/_(\d+|D)_/)
|
// First try to match the pattern _timeframe_
|
||||||
if (!match) return 'Unknown'
|
const match = filename.match(/_(\d+|D|W|M)_/)
|
||||||
const tf = match[1]
|
if (match) {
|
||||||
if (tf === 'D') return '1D'
|
const tf = match[1]
|
||||||
if (tf === '5') return '5m'
|
if (tf === 'D') return '1d'
|
||||||
if (tf === '15') return '15m'
|
if (tf === 'W') return '1w'
|
||||||
if (tf === '30') return '30m'
|
if (tf === 'M') return '1M'
|
||||||
if (tf === '60') return '1h'
|
if (tf === '5') return '5m'
|
||||||
if (tf === '120') return '2h'
|
if (tf === '15') return '15m'
|
||||||
if (tf === '240') return '4h'
|
if (tf === '30') return '30m'
|
||||||
return `${tf}m`
|
if (tf === '60') return '1h'
|
||||||
|
if (tf === '120') return '2h'
|
||||||
|
if (tf === '240') return '4h'
|
||||||
|
return `${tf}m`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to match timeframe patterns anywhere in the filename
|
||||||
|
const timeframePatterns = [
|
||||||
|
{ pattern: /5m|_5_/i, value: '5m' },
|
||||||
|
{ pattern: /15m|_15_/i, value: '15m' },
|
||||||
|
{ pattern: /30m|_30_/i, value: '30m' },
|
||||||
|
{ pattern: /1h|60m|_60_/i, value: '1h' },
|
||||||
|
{ pattern: /2h|120m|_120_/i, value: '2h' },
|
||||||
|
{ pattern: /4h|240m|_240_/i, value: '4h' },
|
||||||
|
{ pattern: /1d|daily|_D_/i, value: '1d' },
|
||||||
|
{ pattern: /1w|weekly|_W_/i, value: '1w' },
|
||||||
|
{ pattern: /1M|monthly|_M_/i, value: '1M' }
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const { pattern, value } of timeframePatterns) {
|
||||||
|
if (pattern.test(filename)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to detect layout from filename
|
// Helper function to detect layout from filename
|
||||||
@@ -76,7 +103,13 @@ export default function ScreenshotGallery({
|
|||||||
? screenshot
|
? screenshot
|
||||||
: (screenshot as any)?.url || String(screenshot)
|
: (screenshot as any)?.url || String(screenshot)
|
||||||
const filename = screenshotUrl.split('/').pop() || ''
|
const filename = screenshotUrl.split('/').pop() || ''
|
||||||
const timeframe = timeframes[index] || extractTimeframeFromFilename(filename)
|
|
||||||
|
// Extract timeframe from filename first, then use timeframes array as fallback
|
||||||
|
const extractedTimeframe = extractTimeframeFromFilename(filename)
|
||||||
|
const timeframe = extractedTimeframe !== 'Unknown'
|
||||||
|
? extractedTimeframe
|
||||||
|
: (timeframes[index] || extractedTimeframe)
|
||||||
|
|
||||||
const layout = detectLayout(filename)
|
const layout = detectLayout(filename)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
34
docker-entrypoint.sh
Executable file
34
docker-entrypoint.sh
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Trading Bot Startup Script with Process Cleanup
|
||||||
|
# This script initializes the process cleanup handlers and starts the Next.js app
|
||||||
|
|
||||||
|
echo "🚀 Starting Trading Bot with Process Cleanup..."
|
||||||
|
|
||||||
|
# Initialize process cleanup
|
||||||
|
echo "🧹 Initializing process cleanup handlers..."
|
||||||
|
|
||||||
|
# Create a signal handler to cleanup on container stop
|
||||||
|
cleanup() {
|
||||||
|
echo "🛑 Received shutdown signal, cleaning up..."
|
||||||
|
|
||||||
|
# Kill any remaining chromium processes
|
||||||
|
pkill -f "chromium" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Clean up temporary files
|
||||||
|
rm -rf /tmp/puppeteer_dev_chrome_profile-* 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "✅ Cleanup completed"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register signal handlers
|
||||||
|
trap cleanup SIGINT SIGTERM SIGQUIT
|
||||||
|
|
||||||
|
# Start the Next.js application
|
||||||
|
echo "🚀 Starting Next.js application..."
|
||||||
|
if [ "$NODE_ENV" = "development" ]; then
|
||||||
|
exec npm run dev:docker
|
||||||
|
else
|
||||||
|
exec npm start
|
||||||
|
fi
|
||||||
197
lib/aggressive-cleanup.ts
Normal file
197
lib/aggressive-cleanup.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// Aggressive process cleanup utility
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
class AggressiveCleanup {
|
||||||
|
private static instance: AggressiveCleanup
|
||||||
|
private cleanupInterval: NodeJS.Timeout | null = null
|
||||||
|
private isRunning = false
|
||||||
|
private isInitialized = false
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
// Don't auto-start - let startup.ts control it
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): AggressiveCleanup {
|
||||||
|
if (!AggressiveCleanup.instance) {
|
||||||
|
AggressiveCleanup.instance = new AggressiveCleanup()
|
||||||
|
}
|
||||||
|
return AggressiveCleanup.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
startPeriodicCleanup() {
|
||||||
|
if (this.isInitialized) {
|
||||||
|
console.log('🔄 Aggressive cleanup already initialized')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isInitialized = true
|
||||||
|
console.log('🚀 Starting aggressive cleanup system')
|
||||||
|
|
||||||
|
// In development, use on-demand cleanup instead of periodic
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('🔧 Development mode: Using on-demand cleanup (triggered after analysis)')
|
||||||
|
console.log('✅ On-demand cleanup system ready')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production: Clean up every 10 minutes (longer intervals)
|
||||||
|
this.cleanupInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await this.cleanupOrphanedProcesses()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in periodic cleanup:', error)
|
||||||
|
}
|
||||||
|
}, 10 * 60 * 1000) // 10 minutes
|
||||||
|
|
||||||
|
// Also run initial cleanup after 60 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.cleanupOrphanedProcesses().catch(console.error)
|
||||||
|
}, 60000)
|
||||||
|
|
||||||
|
console.log('✅ Periodic cleanup system started (10 min intervals)')
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanupOrphanedProcesses(): Promise<void> {
|
||||||
|
if (this.isRunning) return
|
||||||
|
|
||||||
|
this.isRunning = true
|
||||||
|
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||||
|
const cleanupType = isDevelopment ? 'gentle' : 'aggressive'
|
||||||
|
|
||||||
|
console.log(`🧹 Running ${cleanupType} cleanup for orphaned processes...`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check for active analysis sessions
|
||||||
|
try {
|
||||||
|
const { progressTracker } = await import('./progress-tracker')
|
||||||
|
const activeSessions = progressTracker.getActiveSessions()
|
||||||
|
|
||||||
|
if (activeSessions.length > 0) {
|
||||||
|
console.log(`⚠️ Skipping cleanup - ${activeSessions.length} active analysis sessions: ${activeSessions.join(', ')}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ No active analysis sessions, proceeding with cleanup')
|
||||||
|
} catch (importError) {
|
||||||
|
console.error('❌ Error importing progress tracker:', importError)
|
||||||
|
console.log('⚠️ Skipping cleanup due to import error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and kill orphaned chromium processes
|
||||||
|
const chromiumProcesses = await this.findChromiumProcesses()
|
||||||
|
|
||||||
|
if (chromiumProcesses.length > 0) {
|
||||||
|
console.log(`Found ${chromiumProcesses.length} chromium processes, cleaning up...`)
|
||||||
|
|
||||||
|
for (const pid of chromiumProcesses) {
|
||||||
|
try {
|
||||||
|
if (isDevelopment) {
|
||||||
|
// In development, use gentler SIGTERM first
|
||||||
|
console.log(`🔧 Dev mode: Gentle shutdown of process ${pid}`)
|
||||||
|
await execAsync(`kill -TERM ${pid}`)
|
||||||
|
// Give process 3 seconds to shut down gracefully
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
|
|
||||||
|
// Check if process is still running
|
||||||
|
try {
|
||||||
|
await execAsync(`kill -0 ${pid}`)
|
||||||
|
// Process still running, force kill
|
||||||
|
console.log(`⚠️ Process ${pid} didn't shut down gracefully, force killing`)
|
||||||
|
await execAsync(`kill -9 ${pid}`)
|
||||||
|
} catch {
|
||||||
|
// Process already dead, that's good
|
||||||
|
console.log(`✅ Process ${pid} shut down gracefully`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Production: immediate force kill
|
||||||
|
await execAsync(`kill -9 ${pid}`)
|
||||||
|
console.log(`✅ Killed process ${pid}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Process might already be dead
|
||||||
|
console.log(`ℹ️ Process ${pid} may already be terminated`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('✅ No orphaned chromium processes found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temp directories
|
||||||
|
try {
|
||||||
|
await execAsync('rm -rf /tmp/puppeteer_dev_chrome_profile-* 2>/dev/null || true')
|
||||||
|
console.log('✅ Cleaned up temp directories')
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up shared memory
|
||||||
|
try {
|
||||||
|
await execAsync('rm -rf /dev/shm/.org.chromium.* 2>/dev/null || true')
|
||||||
|
console.log('✅ Cleaned up shared memory')
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in ${cleanupType} cleanup:`, error)
|
||||||
|
} finally {
|
||||||
|
this.isRunning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findChromiumProcesses(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('ps aux | grep -E "(chromium|chrome)" | grep -v grep | awk \'{print $2}\'')
|
||||||
|
return stdout.trim().split('\n').filter((pid: string) => pid && pid !== '')
|
||||||
|
} catch (error) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async forceCleanup(): Promise<void> {
|
||||||
|
console.log('🚨 Force cleanup initiated...')
|
||||||
|
|
||||||
|
// Stop periodic cleanup
|
||||||
|
if (this.cleanupInterval) {
|
||||||
|
clearInterval(this.cleanupInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run aggressive cleanup
|
||||||
|
await this.cleanupOrphanedProcesses()
|
||||||
|
|
||||||
|
// Kill all chromium processes
|
||||||
|
try {
|
||||||
|
await execAsync('pkill -9 -f "chromium" 2>/dev/null || true')
|
||||||
|
await execAsync('pkill -9 -f "chrome" 2>/dev/null || true')
|
||||||
|
console.log('✅ Force killed all browser processes')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in force cleanup:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method for on-demand cleanup after analysis
|
||||||
|
async runPostAnalysisCleanup(): Promise<void> {
|
||||||
|
console.log('🧹 Post-analysis cleanup triggered...')
|
||||||
|
|
||||||
|
// Small delay to ensure analysis processes are fully closed
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
await this.cleanupOrphanedProcesses()
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
if (this.cleanupInterval) {
|
||||||
|
clearInterval(this.cleanupInterval)
|
||||||
|
this.cleanupInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the aggressive cleanup
|
||||||
|
const aggressiveCleanup = AggressiveCleanup.getInstance()
|
||||||
|
|
||||||
|
export default aggressiveCleanup
|
||||||
@@ -718,6 +718,19 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
|
|||||||
setTimeout(() => progressTracker.deleteSession(sessionId), 1000)
|
setTimeout(() => progressTracker.deleteSession(sessionId), 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger post-analysis cleanup in development mode
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
try {
|
||||||
|
// Dynamic import to avoid circular dependencies
|
||||||
|
const aggressiveCleanupModule = await import('./aggressive-cleanup')
|
||||||
|
const aggressiveCleanup = aggressiveCleanupModule.default
|
||||||
|
// Run cleanup in background, don't block the response
|
||||||
|
aggressiveCleanup.runPostAnalysisCleanup().catch(console.error)
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('Error triggering post-analysis cleanup:', cleanupError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
screenshots,
|
screenshots,
|
||||||
analysis
|
analysis
|
||||||
|
|||||||
152
lib/price-fetcher.ts
Normal file
152
lib/price-fetcher.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// Price fetcher utility for getting current market prices
|
||||||
|
export class PriceFetcher {
|
||||||
|
private static cache = new Map<string, { price: number; timestamp: number }>()
|
||||||
|
private static readonly CACHE_DURATION = 30000 // 30 seconds
|
||||||
|
|
||||||
|
static async getCurrentPrice(symbol: string): Promise<number> {
|
||||||
|
const cacheKey = symbol.toUpperCase()
|
||||||
|
const cached = this.cache.get(cacheKey)
|
||||||
|
|
||||||
|
// Return cached price if recent
|
||||||
|
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
|
||||||
|
return cached.price
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try multiple price sources
|
||||||
|
let price = await this.fetchFromCoinGecko(symbol)
|
||||||
|
|
||||||
|
if (!price) {
|
||||||
|
price = await this.fetchFromCoinCap(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!price) {
|
||||||
|
price = await this.fetchFromBinance(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (price) {
|
||||||
|
this.cache.set(cacheKey, { price, timestamp: Date.now() })
|
||||||
|
return price
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching price:', error)
|
||||||
|
return cached?.price || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fetchFromCoinGecko(symbol: string): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const coinId = this.getCoinGeckoId(symbol)
|
||||||
|
if (!coinId) return null
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd`,
|
||||||
|
{ cache: 'no-cache' }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) return null
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return data[coinId]?.usd || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('CoinGecko fetch error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fetchFromCoinCap(symbol: string): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const asset = this.getCoinCapAsset(symbol)
|
||||||
|
if (!asset) return null
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.coincap.io/v2/assets/${asset}`,
|
||||||
|
{ cache: 'no-cache' }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) return null
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return parseFloat(data.data?.priceUsd) || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('CoinCap fetch error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fetchFromBinance(symbol: string): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const binanceSymbol = this.getBinanceSymbol(symbol)
|
||||||
|
if (!binanceSymbol) return null
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.binance.com/api/v3/ticker/price?symbol=${binanceSymbol}`,
|
||||||
|
{ cache: 'no-cache' }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) return null
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return parseFloat(data.price) || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Binance fetch error:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getCoinGeckoId(symbol: string): string | null {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
'BTCUSD': 'bitcoin',
|
||||||
|
'ETHUSD': 'ethereum',
|
||||||
|
'SOLUSD': 'solana',
|
||||||
|
'SUIUSD': 'sui',
|
||||||
|
'ADAUSD': 'cardano',
|
||||||
|
'DOTUSD': 'polkadot',
|
||||||
|
'AVAXUSD': 'avalanche-2',
|
||||||
|
'LINKUSD': 'chainlink',
|
||||||
|
'MATICUSD': 'matic-network',
|
||||||
|
'UNIUSD': 'uniswap'
|
||||||
|
}
|
||||||
|
return mapping[symbol.toUpperCase()] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getCoinCapAsset(symbol: string): string | null {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
'BTCUSD': 'bitcoin',
|
||||||
|
'ETHUSD': 'ethereum',
|
||||||
|
'SOLUSD': 'solana',
|
||||||
|
'SUIUSD': 'sui',
|
||||||
|
'ADAUSD': 'cardano',
|
||||||
|
'DOTUSD': 'polkadot',
|
||||||
|
'AVAXUSD': 'avalanche',
|
||||||
|
'LINKUSD': 'chainlink',
|
||||||
|
'MATICUSD': 'polygon',
|
||||||
|
'UNIUSD': 'uniswap'
|
||||||
|
}
|
||||||
|
return mapping[symbol.toUpperCase()] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getBinanceSymbol(symbol: string): string | null {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
'BTCUSD': 'BTCUSDT',
|
||||||
|
'ETHUSD': 'ETHUSDT',
|
||||||
|
'SOLUSD': 'SOLUSDT',
|
||||||
|
'SUIUSD': 'SUIUSDT',
|
||||||
|
'ADAUSD': 'ADAUSDT',
|
||||||
|
'DOTUSD': 'DOTUSDT',
|
||||||
|
'AVAXUSD': 'AVAXUSDT',
|
||||||
|
'LINKUSD': 'LINKUSDT',
|
||||||
|
'MATICUSD': 'MATICUSDT',
|
||||||
|
'UNIUSD': 'UNIUSDT'
|
||||||
|
}
|
||||||
|
return mapping[symbol.toUpperCase()] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
static clearCache(): void {
|
||||||
|
this.cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PriceFetcher
|
||||||
96
lib/process-cleanup.ts
Normal file
96
lib/process-cleanup.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Process cleanup utility for graceful shutdown
|
||||||
|
import { enhancedScreenshotService } from './enhanced-screenshot'
|
||||||
|
import { tradingViewAutomation } from './tradingview-automation'
|
||||||
|
|
||||||
|
class ProcessCleanup {
|
||||||
|
private static instance: ProcessCleanup
|
||||||
|
private isShuttingDown = false
|
||||||
|
private aggressiveCleanup: any = null
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
// Register cleanup handlers
|
||||||
|
process.on('SIGINT', this.handleShutdown.bind(this))
|
||||||
|
process.on('SIGTERM', this.handleShutdown.bind(this))
|
||||||
|
process.on('SIGQUIT', this.handleShutdown.bind(this))
|
||||||
|
process.on('uncaughtException', this.handleError.bind(this))
|
||||||
|
process.on('unhandledRejection', this.handleError.bind(this))
|
||||||
|
|
||||||
|
// Lazy load aggressive cleanup to avoid circular imports
|
||||||
|
this.loadAggressiveCleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadAggressiveCleanup() {
|
||||||
|
try {
|
||||||
|
const { default: aggressiveCleanup } = await import('./aggressive-cleanup')
|
||||||
|
this.aggressiveCleanup = aggressiveCleanup
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load aggressive cleanup:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): ProcessCleanup {
|
||||||
|
if (!ProcessCleanup.instance) {
|
||||||
|
ProcessCleanup.instance = new ProcessCleanup()
|
||||||
|
}
|
||||||
|
return ProcessCleanup.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleShutdown(signal: string): Promise<void> {
|
||||||
|
if (this.isShuttingDown) {
|
||||||
|
console.log('Already shutting down, forcing exit...')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isShuttingDown = true
|
||||||
|
console.log(`\n🛑 Received ${signal}, initiating graceful shutdown...`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use aggressive cleanup first
|
||||||
|
if (this.aggressiveCleanup) {
|
||||||
|
console.log('🧹 Running aggressive cleanup...')
|
||||||
|
await this.aggressiveCleanup.forceCleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up screenshot service
|
||||||
|
console.log('🧹 Cleaning up screenshot service...')
|
||||||
|
await enhancedScreenshotService.cleanup()
|
||||||
|
|
||||||
|
// Clean up trading view automation
|
||||||
|
console.log('🧹 Cleaning up TradingView automation...')
|
||||||
|
await tradingViewAutomation.forceCleanup()
|
||||||
|
|
||||||
|
console.log('✅ Graceful shutdown completed')
|
||||||
|
process.exit(0)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during shutdown:', error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleError(error: Error): Promise<void> {
|
||||||
|
console.error('❌ Unhandled error:', error)
|
||||||
|
|
||||||
|
// Attempt cleanup
|
||||||
|
try {
|
||||||
|
if (this.aggressiveCleanup) {
|
||||||
|
await this.aggressiveCleanup.forceCleanup()
|
||||||
|
}
|
||||||
|
await enhancedScreenshotService.cleanup()
|
||||||
|
await tradingViewAutomation.forceCleanup()
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('❌ Error during error cleanup:', cleanupError)
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual cleanup method
|
||||||
|
async cleanup(): Promise<void> {
|
||||||
|
await this.handleShutdown('MANUAL')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the cleanup handler
|
||||||
|
const processCleanup = ProcessCleanup.getInstance()
|
||||||
|
|
||||||
|
export default processCleanup
|
||||||
18
lib/startup.ts
Normal file
18
lib/startup.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Startup initialization for the trading bot
|
||||||
|
// This file initializes critical systems and cleanup handlers
|
||||||
|
|
||||||
|
import processCleanup from './process-cleanup'
|
||||||
|
import aggressiveCleanup from './aggressive-cleanup'
|
||||||
|
|
||||||
|
// Initialize cleanup system
|
||||||
|
console.log('🚀 Initializing trading bot systems...')
|
||||||
|
console.log('🧹 Process cleanup handlers initialized')
|
||||||
|
|
||||||
|
// Start aggressive cleanup system (singleton pattern ensures only one instance)
|
||||||
|
aggressiveCleanup.startPeriodicCleanup()
|
||||||
|
|
||||||
|
// Export cleanup for manual access
|
||||||
|
export { processCleanup, aggressiveCleanup }
|
||||||
|
|
||||||
|
// Initialize on import
|
||||||
|
export default true
|
||||||
@@ -124,7 +124,19 @@ export class TradingViewAutomation {
|
|||||||
'--disable-default-apps',
|
'--disable-default-apps',
|
||||||
'--disable-sync',
|
'--disable-sync',
|
||||||
'--window-size=1920,1080',
|
'--window-size=1920,1080',
|
||||||
'--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
'--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||||
|
// Additional flags to prevent zombie processes
|
||||||
|
'--disable-background-networking',
|
||||||
|
'--disable-background-timer-throttling',
|
||||||
|
'--disable-backgrounding-occluded-windows',
|
||||||
|
'--disable-breakpad',
|
||||||
|
'--disable-client-side-phishing-detection',
|
||||||
|
'--disable-component-extensions-with-background-pages',
|
||||||
|
'--disable-crash-reporter',
|
||||||
|
'--disable-hang-monitor',
|
||||||
|
'--disable-prompt-on-repost',
|
||||||
|
'--no-crash-upload'
|
||||||
|
// Removed --single-process as it causes issues with Puppeteer
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -148,7 +160,24 @@ export class TradingViewAutomation {
|
|||||||
console.log('🧹 Force cleanup: Closing browser and resetting state...')
|
console.log('🧹 Force cleanup: Closing browser and resetting state...')
|
||||||
try {
|
try {
|
||||||
if (this.browser) {
|
if (this.browser) {
|
||||||
|
// First, close all pages
|
||||||
|
const pages = await this.browser.pages()
|
||||||
|
for (const page of pages) {
|
||||||
|
try {
|
||||||
|
await page.close()
|
||||||
|
} catch (e) {
|
||||||
|
console.log('WARNING: Error closing page:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then close the browser
|
||||||
await this.browser.close()
|
await this.browser.close()
|
||||||
|
|
||||||
|
// Force kill any remaining processes
|
||||||
|
const browserProcess = this.browser.process()
|
||||||
|
if (browserProcess && !browserProcess.killed) {
|
||||||
|
browserProcess.kill('SIGKILL')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('WARNING: Error during browser cleanup:', e)
|
console.log('WARNING: Error during browser cleanup:', e)
|
||||||
@@ -157,6 +186,7 @@ export class TradingViewAutomation {
|
|||||||
this.browser = null
|
this.browser = null
|
||||||
this.page = null
|
this.page = null
|
||||||
this.isAuthenticated = false
|
this.isAuthenticated = false
|
||||||
|
this.operationLock = false
|
||||||
console.log('✅ Cleanup completed')
|
console.log('✅ Cleanup completed')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
export const metadata = {
|
|
||||||
title: 'Next.js',
|
|
||||||
description: 'Generated by Next.js',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>{children}</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user