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 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
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
ENV DOCKER_ENV=true
|
||||
|
||||
# Start the app (default to development mode)
|
||||
CMD ["npm", "run", "dev:docker"]
|
||||
# Start the app with cleanup handlers
|
||||
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' : ''}`
|
||||
}
|
||||
|
||||
// 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)
|
||||
} catch (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 Navigation from '../components/Navigation.tsx'
|
||||
|
||||
// Initialize cleanup system
|
||||
import '../lib/startup'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Trading Bot Dashboard',
|
||||
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 TradeModal from './TradeModal'
|
||||
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 timeframes = [
|
||||
@@ -65,6 +67,7 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
const [enlargedScreenshot, setEnlargedScreenshot] = useState<string | null>(null)
|
||||
const [tradeModalOpen, setTradeModalOpen] = useState(false)
|
||||
const [tradeModalData, setTradeModalData] = useState<any>(null)
|
||||
const [currentPrice, setCurrentPrice] = useState<number>(0)
|
||||
|
||||
// Helper function to safely render any value
|
||||
const safeRender = (value: any): string => {
|
||||
@@ -118,6 +121,25 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
}
|
||||
}, [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) => {
|
||||
setSelectedLayouts(prev =>
|
||||
prev.includes(layout)
|
||||
@@ -229,68 +251,57 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
onAnalysisComplete(data.analysis, analysisSymbol)
|
||||
}
|
||||
} else {
|
||||
// Multiple timeframe analysis
|
||||
const results = []
|
||||
// Multiple timeframe analysis - use batch analysis endpoint
|
||||
console.log(`🧪 Starting batch analysis for ${analysisTimeframes.length} timeframes`)
|
||||
|
||||
for (let i = 0; i < analysisTimeframes.length; i++) {
|
||||
const tf = analysisTimeframes[i]
|
||||
const timeframeLabel = timeframes.find(t => t.value === tf)?.label || tf
|
||||
|
||||
console.log(`🧪 Analyzing timeframe: ${timeframeLabel}`)
|
||||
|
||||
const response = await fetch('/api/enhanced-screenshot', {
|
||||
method: 'POST',
|
||||
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
|
||||
const response = await fetch('/api/batch-analysis', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
symbol: analysisSymbol,
|
||||
timeframes: analysisTimeframes,
|
||||
layouts: selectedLayouts,
|
||||
analyze: true
|
||||
})
|
||||
})
|
||||
|
||||
// Start progress tracking for the first timeframe session
|
||||
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 data = await response.json()
|
||||
|
||||
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',
|
||||
symbol: analysisSymbol,
|
||||
summary: `Analyzed ${results.length} timeframes for ${analysisSymbol}`,
|
||||
results
|
||||
summary: data.summary,
|
||||
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
|
||||
if (onAnalysisComplete) {
|
||||
const firstSuccessfulResult = results.find(r => r.success && r.result?.analysis)
|
||||
if (firstSuccessfulResult) {
|
||||
onAnalysisComplete(firstSuccessfulResult.result.analysis, analysisSymbol)
|
||||
}
|
||||
// Call the callback with the comprehensive analysis result if provided
|
||||
if (onAnalysisComplete && data.analysis) {
|
||||
onAnalysisComplete(data.analysis, analysisSymbol)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1375,6 +1386,39 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP
|
||||
</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 && (
|
||||
<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>
|
||||
|
||||
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('4h') || tf === '240') return 240
|
||||
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
|
||||
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`
|
||||
// First try to match the pattern _timeframe_
|
||||
const match = filename.match(/_(\d+|D|W|M)_/)
|
||||
if (match) {
|
||||
const tf = match[1]
|
||||
if (tf === 'D') return '1d'
|
||||
if (tf === 'W') return '1w'
|
||||
if (tf === 'M') return '1M'
|
||||
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`
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -76,7 +103,13 @@ export default function ScreenshotGallery({
|
||||
? screenshot
|
||||
: (screenshot as any)?.url || String(screenshot)
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
screenshots,
|
||||
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-sync',
|
||||
'--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...')
|
||||
try {
|
||||
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()
|
||||
|
||||
// Force kill any remaining processes
|
||||
const browserProcess = this.browser.process()
|
||||
if (browserProcess && !browserProcess.killed) {
|
||||
browserProcess.kill('SIGKILL')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('WARNING: Error during browser cleanup:', e)
|
||||
@@ -157,6 +186,7 @@ export class TradingViewAutomation {
|
||||
this.browser = null
|
||||
this.page = null
|
||||
this.isAuthenticated = false
|
||||
this.operationLock = false
|
||||
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