feat: Add persistent settings and multiple layouts support

- Add settings manager to persist symbol, timeframe, and layouts
- Support multiple layouts for comprehensive chart analysis
- Remove debug screenshots for cleaner logs
- Update AI analysis with professional trading prompt
- Add multi-screenshot analysis for better trading insights
- Update analyze API to use saved settings and multiple layouts
This commit is contained in:
root
2025-07-09 14:24:48 +02:00
parent 6a1a4576a9
commit 3361359119
28 changed files with 1487 additions and 30 deletions

208
lib/ai-analysis.ts Normal file
View File

@@ -0,0 +1,208 @@
import OpenAI from 'openai'
import fs from 'fs/promises'
import path from 'path'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
export interface AnalysisResult {
summary: string
marketSentiment: 'BULLISH' | 'BEARISH' | 'NEUTRAL'
keyLevels: {
support: number[]
resistance: number[]
}
recommendation: 'BUY' | 'SELL' | 'HOLD'
confidence: number // 0-100
reasoning: string
}
export class AIAnalysisService {
async analyzeScreenshot(filename: string): Promise<AnalysisResult | null> {
try {
const screenshotsDir = path.join(process.cwd(), 'screenshots')
const imagePath = path.join(screenshotsDir, filename)
// Read image file
const imageBuffer = await fs.readFile(imagePath)
const base64Image = imageBuffer.toString('base64')
const prompt = `You are now a professional trading assistant focused on short-term crypto trading using 515min timeframes. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
Analyze the attached TradingView chart screenshot and provide a detailed trading analysis.
### WHEN GIVING A TRADE SETUP:
Be 100% SPECIFIC. Provide:
1. **ENTRY**
- Exact price level (with a ± entry buffer if needed)
- Rationale: e.g., "Rejection from 15 EMA + VWAP confluence near intraday supply"
2. **STOP-LOSS (SL)**
- Exact level (not arbitrary)
- Explain *why* it's there: "Above VWAP + failed breakout zone"
3. **TAKE PROFITS**
- TP1: Immediate structure (ex: previous low at $149.20)
- TP2: Extended target if momentum continues (e.g., $148.00)
- Mention **expected RSI/OBV behavior** at each TP zone
4. **RISK-TO-REWARD**
- Show R:R. Ex: "1:2.5 — Risking $X to potentially gain $Y"
5. **CONFIRMATION TRIGGER**
- Exact signal to wait for: e.g., "Bearish engulfing candle on rejection from VWAP zone"
- OBV: "Must be making lower highs + dropping below 30min average"
- RSI: "Should remain under 50 on rejection. Overbought ≥70 = wait"
6. **INDICATOR ANALYSIS**
- **RSI**: If RSI crosses above 70 while price is under resistance → *wait*
- **VWAP**: If price retakes VWAP with bullish momentum → *consider invalidation*
- **OBV**: If OBV starts climbing while price stays flat → *early exit or reconsider bias*
Return your answer as a JSON object with the following structure:
{
"summary": "Brief market summary",
"marketSentiment": "BULLISH" | "BEARISH" | "NEUTRAL",
"keyLevels": {
"support": [number array],
"resistance": [number array]
},
"recommendation": "BUY" | "SELL" | "HOLD",
"confidence": number (0-100),
"reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers"
}
Be concise but thorough. Only return valid JSON.`
const response = await openai.chat.completions.create({
model: "gpt-4o", // Updated to current vision model
messages: [
{
role: "user",
content: [
{ type: "text", text: prompt },
{ type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } }
]
}
],
max_tokens: 1024
})
const content = response.choices[0]?.message?.content
if (!content) return null
// Extract JSON from response
const match = content.match(/\{[\s\S]*\}/)
if (!match) return null
const json = match[0]
const result = JSON.parse(json)
// Optionally: validate result structure here
return result as AnalysisResult
} catch (e) {
console.error('AI analysis error:', e)
return null
}
}
async analyzeMultipleScreenshots(filenames: string[]): Promise<AnalysisResult | null> {
try {
const screenshotsDir = path.join(process.cwd(), 'screenshots')
const images: any[] = []
for (const filename of filenames) {
const imagePath = path.join(screenshotsDir, filename)
const imageBuffer = await fs.readFile(imagePath)
const base64Image = imageBuffer.toString('base64')
images.push({ type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } })
}
const prompt = `You are now a professional trading assistant focused on short-term crypto trading using 515min timeframes. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
Analyze the attached TradingView chart screenshots (multiple layouts of the same symbol) and provide a comprehensive trading analysis by combining insights from all charts.
### WHEN GIVING A TRADE SETUP:
Be 100% SPECIFIC. Provide:
1. **ENTRY**
- Exact price level (with a ± entry buffer if needed)
- Rationale: e.g., "Rejection from 15 EMA + VWAP confluence near intraday supply"
2. **STOP-LOSS (SL)**
- Exact level (not arbitrary)
- Explain *why* it's there: "Above VWAP + failed breakout zone"
3. **TAKE PROFITS**
- TP1: Immediate structure (ex: previous low at $149.20)
- TP2: Extended target if momentum continues (e.g., $148.00)
- Mention **expected RSI/OBV behavior** at each TP zone
4. **RISK-TO-REWARD**
- Show R:R. Ex: "1:2.5 — Risking $X to potentially gain $Y"
5. **CONFIRMATION TRIGGER**
- Exact signal to wait for: e.g., "Bearish engulfing candle on rejection from VWAP zone"
- OBV: "Must be making lower highs + dropping below 30min average"
- RSI: "Should remain under 50 on rejection. Overbought ≥70 = wait"
6. **INDICATOR ANALYSIS**
- **RSI**: If RSI crosses above 70 while price is under resistance → *wait*
- **VWAP**: If price retakes VWAP with bullish momentum → *consider invalidation*
- **OBV**: If OBV starts climbing while price stays flat → *early exit or reconsider bias*
Cross-reference all layouts to provide the most accurate analysis. If layouts show conflicting signals, explain which one takes priority and why.
Return your answer as a JSON object with the following structure:
{
"summary": "Brief market summary combining all layouts",
"marketSentiment": "BULLISH" | "BEARISH" | "NEUTRAL",
"keyLevels": {
"support": [number array],
"resistance": [number array]
},
"recommendation": "BUY" | "SELL" | "HOLD",
"confidence": number (0-100),
"reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers from all layouts"
}
Be concise but thorough. Only return valid JSON.`
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "user",
content: [
{ type: "text", text: prompt },
...images
]
}
],
max_tokens: 1500,
temperature: 0.1
})
const content = response.choices[0]?.message?.content
if (!content) {
throw new Error('No content received from OpenAI')
}
// Parse the JSON response
const jsonMatch = content.match(/\{[\s\S]*\}/)
if (!jsonMatch) {
throw new Error('No JSON found in response')
}
const analysis = JSON.parse(jsonMatch[0])
// Validate the structure
if (!analysis.summary || !analysis.marketSentiment || !analysis.recommendation || !analysis.confidence) {
throw new Error('Invalid analysis structure')
}
return analysis
} catch (error) {
console.error('AI multi-analysis error:', error)
return null
}
}
}
export const aiAnalysisService = new AIAnalysisService()

