- Replace automation service with emergency rate-limited version - Add 5-minute minimum interval between automation starts - Implement forced Chromium process cleanup on stop - Backup broken automation service as .broken file - Emergency service prevents multiple simultaneous automations - Fixed 1400+ Chromium process accumulation issue - Tested and confirmed: rate limiting works, processes stay at 0
290 lines
9.5 KiB
TypeScript
290 lines
9.5 KiB
TypeScript
import { tradingViewAutomation, TradingViewAutomation, TradingViewCredentials } from './tradingview-automation'
|
||
import { progressTracker } from './progress-tracker'
|
||
import fs from 'fs/promises'
|
||
import path from 'path'
|
||
|
||
export interface BatchScreenshotConfig {
|
||
symbol: string
|
||
timeframes: string[] // Multiple timeframes
|
||
layouts?: string[] // Multiple chart layouts
|
||
credentials?: TradingViewCredentials
|
||
sessionId?: string
|
||
}
|
||
|
||
export interface ScreenshotBatch {
|
||
symbol: string
|
||
timeframe: string
|
||
layout: string
|
||
filepath: string
|
||
timestamp: number
|
||
}
|
||
|
||
// Layout URL mappings for direct navigation
|
||
const LAYOUT_URLS: { [key: string]: string } = {
|
||
'ai': 'Z1TzpUrf', // RSI + EMAs + MACD
|
||
'diy': 'vWVvjLhP' // Stochastic RSI + VWAP + OBV
|
||
}
|
||
|
||
export class BatchScreenshotService {
|
||
private static readonly OPERATION_TIMEOUT = 180000 // 3 minutes for batch operations
|
||
private aiSession: TradingViewAutomation | null = null
|
||
private diySession: TradingViewAutomation | null = null
|
||
private sessionId: string
|
||
|
||
constructor(sessionId?: string) {
|
||
this.sessionId = sessionId || `batch_${Date.now()}`
|
||
}
|
||
|
||
/**
|
||
* Capture screenshots for multiple timeframes and layouts in parallel
|
||
* This dramatically speeds up analysis by batching all screenshots
|
||
*/
|
||
async captureMultipleTimeframes(config: BatchScreenshotConfig): Promise<ScreenshotBatch[]> {
|
||
console.log('🚀 Batch Screenshot Service - Multi-Timeframe Capture')
|
||
console.log('📋 Config:', config)
|
||
|
||
const { symbol, timeframes, layouts = ['ai', 'diy'], sessionId } = config
|
||
const screenshotBatches: ScreenshotBatch[] = []
|
||
|
||
if (sessionId) {
|
||
progressTracker.updateStep(sessionId, 'init', 'active', `Initializing batch capture for ${timeframes.length} timeframes`)
|
||
}
|
||
|
||
try {
|
||
// Ensure screenshots directory exists
|
||
const screenshotsDir = path.join(process.cwd(), 'screenshots')
|
||
await fs.mkdir(screenshotsDir, { recursive: true })
|
||
|
||
console.log(`\n🔄 Starting batch capture: ${timeframes.length} timeframes × ${layouts.length} layouts = ${timeframes.length * layouts.length} screenshots`)
|
||
|
||
if (sessionId) {
|
||
progressTracker.updateStep(sessionId, 'auth', 'active', 'Initializing browser sessions')
|
||
}
|
||
|
||
// Create parallel promises for each layout
|
||
const layoutPromises = layouts.map(async (layout) => {
|
||
const session = await this.getOrCreateSession(layout, config.credentials)
|
||
const layoutResults: ScreenshotBatch[] = []
|
||
|
||
console.log(`📊 Starting ${layout.toUpperCase()} session for ${timeframes.length} timeframes`)
|
||
|
||
if (sessionId) {
|
||
progressTracker.updateStep(sessionId, 'navigation', 'active', `Navigating ${layout} layout to ${symbol}`)
|
||
}
|
||
|
||
// Navigate to first timeframe to establish base chart
|
||
const firstTimeframe = timeframes[0]
|
||
await this.navigateToChart(session, symbol, firstTimeframe, layout)
|
||
|
||
console.log(`✅ ${layout.toUpperCase()} session established on ${symbol} ${firstTimeframe}`)
|
||
|
||
// Now capture all timeframes for this layout sequentially (but layouts run in parallel)
|
||
for (let i = 0; i < timeframes.length; i++) {
|
||
const timeframe = timeframes[i]
|
||
|
||
try {
|
||
if (sessionId) {
|
||
progressTracker.updateStep(sessionId, 'capture', 'active',
|
||
`Capturing ${layout} ${timeframe} (${i + 1}/${timeframes.length})`)
|
||
}
|
||
|
||
console.log(`📸 ${layout.toUpperCase()}: Capturing ${symbol} ${timeframe}...`)
|
||
|
||
// Change timeframe if not the first one
|
||
if (i > 0) {
|
||
await this.changeTimeframe(session, timeframe, symbol)
|
||
}
|
||
|
||
// Take screenshot
|
||
const timestamp = Date.now()
|
||
const filename = `${symbol}_${timeframe}_${layout}_${timestamp}.png`
|
||
const filepath = path.join(screenshotsDir, filename)
|
||
|
||
await session.takeScreenshot({ filename })
|
||
|
||
const batch: ScreenshotBatch = {
|
||
symbol,
|
||
timeframe,
|
||
layout,
|
||
filepath: filename, // Store relative filename for compatibility
|
||
timestamp
|
||
}
|
||
|
||
layoutResults.push(batch)
|
||
console.log(`✅ ${layout.toUpperCase()}: ${timeframe} captured → ${filename}`)
|
||
|
||
// Small delay between timeframe changes to ensure chart loads
|
||
if (i < timeframes.length - 1) {
|
||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error(`❌ ${layout.toUpperCase()}: Failed to capture ${timeframe}:`, error)
|
||
}
|
||
}
|
||
|
||
console.log(`🎯 ${layout.toUpperCase()} session completed: ${layoutResults.length}/${timeframes.length} screenshots`)
|
||
return layoutResults
|
||
})
|
||
|
||
// Wait for all layout sessions to complete
|
||
const allLayoutResults = await Promise.all(layoutPromises)
|
||
|
||
// Flatten results
|
||
screenshotBatches.push(...allLayoutResults.flat())
|
||
|
||
if (sessionId) {
|
||
progressTracker.updateStep(sessionId, 'capture', 'completed',
|
||
`Batch capture completed: ${screenshotBatches.length} screenshots`)
|
||
}
|
||
|
||
console.log(`\n🎯 BATCH CAPTURE COMPLETED`)
|
||
console.log(`📊 Total Screenshots: ${screenshotBatches.length}`)
|
||
console.log(`⏱️ Efficiency: ${timeframes.length * layouts.length} screenshots captured with ${layouts.length} parallel sessions`)
|
||
|
||
return screenshotBatches
|
||
|
||
} catch (error: any) {
|
||
console.error('❌ Batch screenshot capture failed:', error)
|
||
|
||
if (sessionId) {
|
||
progressTracker.updateStep(sessionId, 'capture', 'error', `Batch capture failed: ${error?.message || 'Unknown error'}`)
|
||
}
|
||
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get or create a persistent session for a layout
|
||
*/
|
||
private async getOrCreateSession(layout: string, credentials?: TradingViewCredentials): Promise<TradingViewAutomation> {
|
||
if (layout === 'ai' && this.aiSession) {
|
||
return this.aiSession
|
||
}
|
||
|
||
if (layout === 'diy' && this.diySession) {
|
||
return this.diySession
|
||
}
|
||
|
||
// Create new session
|
||
console.log(`🔧 Creating new ${layout.toUpperCase()} session...`)
|
||
const session = new TradingViewAutomation()
|
||
|
||
// Initialize and login
|
||
await session.init()
|
||
await session.login(credentials || {
|
||
email: process.env.TRADINGVIEW_EMAIL || '',
|
||
password: process.env.TRADINGVIEW_PASSWORD || ''
|
||
})
|
||
|
||
// Store session
|
||
if (layout === 'ai') {
|
||
this.aiSession = session
|
||
} else {
|
||
this.diySession = session
|
||
}
|
||
|
||
return session
|
||
}
|
||
|
||
/**
|
||
* Navigate to a specific chart with symbol, timeframe, and layout
|
||
*/
|
||
private async navigateToChart(session: TradingViewAutomation, symbol: string, timeframe: string, layout: string): Promise<void> {
|
||
const layoutId = LAYOUT_URLS[layout]
|
||
if (!layoutId) {
|
||
throw new Error(`Unknown layout: ${layout}`)
|
||
}
|
||
|
||
// Use the navigateToLayout method
|
||
console.log(`🌐 ${layout.toUpperCase()}: Navigating to layout ${layoutId} with ${symbol}`)
|
||
const success = await session.navigateToLayout(layoutId, symbol, this.normalizeTimeframe(timeframe))
|
||
|
||
if (!success) {
|
||
throw new Error(`Failed to navigate to ${layout} layout`)
|
||
}
|
||
|
||
// Wait for chart to fully load
|
||
await new Promise(resolve => setTimeout(resolve, 5000))
|
||
}
|
||
|
||
/**
|
||
* Change timeframe on an existing chart session
|
||
*/
|
||
private async changeTimeframe(session: TradingViewAutomation, timeframe: string, symbol: string): Promise<void> {
|
||
console.log(`⏱️ Changing timeframe to ${timeframe}`)
|
||
|
||
// Use navigateToSymbol with timeframe parameter to change timeframe
|
||
const success = await session.navigateToSymbol(symbol, this.normalizeTimeframe(timeframe))
|
||
|
||
if (!success) {
|
||
console.warn(`Failed to change timeframe to ${timeframe}, continuing...`)
|
||
}
|
||
|
||
// Wait for chart to reload with new timeframe
|
||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||
}
|
||
|
||
/**
|
||
* Normalize timeframe for TradingView URL compatibility
|
||
*/
|
||
private normalizeTimeframe(timeframe: string): string {
|
||
const timeframeMap: { [key: string]: string } = {
|
||
'5m': '5',
|
||
'15m': '15',
|
||
'30m': '30',
|
||
'1h': '60',
|
||
'2h': '120',
|
||
'4h': '240',
|
||
'1d': 'D',
|
||
'1w': 'W',
|
||
'1M': 'M'
|
||
}
|
||
|
||
return timeframeMap[timeframe] || timeframe
|
||
}
|
||
|
||
/**
|
||
* Clean up all sessions
|
||
*/
|
||
async cleanup(): Promise<void> {
|
||
console.log('🧹 Cleaning up batch screenshot sessions...')
|
||
|
||
try {
|
||
if (this.aiSession) {
|
||
await this.aiSession.forceCleanup()
|
||
this.aiSession = null
|
||
}
|
||
|
||
if (this.diySession) {
|
||
await this.diySession.forceCleanup()
|
||
this.diySession = null
|
||
}
|
||
|
||
console.log('✅ Batch screenshot cleanup completed')
|
||
} catch (error) {
|
||
console.error('❌ Batch screenshot cleanup failed:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Convert batch results to format expected by existing systems
|
||
*/
|
||
static formatBatchForAnalysis(batches: ScreenshotBatch[]): { [timeframe: string]: string[] } {
|
||
const timeframeGroups: { [timeframe: string]: string[] } = {}
|
||
|
||
for (const batch of batches) {
|
||
if (!timeframeGroups[batch.timeframe]) {
|
||
timeframeGroups[batch.timeframe] = []
|
||
}
|
||
timeframeGroups[batch.timeframe].push(batch.filepath)
|
||
}
|
||
|
||
return timeframeGroups
|
||
}
|
||
}
|
||
|
||
// Export a factory function instead of a singleton instance
|
||
export const createBatchScreenshotService = (sessionId?: string) => new BatchScreenshotService(sessionId)
|