Compare commits

...

8 Commits

Author SHA1 Message Date
mindesbunister
74b0087f17 feat: implement on-demand cleanup triggered after analysis completion
- Replace time-based cleanup with on-demand cleanup in development mode
- Cleanup is triggered immediately after AI analysis completes
- Added runPostAnalysisCleanup() method to aggressive-cleanup service
- Cleanup triggers added to both single and batch analysis endpoints
- More efficient: cleanup happens only when needed, not on timer
- Prevents zombie processes without interfering with active analysis
- Production mode still uses periodic cleanup as backup (10 min intervals)
- Gentle cleanup in development: SIGTERM first, then SIGKILL if needed
2025-07-18 19:11:15 +02:00
mindesbunister
2bdf9e2b41 fix: implement proper multi-timeframe batch analysis
- Create /api/batch-analysis endpoint that collects ALL screenshots first
- Then sends all screenshots to AI for comprehensive analysis
- Fixes issue where individual timeframes were analyzed immediately
- Multi-timeframe analysis now provides cross-timeframe consensus
- Update AIAnalysisPanel to use batch analysis for multiple timeframes
- Maintains backward compatibility with single timeframe analysis
2025-07-18 18:32:08 +02:00
mindesbunister
bd49c65867 feat: Fix position calculator visibility and add auto-detection
- Fixed position calculator not showing when analysis entry price is 0
- Added auto-detection of Long/Short position type from AI analysis recommendation
- Implemented independent price fetching from CoinGecko API
- Added current market price display with AI recommendation
- Enhanced position calculator with fallback prices for testing
- Added API endpoint /api/price for real-time price data
- Position calculator now works even when analysis lacks specific entry prices
- Shows 'Auto-detected from analysis' label when position type is determined from AI

The position calculator is now always visible and uses:
1. Current market price from CoinGecko API
2. Auto-detected position type from analysis (Long/Short)
3. Fallback prices for testing when API is unavailable
4. Default stop loss/take profit levels when not specified in analysis
2025-07-18 13:33:34 +02:00
mindesbunister
ba354c609d feat: implement dynamic position calculator with leverage slider
- Added comprehensive PositionCalculator component with real-time PnL calculations
- Implemented dynamic leverage adjustment with slider (1x to 100x)
- Added investment amount input for position sizing
- Integrated liquidation price calculations based on leverage and maintenance margin
- Added real-time price fetching from multiple sources (CoinGecko, CoinCap, Binance)
- Implemented automatic stop loss and take profit extraction from AI analysis
- Added risk/reward ratio calculations and position metrics
- Included trading fee calculations and net investment display
- Added position type selection (Long/Short) with dynamic PnL calculation
- Integrated high leverage warning system for risk management
- Added advanced settings for customizable trading fees and maintenance margins
- Automatically updates calculations when analysis parameters change
- Supports both manual price input and real-time market data
- Fully responsive design with gradient styling matching app theme
2025-07-18 13:16:11 +02:00
mindesbunister
56409b1161 feat: add manual cleanup API endpoint
- Added POST /api/cleanup endpoint for manual process cleanup
- Added GET /api/cleanup endpoint to check cleanup status
- Enables manual triggering of aggressive cleanup via API
- Useful for testing and manual maintenance
2025-07-18 13:09:23 +02:00
mindesbunister
6232c457ad feat: implement comprehensive process cleanup system
- Added aggressive cleanup system that runs every 5 minutes to kill orphaned processes
- Enhanced process cleanup with better signal handling and forced cleanup
- Added startup initialization system to ensure cleanup is properly loaded
- Integrated cleanup system into app layouts for automatic initialization
- Added zombie process cleanup and temp directory cleanup
- Improved Docker container restart behavior for proper process cleanup
- Resolves issue with zombie Chrome processes accumulating
2025-07-18 13:08:31 +02:00
mindesbunister
186cb6355c fix: correct timeframe display in screenshot gallery
- Fixed timeframe mapping logic in ScreenshotGallery component
- Improved timeframe extraction from filenames with better pattern matching
- Added fallback logic to prioritize filename-based timeframe detection
- Enhanced sorting to handle all timeframe formats (5m, 1h, 4h, 1d, 1w, 1M)
- Resolves UI bug where gallery showed incorrect timeframe descriptions
2025-07-18 12:28:12 +02:00
mindesbunister
1b0d92d6ad feat: implement robust browser process cleanup system
- Add cleanup-chromium.sh script for manual zombie process cleanup
- Add docker-entrypoint.sh with signal handlers for graceful shutdown
- Add lib/process-cleanup.ts for automatic cleanup on app termination
- Enhanced forceCleanup() method in tradingview-automation.ts:
  - Individual page closing before browser termination
  - Force kill remaining processes with SIGKILL
  - Reset operation locks after cleanup
- Improved browser launch args to prevent zombie processes:
  - Better crash reporter handling
  - Enhanced background process management
  - Removed problematic --single-process flag
- Updated Dockerfile to use new entrypoint with cleanup handlers
- Set DOCKER_ENV environment variable for container detection
- Add proper signal handling (SIGINT, SIGTERM, SIGQUIT)
- Automatic cleanup of temporary Puppeteer profiles

Resolves zombie Chromium process accumulation issue
2025-07-18 12:15:59 +02:00
18 changed files with 1618 additions and 84 deletions

View File

@@ -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"]

View 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
View 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'
}
})
}

View File

@@ -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
View 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
})
}
}

View File

@@ -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
View 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"

View File

@@ -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>

View 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>
)
}

View File

@@ -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
View 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
View 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

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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')
}

View File

@@ -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>
)
}