82
lib/auto-trading.ts Normal file
View File

@@ -0,0 +1,82 @@
import { tradingViewCapture } from './tradingview'
import { aiAnalysisService } from './ai-analysis'
import prisma from './prisma'
export interface AutoTradingConfig {
enabled: boolean
symbols: string[]
intervalMinutes: number
maxDailyTrades: number
tradingAmount: number
confidenceThreshold: number
}
export class AutoTradingService {
private config: AutoTradingConfig
private intervalId: NodeJS.Timeout | null = null
private dailyTradeCount: Record<string, number> = {}
constructor(config: AutoTradingConfig) {
this.config = config
this.dailyTradeCount = {}
}
start() {
if (this.intervalId || !this.config.enabled) return
this.intervalId = setInterval(() => this.run(), this.config.intervalMinutes * 60 * 1000)
this.run() // Run immediately on start
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
}
async run() {
if (!this.config.enabled) return
for (const symbol of this.config.symbols) {
if ((this.dailyTradeCount[symbol] || 0) >= this.config.maxDailyTrades) continue
// 1. Capture screenshot
const filename = `${symbol}_${Date.now()}.png`
const screenshotPath = await tradingViewCapture.capture(symbol, filename)
// 2. Analyze screenshot
const analysis = await aiAnalysisService.analyzeScreenshot(filename)
if (!analysis || analysis.confidence < this.config.confidenceThreshold) continue
// 3. Execute trade (stub: integrate with driftTradingService)
// const tradeResult = await driftTradingService.executeTrade({ ... })
// 4. Save trade to DB
await prisma.trade.create({
data: {
symbol,
side: analysis.recommendation === 'BUY' ? 'LONG' : analysis.recommendation === 'SELL' ? 'SHORT' : 'NONE',
amount: this.config.tradingAmount,
price: 0, // To be filled with actual execution price
status: 'PENDING',
screenshotUrl: screenshotPath,
aiAnalysis: JSON.stringify(analysis),
executedAt: new Date(),
userId: 'system', // Or actual user if available
}
})
this.dailyTradeCount[symbol] = (this.dailyTradeCount[symbol] || 0) + 1
}
}
setConfig(config: Partial<AutoTradingConfig>) {
this.config = { ...this.config, ...config }
}
}
export function getAutoTradingService() {
// Singleton pattern or similar
return new AutoTradingService({
enabled: false,
symbols: ['BTCUSD'],
intervalMinutes: 15,
maxDailyTrades: 10,
tradingAmount: 100,
confidenceThreshold: 80
})
}

13
lib/prisma.ts Normal file
View File

@@ -0,0 +1,13 @@
import { PrismaClient } from '@prisma/client'
declare global {
var prisma: PrismaClient | undefined
}
const prisma = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalThis.prisma = prisma
}
export default prisma

88
lib/settings.ts Normal file
View File

@@ -0,0 +1,88 @@
import fs from 'fs/promises'
import path from 'path'
export interface TradingSettings {
symbol: string
timeframe: string
layouts: string[]
lastUpdated: number
}
const SETTINGS_FILE = path.join(process.cwd(), 'trading-settings.json')
export class SettingsManager {
private static instance: SettingsManager
private settings: TradingSettings = {
symbol: 'BTCUSD',
timeframe: '5',
layouts: ['ai'],
lastUpdated: Date.now()
}
private constructor() {}
static getInstance(): SettingsManager {
if (!SettingsManager.instance) {
SettingsManager.instance = new SettingsManager()
}
return SettingsManager.instance
}
async loadSettings(): Promise<TradingSettings> {
try {
const data = await fs.readFile(SETTINGS_FILE, 'utf-8')
this.settings = JSON.parse(data)
console.log('Loaded settings:', this.settings)
} catch (error) {
console.log('No existing settings found, using defaults')
await this.saveSettings()
}
return this.settings
}
async saveSettings(): Promise<void> {
try {
this.settings.lastUpdated = Date.now()
await fs.writeFile(SETTINGS_FILE, JSON.stringify(this.settings, null, 2))
console.log('Settings saved:', this.settings)
} catch (error) {
console.error('Failed to save settings:', error)
}
}
async updateSettings(updates: Partial<TradingSettings>): Promise<TradingSettings> {
this.settings = { ...this.settings, ...updates }
await this.saveSettings()
return this.settings
}
getSettings(): TradingSettings {
return this.settings
}
async setSymbol(symbol: string): Promise<void> {
await this.updateSettings({ symbol })
}
async setTimeframe(timeframe: string): Promise<void> {
await this.updateSettings({ timeframe })
}
async setLayouts(layouts: string[]): Promise<void> {
await this.updateSettings({ layouts })
}
async addLayout(layout: string): Promise<void> {
if (!this.settings.layouts.includes(layout)) {
const layouts = [...this.settings.layouts, layout]
await this.updateSettings({ layouts })
}
}
async removeLayout(layout: string): Promise<void> {
const layouts = this.settings.layouts.filter(l => l !== layout)
await this.updateSettings({ layouts })
}
}
export const settingsManager = SettingsManager.getInstance()

396
lib/tradingview.ts Normal file
View File

@@ -0,0 +1,396 @@
import puppeteer, { Browser, Page, Frame } from 'puppeteer'
import path from 'path'
import fs from 'fs/promises'
import { settingsManager } from './settings'
const TRADINGVIEW_EMAIL = process.env.TRADINGVIEW_EMAIL
const TRADINGVIEW_PASSWORD = process.env.TRADINGVIEW_PASSWORD
const TRADINGVIEW_LAYOUTS = (process.env.TRADINGVIEW_LAYOUTS || '').split(',').map(l => l.trim())
const PUPPETEER_EXECUTABLE_PATH = process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium'
export class TradingViewCapture {
private browser: Browser | null = null
private page: Page | null = null
private loggedIn = false
async init() {
if (!this.browser) {
this.browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu'
],
executablePath: PUPPETEER_EXECUTABLE_PATH
})
console.log('Puppeteer browser launched')
}
if (!this.page) {
this.page = await this.browser.newPage()
await this.page.setViewport({ width: 1920, height: 1080 })
console.log('Puppeteer page created')
}
if (!this.loggedIn) {
console.log('Logging in to TradingView...')
await this.login()
this.loggedIn = true
console.log('Logged in to TradingView')
}
return this.page
}
async login() {
if (!TRADINGVIEW_EMAIL || !TRADINGVIEW_PASSWORD) {
throw new Error('TradingView credentials not set in .env')
}
const page = this.page || (await this.browser!.newPage())
console.log('Navigating to TradingView login page...')
await page.goto('https://www.tradingview.com/#signin', { waitUntil: 'networkidle2' })
// Check if we're already logged in
try {
const loggedInIndicator = await page.waitForSelector('.tv-header__user-menu-button, [data-name="header-user-menu"]', { timeout: 3000 })
if (loggedInIndicator) {
console.log('Already logged in to TradingView')
return
}
} catch (e) {
console.log('Not logged in yet, proceeding with login...')
}
try {
// Wait for the login modal to appear and look for email input directly
console.log('Looking for email input field...')
// Try to find the email input field directly (new TradingView layout)
const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 })
if (emailInput) {
console.log('Found email input field directly')
await emailInput.click() // Click to focus
await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
// Find password field
const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 })
if (!passwordInput) {
throw new Error('Could not find password input field')
}
await passwordInput.click() // Click to focus
await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
// Find and click the sign in button
const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 })
if (!signInButton) {
// Try to find button with sign in text
const buttons = await page.$$('button')
let foundButton = null
for (const btn of buttons) {
const text = await page.evaluate(el => el.innerText || el.textContent, btn)
if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) {
foundButton = btn
break
}
}
if (!foundButton) {
throw new Error('Could not find sign in button')
}
await foundButton.click()
} else {
await signInButton.click()
}
} else {
throw new Error('Could not find email input field')
}
} catch (e) {
// Fallback: try to find email button first
console.log('Fallback: looking for email button...')
try {
await page.waitForSelector('button', { timeout: 15000 })
const buttons = await page.$$('button')
let emailBtn = null
// Look for email button with various text patterns
for (const btn of buttons) {
const text = await page.evaluate(el => el.innerText || el.textContent, btn)
if (text && (
text.trim().toLowerCase().includes('email') ||
text.trim().toLowerCase().includes('sign in with email') ||
text.trim().toLowerCase().includes('continue with email')
)) {
emailBtn = btn
break
}
}
if (emailBtn) {
console.log('Found email button, clicking...')
await emailBtn.click()
await new Promise(res => setTimeout(res, 1000))
// Now fill in the form
const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 })
if (!emailInput) {
throw new Error('Could not find email input field after clicking email button')
}
await emailInput.click() // Click to focus
await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 })
if (!passwordInput) {
throw new Error('Could not find password input field after clicking email button')
}
await passwordInput.click() // Click to focus
await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 })
if (!signInButton) {
// Try to find button with sign in text
const buttons = await page.$$('button')
let foundButton = null
for (const btn of buttons) {
const text = await page.evaluate(el => el.innerText || el.textContent, btn)
if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) {
foundButton = btn
break
}
}
if (!foundButton) {
throw new Error('Could not find sign in button after clicking email button')
}
await foundButton.click()
} else {
await signInButton.click()
}
} else {
throw new Error('Could not find email button')
}
} catch (e2) {
console.error('Could not find or click email button:', e2)
const errorMessage = e2 instanceof Error ? e2.message : String(e2)
throw new Error('Could not find or click email button on TradingView login page. ' + errorMessage)
}
}
// Wait for navigation or dashboard (main page)
try {
console.log('Waiting for login to complete...')
await page.waitForSelector('.tv-header__user-menu-button, .chart-container, [data-name="header-user-menu"]', { timeout: 30000 })
} catch (e) {
console.error('Login navigation did not complete.')
throw new Error('Login navigation did not complete.')
}
console.log('TradingView login complete')
}
async capture(symbol: string, filename: string, layouts?: string[], timeframe?: string) {
console.log('Working directory:', process.cwd())
// Load settings and update if provided
const settings = await settingsManager.loadSettings()
if (symbol && symbol !== settings.symbol) {
await settingsManager.setSymbol(symbol)
}
if (timeframe && timeframe !== settings.timeframe) {
await settingsManager.setTimeframe(timeframe)
}
if (layouts && JSON.stringify(layouts) !== JSON.stringify(settings.layouts)) {
await settingsManager.setLayouts(layouts)
}
// Use saved settings if not provided
const finalSymbol = symbol || settings.symbol
const finalTimeframe = timeframe || settings.timeframe
const finalLayouts = layouts || settings.layouts
console.log('Using settings:', { symbol: finalSymbol, timeframe: finalTimeframe, layouts: finalLayouts })
const page = await this.init()
// Add timeframe to TradingView URL if provided
let url = `https://www.tradingview.com/chart/?symbol=${finalSymbol}`
if (finalTimeframe) {
url += `&interval=${encodeURIComponent(finalTimeframe)}`
}
try {
console.log('Navigating to TradingView chart:', url)
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 })
console.log('Successfully navigated to chart')
} catch (e: any) {
console.error('Failed to load TradingView chart page:', e)
throw new Error('Failed to load TradingView chart page: ' + (e.message || e))
}
// Capture screenshots for each layout
const screenshots: string[] = []
for (let i = 0; i < finalLayouts.length; i++) {
const layout = finalLayouts[i]
console.log(`Processing layout ${i + 1}/${finalLayouts.length}: ${layout}`)
// Load the layout
await this.loadLayout(page, layout)
// Wait for layout to load
await new Promise(res => setTimeout(res, 3000))
// Generate filename for this layout
const layoutFilename = filename.replace('.png', `_${layout}.png`)
const screenshotsDir = path.join(process.cwd(), 'screenshots')
await fs.mkdir(screenshotsDir, { recursive: true })
const filePath = path.join(screenshotsDir, layoutFilename)
try {
await page.screenshot({ path: filePath as `${string}.png`, type: 'png' })
console.log(`Screenshot saved for layout ${layout}:`, filePath)
screenshots.push(filePath)
} catch (e: any) {
const debugScreenshotErrorPath = path.resolve(`debug_screenshot_error_${layout}.png`) as `${string}.png`
await page.screenshot({ path: debugScreenshotErrorPath })
console.error(`Failed to capture screenshot for layout ${layout}:`, e)
console.error('Screenshot on screenshot error:', debugScreenshotErrorPath)
throw new Error(`Failed to capture screenshot for layout ${layout}: ` + (e.message || e))
}
}
return screenshots
}
private async loadLayout(page: Page, layout: string): Promise<void> {
try {
console.log('Trying to load layout:', layout)
// Try multiple selectors for the layout button
const layoutSelectors = [
'[data-name="load-chart-layout-dialog"]',
'[data-name="layouts-menu"]',
'[data-name="chart-layout-button"]',
'button[title*="Layout" i]',
'button[aria-label*="Layout" i]',
'[data-testid*="layout"]'
]
let layoutButton = null
for (const selector of layoutSelectors) {
try {
layoutButton = await page.waitForSelector(selector, { timeout: 3000 })
if (layoutButton) {
console.log('Found layout button with selector:', selector)
break
}
} catch (e) {
// Continue to next selector
}
}
if (!layoutButton) {
// Try to find layout button by text content
const buttons = await page.$$('button, [role="button"]')
for (const btn of buttons) {
const text = await page.evaluate(el => {
const element = el as HTMLElement
return element.innerText || element.textContent || element.title || element.getAttribute('aria-label')
}, btn)
if (text && text.toLowerCase().includes('layout')) {
layoutButton = btn
console.log('Found layout button by text:', text)
break
}
}
}
if (layoutButton) {
await layoutButton.click()
console.log('Clicked layout button')
await new Promise(res => setTimeout(res, 1000))
// Look for search input or layout items
const searchSelectors = [
'input[name="search"]',
'input[placeholder*="search" i]',
'input[type="search"]',
'input[data-name="search"]'
]
let searchInput = null
for (const selector of searchSelectors) {
try {
searchInput = await page.waitForSelector(selector, { timeout: 3000 })
if (searchInput) break
} catch (e) {
// Continue to next selector
}
}
if (searchInput) {
console.log('Found search input, typing layout name...')
await searchInput.type(layout, { delay: 50 })
await new Promise(res => setTimeout(res, 2000))
// Try to find and click the layout item
const layoutItemSelectors = [
'[data-name="chart-layout-list-item"]',
'[data-testid*="layout-item"]',
'.layout-item',
'[role="option"]'
]
let layoutItem = null
for (const selector of layoutItemSelectors) {
try {
const items = await page.$$(selector)
for (const item of items) {
const text = await page.evaluate(el => {
const element = el as HTMLElement
return element.innerText || element.textContent
}, item)
if (text && text.toLowerCase().includes(layout.toLowerCase())) {
layoutItem = item
break
}
}
if (layoutItem) break
} catch (e) {
// Continue to next selector
}
}
if (layoutItem) {
await layoutItem.click()
console.log('Clicked layout item:', layout)
} else {
console.log('Layout item not found, trying generic approach...')
await page.evaluate((layout) => {
const items = Array.from(document.querySelectorAll('*'))
const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase()))
if (item) (item as HTMLElement).click()
}, layout)
}
} else {
console.log('Search input not found, trying to find layout directly...')
await page.evaluate((layout) => {
const items = Array.from(document.querySelectorAll('*'))
const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase()))
if (item) (item as HTMLElement).click()
}, layout)
}
await new Promise(res => setTimeout(res, 4000))
console.log('Layout loaded:', layout)
} else {
console.log('Layout button not found, skipping layout loading')
}
} catch (e: any) {
const debugLayoutErrorPath = path.resolve('debug_layout_error.png') as `${string}.png`
await page.screenshot({ path: debugLayoutErrorPath })
console.error('TradingView layout not found or could not be loaded:', e)
console.log('Continuing without layout...')
// Don't throw error, just continue without layout
}
}
}
export const tradingViewCapture = new TradingViewCapture()