import puppeteer, { Browser, Page } from 'puppeteer' import { promises as fs } from 'fs' import * as path from 'path' export interface TradingViewCredentials { email: string password: string } // Environment variables fallback const TRADINGVIEW_EMAIL = process.env.TRADINGVIEW_EMAIL const TRADINGVIEW_PASSWORD = process.env.TRADINGVIEW_PASSWORD // Utility function to replace Puppeteer's waitForTimeout const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) // Helper function to check if element is visible using Puppeteer APIs async function isElementVisible(page: Page, selector: string, timeout: number = 1000): Promise { try { await page.waitForSelector(selector, { timeout, visible: true }) return true } catch { return false } } export interface NavigationOptions { symbol?: string // e.g., 'SOLUSD', 'BTCUSD' timeframe?: string // e.g., '5', '15', '1H' waitForChart?: boolean } // Session persistence configuration const SESSION_DATA_DIR = path.join(process.cwd(), '.tradingview-session') const COOKIES_FILE = path.join(SESSION_DATA_DIR, 'cookies.json') const SESSION_STORAGE_FILE = path.join(SESSION_DATA_DIR, 'session-storage.json') export class TradingViewAutomation { private browser: Browser | null = null private page: Page | null = null private isAuthenticated: boolean = false private static instance: TradingViewAutomation | null = null private initPromise: Promise | null = null private operationLock: boolean = false private lastRequestTime = 0 private requestCount = 0 private acquireOperationLock(): void { if (this.operationLock) { throw new Error('Another operation is already in progress. Please wait.') } this.operationLock = true } private releaseOperationLock(): void { this.operationLock = false } // Singleton pattern static getInstance(): TradingViewAutomation { if (!TradingViewAutomation.instance) { TradingViewAutomation.instance = new TradingViewAutomation() } return TradingViewAutomation.instance } async init(forceCleanup: boolean = false): Promise { this.acquireOperationLock() try { if (this.initPromise) { console.log('๐Ÿ”„ Initialization already in progress, waiting...') await this.initPromise return } if (forceCleanup && this.browser) { console.log('๐Ÿงน Force cleanup requested') await this.forceCleanup() } if (this.browser) { console.log('SUCCESS: Browser already initialized and connected') return } this.initPromise = this._doInit() try { await this.initPromise } finally { this.initPromise = null } } finally { this.releaseOperationLock() } } private async _doInit(): Promise { console.log('๐Ÿš€ Initializing TradingView automation with session persistence...') // Ensure session directory exists await fs.mkdir(SESSION_DATA_DIR, { recursive: true }) try { this.browser = await puppeteer.launch({ headless: true, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || '/usr/bin/chromium', timeout: 60000, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--disable-gpu', '--disable-web-security', '--disable-features=VizDisplayCompositor', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-features=TranslateUI', '--disable-ipc-flooding-protection', '--disable-extensions', '--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', // 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 ] }) this.page = await this.browser.newPage() // Set viewport await this.page.setViewport({ width: 1920, height: 1080 }) // Load saved session if available await this.loadSession() console.log('โœ… Browser initialized successfully') } catch (error) { console.error('โŒ Failed to initialize browser:', error) await this.forceCleanup() throw error } } async forceCleanup(): Promise { 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) } this.browser = null this.page = null this.isAuthenticated = false this.operationLock = false console.log('โœ… Cleanup completed') } private async loadSession(): Promise { if (!this.page) return try { // Load cookies if (await fs.access(COOKIES_FILE).then(() => true).catch(() => false)) { const cookiesData = await fs.readFile(COOKIES_FILE, 'utf8') const cookies = JSON.parse(cookiesData) await this.page.setCookie(...cookies) console.log('โœ… Loaded saved cookies') } } catch (e) { console.log('WARNING: Could not load session:', e) } } private async saveSession(): Promise { if (!this.page) return try { // Save cookies const cookies = await this.page.cookies() await fs.writeFile(COOKIES_FILE, JSON.stringify(cookies, null, 2)) console.log('โœ… Session saved') } catch (e) { console.log('WARNING: Could not save session:', e) } } async checkLoginStatus(): Promise { if (!this.page) return false try { console.log('๐Ÿ” CHECKING LOGIN STATUS - Starting comprehensive authentication detection...') // Strategy 1: Check JavaScript authentication variables console.log('CHECKING: Strategy 1: JavaScript authentication variables...') const authStatus = await this.page.evaluate(() => { console.log('EVAL: Starting JavaScript authentication check...') // Check window.is_authenticated console.log('EVAL: Checking window.is_authenticated...') const isAuthVar = (window as any).is_authenticated console.log('EVAL: window.is_authenticated =', isAuthVar, typeof isAuthVar) // Check window.user console.log('EVAL: Checking window.user...') const userVar = (window as any).user console.log('EVAL: window.user =', userVar, typeof userVar) // Check if user object has meaningful data if (userVar && typeof userVar === 'object') { console.log('EVAL: User object keys:', Object.keys(userVar)) console.log('EVAL: User object values preview:', JSON.stringify(userVar).substring(0, 200)) } // Return authentication status const result = isAuthVar === true || (userVar && typeof userVar === 'object' && Object.keys(userVar).length > 0) console.log('EVAL: Final Strategy 1 result:', result) return result }) console.log('CHECKING: Strategy 1 JavaScript result:', authStatus) if (authStatus) { console.log('โœ… Strategy 1: Authenticated via JavaScript variables') return true } // Strategy 1.5: Check for user avatar/profile elements (indicates logged in state) console.log('CHECKING: Strategy 1.5: User interface elements...') const userElements = await this.page.$$eval('[data-testid="header-user-menu"], .tv-header__user-menu, [class*="user-menu"], [class*="avatar"], [data-name="header-user-menu"]', elements => { console.log('EVAL: Found', elements.length, 'potential user elements') return elements.length > 0 }).catch(() => false) if (userElements) { console.log('Strategy 1.5: Found user interface elements - likely authenticated') return true } // Strategy 2: Check for absence of login forms (indicates already logged in) console.log('CHECKING: Strategy 2: Absence of login forms...') const pageUrl = this.page.url() console.log('CHECKING: Current URL:', pageUrl) // If we're not on a login page and no login forms exist, we're likely logged in const hasLoginForms = await this.page.$('input[type="email"], input[name="username"], input[name="email"], input[type="password"]').then(el => !!el).catch(() => false) const isOnLoginPage = pageUrl.includes('/signin') || pageUrl.includes('/login') || pageUrl.includes('/accounts/signin') console.log('CHECKING: Has login forms:', hasLoginForms) console.log('CHECKING: Is on login page:', isOnLoginPage) if (!hasLoginForms && !isOnLoginPage) { console.log('Strategy 2: No login forms and not on login page - likely authenticated') return true } // Strategy 3: Check for "Sign In" vs "User Menu" buttons console.log('CHECKING: Strategy 3: Page action buttons...') const hasSignInButton = await this.page.$('[data-testid="header-signin-button"], a[href*="signin"], button:has-text("Sign in")').then(el => !!el).catch(() => false) const hasUserMenu = await this.page.$('[data-testid="header-user-menu"], .tv-header__user-menu, [class*="user-menu"]').then(el => !!el).catch(() => false) console.log('CHECKING: Has sign in button:', hasSignInButton) console.log('CHECKING: Has user menu:', hasUserMenu) if (!hasSignInButton && hasUserMenu) { console.log('Strategy 3: User menu present, no sign in button - authenticated') return true } if (!hasSignInButton && !isOnLoginPage) { console.log('Strategy 3: No sign in button and not on login page - likely authenticated') return true } // Strategy 2: Check for user account indicators (positive indicators) console.log('CHECKING: Strategy 2: Checking for user account indicators...') await this.takeDebugScreenshot('login_status_check') const userIndicators = [ '.js-header-user-menu-button', // TradingView's main user button '[data-name="header-user-menu"]', '.tv-header__user-menu-button:not(.tv-header__user-menu-button--anonymous)', '.tv-header__user-menu-wrap' ] for (const selector of userIndicators) { try { const element = await this.page.$(selector) if (element) { const isVisible = await element.boundingBox() if (isVisible) { console.log('SUCCESS: Found user account element: ' + selector) return true } } } catch (e) { continue } } // Strategy 3: Check for anonymous/sign-in indicators (negative indicators) console.log('CHECKING: Strategy 3: Checking for anonymous/sign-in indicators...') const anonymousIndicators = [ '.tv-header__user-menu-button--anonymous', '[data-name="header-user-menu-sign-in"]', 'button:contains("Sign in")', 'a:contains("Sign in")' ] for (const selector of anonymousIndicators) { try { const element = await this.page.$(selector) if (element) { const isVisible = await element.boundingBox() if (isVisible) { console.log('ERROR: Found anonymous indicator: ' + selector + ' - not logged in') return false } } } catch (e) { continue } } // Strategy 4: Check URL patterns console.log('CHECKING: Strategy 4: Checking URL patterns...') const urlCheck = this.page.url() if (urlCheck.includes('/signin') || urlCheck.includes('/login')) { console.log('ERROR: On login page - not logged in') return false } // Strategy 5: Check authentication cookies console.log('CHECKING: Strategy 5: Checking authentication cookies...') const cookies = await this.page.cookies() const authCookies = cookies.filter(cookie => cookie.name.includes('auth') || cookie.name.includes('session') || cookie.name.includes('token') ) if (authCookies.length === 0) { console.log('WARNING: No authentication cookies found') } // Strategy 6: Check for personal content console.log('CHECKING: Strategy 6: Checking for personal content...') const personalContentSelectors = [ '[data-name="watchlist"]', '.tv-header__watchlist', '.js-backtesting-head' ] for (const selector of personalContentSelectors) { try { const element = await this.page.$(selector) if (element) { console.log('SUCCESS: Found personal content: ' + selector) return true } } catch (e) { continue } } // Strategy 7: Check for absence of login interface (final check) console.log('CHECKING: Strategy 7: Final absence of login interface check...') const finalUrlCheck = this.page.url() const hasAnyLoginElements = await this.page.$('input[type="email"], input[name="username"], input[name="email"], input[type="password"], [href*="signin"], [href*="login"], button:has-text("Sign in")').then(el => !!el).catch(() => false) const isOnMainTradingViewDomain = finalUrlCheck.includes('tradingview.com') && !finalUrlCheck.includes('/signin') && !finalUrlCheck.includes('/login') console.log('CHECKING: Has any login elements:', hasAnyLoginElements) console.log('CHECKING: Is on main TradingView domain (not login page):', isOnMainTradingViewDomain) if (isOnMainTradingViewDomain && !hasAnyLoginElements) { console.log('โœ… Strategy 7: On main TradingView domain with no login elements - authenticated') return true } // If we can't determine status clearly, assume not logged in to be safe console.log('WARNING: Could not determine login status clearly, assuming not logged in') return false } catch (e) { console.log('ERROR: Error checking login status:', e) return false } } async login(credentials?: TradingViewCredentials): Promise { if (!this.page) throw new Error('Page not initialized') const email = credentials?.email || TRADINGVIEW_EMAIL const password = credentials?.password || TRADINGVIEW_PASSWORD if (!email || !password) { throw new Error('TradingView credentials not provided') } try { // Check if already logged in const loggedIn = await this.checkLoginStatus() if (loggedIn) { console.log('SUCCESS: Already logged in, skipping login steps') return true } console.log('๐Ÿ” Starting login process...') // Navigate to main TradingView page first, then find login console.log('๐Ÿ“„ Navigating to TradingView main page...') try { await this.page.goto('https://www.tradingview.com/', { waitUntil: 'domcontentloaded', timeout: 30000 }) await sleep(3000) // Look for sign in buttons on main page console.log('๐Ÿ” Looking for Sign In button on main page...') const signinButtons = [ 'a[href*="signin"]', 'a[href*="accounts/signin"]', '[data-name="header-user-menu-sign-in"]', '.tv-header__user-menu-button', '.tv-header__user-menu-button--anonymous', '.js-signin-switch', 'button[data-role="button"]', 'button.tv-button', 'a.tv-button' ] let foundSignin = false for (const selector of signinButtons) { try { const elements = await this.page.$$(selector) for (const element of elements) { const isVisible = await element.boundingBox() if (isVisible) { const text = await element.evaluate(el => { const elem = el as HTMLElement return elem.textContent || elem.getAttribute('title') || elem.getAttribute('href') || '' }) console.log(`Found signin element: ${selector} with text: "${text}"`) // Check if this looks like a sign in button const lowerText = text.toLowerCase() if (lowerText.includes('sign in') || lowerText.includes('login') || lowerText.includes('signin') || selector.includes('signin') || selector.includes('user-menu-button')) { await element.click() console.log('โœ… Clicked sign in button') await sleep(5000) // Wait for login page/modal to load foundSignin = true break } } } if (foundSignin) break } catch (e) { const error = e as Error console.log(`โŒ Error with selector ${selector}: ${error.message}`) continue } } // If no selector-based approach works, try finding by text content if (!foundSignin) { console.log('๐Ÿ” Trying to find sign-in button by text content...') const clickableElements = await this.page.$$('button, a, div[role="button"], span[role="button"]') for (const element of clickableElements) { try { const isVisible = await element.boundingBox() if (isVisible) { const text = await element.evaluate(el => { const elem = el as HTMLElement return (elem.textContent || '').toLowerCase().trim() }) if (text.includes('sign in') || text.includes('signin') || text.includes('log in') || text.includes('login')) { console.log(`๐ŸŽฏ Found sign-in button with text: "${text}"`) await element.click() console.log('โœ… Clicked sign-in button by text') await sleep(5000) foundSignin = true break } } } catch (e) { continue } } } if (!foundSignin) { console.log('โš ๏ธ No sign in button found on main page, trying direct navigation...') await this.page.goto('https://www.tradingview.com/accounts/signin/', { waitUntil: 'domcontentloaded', timeout: 30000 }) } } catch (e) { const error = e as Error console.log(`โŒ Navigation error: ${error.message}`) // Fallback to direct login URL await this.page.goto('https://www.tradingview.com/accounts/signin/', { waitUntil: 'domcontentloaded', timeout: 30000 }) } await sleep(3000) await this.takeDebugScreenshot('login_page_loaded') // Check current URL and page state const currentUrl = await this.page.url() const pageTitle = await this.page.title() console.log(`๐ŸŒ Current URL: ${currentUrl}`) console.log(`๐Ÿ“‹ Page title: ${pageTitle}`) // Wait for login form console.log('โณ Waiting for login form...') await sleep(5000) // Look for email login option with comprehensive approach console.log('CHECKING: Looking for Email login option...') // Try to find email trigger button using XPath and text content let emailFormVisible = false try { // First try standard selectors const emailTriggers = [ 'button[data-overflow-tooltip-text="Email"]', '[data-name="email"]', '.tv-signin-dialog__toggle-email', '.js-signin__email', '[data-testid="email-signin"]', 'button[class*="email"]', '.signin-form__toggle' ] for (const trigger of emailTriggers) { try { const elements = await this.page.$$(trigger) for (const element of elements) { const isVisible = await element.boundingBox() if (isVisible) { const text = await element.evaluate(el => { const elem = el as HTMLElement return elem.textContent || elem.getAttribute('data-overflow-tooltip-text') || elem.getAttribute('title') || '' }) console.log(`TARGET: Found email trigger: ${trigger} with text: "${text}"`) await element.click() console.log('SUCCESS: Clicked email trigger') await sleep(3000) emailFormVisible = true break } } if (emailFormVisible) break } catch (e) { continue } } // If no standard selectors work, try finding buttons by text content if (!emailFormVisible) { console.log('๐Ÿ” Trying to find email button by text content...') const buttons = await this.page.$$('button, span, div[role="button"], a') for (const button of buttons) { try { const isVisible = await button.boundingBox() if (isVisible) { const text = await button.evaluate(el => { const elem = el as HTMLElement return (elem.textContent || '').toLowerCase().trim() }) if (text.includes('email') || text.includes('continue with email')) { console.log(`๐ŸŽฏ Found email button with text: "${text}"`) await button.click() console.log('โœ… Clicked email button by text') await sleep(3000) emailFormVisible = true break } } } catch (e) { continue } } } } catch (e) { console.log('โš ๏ธ Error finding email trigger:', e) } // If no email trigger found, check if we're in a modal or different login flow if (!emailFormVisible) { console.log('INFO: No email trigger found, checking for alternative login flows...') // Wait a bit more for any modals to fully load await sleep(5000) await this.takeDebugScreenshot('checking_modal_login') // Check for modal dialogs or different login containers const modalSelectors = [ '[role="dialog"]', '.tv-dialog', '.tv-dialog__wrapper', '.js-dialog', '.signin-dialog', '.auth-modal', '.modal', '[data-testid="auth-modal"]' ] for (const modalSelector of modalSelectors) { try { const modal = await this.page.$(modalSelector) if (modal) { console.log(`๐Ÿ“‹ Found modal: ${modalSelector}`) // Look for email options within the modal const modalEmailButtons = await modal.$$('button, a, div[role="button"]') for (const button of modalEmailButtons) { try { const isVisible = await button.boundingBox() if (isVisible) { const text = await button.evaluate(el => { const elem = el as HTMLElement return (elem.textContent || '').toLowerCase().trim() }) console.log(`๐ŸŽฏ Modal button text: "${text}"`) if (text.includes('email') || text.includes('continue with email') || text.includes('sign in with email') || text.includes('use email')) { console.log(`โœ… Found modal email button: "${text}"`) await button.click() console.log('โœ… Clicked modal email button') await sleep(3000) emailFormVisible = true break } } } catch (e) { continue } } if (emailFormVisible) break } } catch (e) { continue } } } if (!emailFormVisible) { console.log('INFO: Still no email form, assuming email form is already visible') } await this.takeDebugScreenshot('after_email_trigger') // Fill email - updated selectors for current TradingView const emailInputSelectors = [ 'input[type="email"]', 'input[name*="email"]', 'input[name="username"]', 'input[name="id_username"]', 'input[placeholder*="email" i]', 'input[placeholder*="Email" i]', 'input[autocomplete="username"]', 'input[data-name="email"]', 'input[class*="email"]', '#id_username', '#email', '[data-testid="email-input"]', '[data-testid="username-input"]' ] // Debug: Check what's actually on the page await this.takeDebugScreenshot('before_email_search') // Get all input elements to debug const allInputs = await this.page.$$('input') console.log(`DEBUG: Found ${allInputs.length} input elements on page`) for (let i = 0; i < Math.min(allInputs.length, 10); i++) { try { const inputType = await allInputs[i].evaluate(el => el.type) const inputName = await allInputs[i].evaluate(el => el.name) const inputPlaceholder = await allInputs[i].evaluate(el => el.placeholder) const inputId = await allInputs[i].evaluate(el => el.id) const inputClass = await allInputs[i].evaluate(el => el.className) const inputValue = await allInputs[i].evaluate(el => el.value) const isVisible = await allInputs[i].boundingBox() console.log(`INPUT ${i}: type="${inputType}", name="${inputName}", placeholder="${inputPlaceholder}", id="${inputId}", class="${inputClass}", value="${inputValue}", visible=${!!isVisible}`) // Check if this looks like an email/username field even if it doesn't match selectors if (isVisible && (inputType === 'text' || inputType === 'email' || !inputType)) { const lowerPlaceholder = (inputPlaceholder || '').toLowerCase() const lowerName = (inputName || '').toLowerCase() const lowerClass = (inputClass || '').toLowerCase() if (lowerPlaceholder.includes('email') || lowerPlaceholder.includes('username') || lowerName.includes('email') || lowerName.includes('username') || lowerClass.includes('email') || lowerClass.includes('username')) { console.log(`๐ŸŽฏ INPUT ${i} looks like email field based on attributes`) } } } catch (e) { const error = e as Error console.log(`INPUT ${i}: Error reading attributes - ${error.message}`) } } let emailInput = null // First try specific selectors for (const selector of emailInputSelectors) { try { emailInput = await this.page.$(selector) if (emailInput) { const isVisible = await emailInput.boundingBox() if (isVisible) { console.log('SUCCESS: Found email input: ' + selector) break } } } catch (e) { continue } emailInput = null // Reset if not visible } // If no specific selector worked, try to find the first visible text input if (!emailInput) { console.log('๐Ÿ” No specific email input found, trying first visible text input...') for (const input of allInputs) { try { const isVisible = await input.boundingBox() const inputType = await input.evaluate(el => el.type) if (isVisible && (inputType === 'text' || inputType === 'email' || !inputType)) { console.log('๐ŸŽฏ Found first visible text input, assuming it\'s email field') emailInput = input break } } catch (e) { continue } } } // Last resort: try to find inputs by looking for labels or surrounding text if (!emailInput) { console.log('๐Ÿ” Last resort: searching for inputs near email-related text...') // Look for text that says "email" and find nearby inputs const emailTexts = await this.page.evaluate(() => { const allElements = Array.from(document.querySelectorAll('*')) return allElements .filter(el => { const text = (el.textContent || '').toLowerCase() return text.includes('email') || text.includes('username') || text.includes('sign in') }) .map(el => { const rect = el.getBoundingClientRect() return { text: (el.textContent || '').trim(), x: rect.x, y: rect.y, tag: el.tagName } }) }) console.log('๐Ÿ“‹ Found email-related text elements:', emailTexts.slice(0, 5)) // Try to find any input that's not already found for (const input of allInputs) { try { const isVisible = await input.boundingBox() if (isVisible) { console.log('๐ŸŽฏ Using any visible input as potential email field') emailInput = input break } } catch (e) { continue } } } if (!emailInput) { // Additional debug info const currentUrl = await this.page.url() const title = await this.page.title() console.log(`โŒ Email input not found on: ${currentUrl}`) console.log(`โŒ Page title: ${title}`) await this.takeDebugScreenshot('email_input_not_found') throw new Error(`Could not find email input field on ${currentUrl}. Found ${allInputs.length} inputs total.`) } await emailInput.click() await emailInput.type(email) console.log('โœ… Filled email field') // Fill password const passwordInputSelectors = [ 'input[type="password"]', 'input[name*="password"]' ] let passwordInput = null for (const selector of passwordInputSelectors) { try { passwordInput = await this.page.$(selector) if (passwordInput) { const isVisible = await passwordInput.boundingBox() if (isVisible) { console.log('SUCCESS: Found password input: ' + selector) break } } } catch (e) { continue } } if (!passwordInput) { throw new Error('Could not find password input field') } await passwordInput.click() await passwordInput.type(password) console.log('โœ… Filled password field') // Submit form const submitSelectors = [ 'button[type="submit"]', 'button:contains("Sign in")', 'button:contains("Log in")', 'button:contains("Login")' ] let submitted = false for (const selector of submitSelectors) { try { const button = await this.page.$(selector) if (button) { const isVisible = await button.boundingBox() if (isVisible) { console.log('SUCCESS: Found submit button: ' + selector) await button.click() submitted = true break } } } catch (e) { continue } } if (!submitted) { // Try pressing Enter on password field await passwordInput.press('Enter') console.log('INFO: Pressed Enter on password field') } console.log('โณ Waiting for login completion...') await sleep(5000) // Check for errors const errorSelectors = [ '.tv-alert-dialog__text', '.tv-dialog__error', '[data-name="auth-error-message"]', '.error-message' ] for (const selector of errorSelectors) { try { const errorElement = await this.page.$(selector) if (errorElement) { const errorText = await this.page.evaluate(el => el.textContent, errorElement) if (errorText && errorText.trim()) { await this.takeDebugScreenshot('login_error') throw new Error('Login failed: ' + errorText.trim()) } } } catch (e) { continue } } // Verify login success await sleep(3000) const loginSuccess = await this.checkLoginStatus() if (loginSuccess) { console.log('โœ… Login successful!') this.isAuthenticated = true await this.saveSession() return true } else { await this.takeDebugScreenshot('login_verification_failed') throw new Error('Login verification failed - still appears not logged in') } } catch (error) { console.error('โŒ Login failed:', error) await this.takeDebugScreenshot('login_error') throw error } } async takeDebugScreenshot(prefix: string = 'debug'): Promise { if (!this.page) throw new Error('Page not initialized') try { const timestamp = Date.now() const filename = `${prefix}_${timestamp}.png` const filepath = path.join(process.cwd(), 'screenshots', filename) // Ensure screenshots directory exists await fs.mkdir(path.dirname(filepath), { recursive: true }) await this.page.screenshot({ path: filepath as `${string}.png`, fullPage: true, type: 'png' }) console.log(`๐Ÿ“ธ Screenshot saved: ${filename}`) return filepath } catch (error) { console.error('Error taking screenshot:', error) throw error } } async navigateToLayout(layoutId: string, symbol: string, timeframe?: string): Promise { if (!this.page) throw new Error('Page not initialized') try { console.log(`๐ŸŽฏ Navigating to layout ${layoutId} with symbol: ${symbol}`) // Construct direct layout URL - this bypasses the generic chart URL issue const directUrl = `https://www.tradingview.com/chart/${layoutId}/?symbol=${symbol}${timeframe ? `&interval=${timeframe}` : ''}` console.log(`๐Ÿ“ Direct layout navigation to: ${directUrl}`) await this.page.goto(directUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }) // Wait for chart to load await sleep(5000) // Wait for chart container await this.page.waitForSelector('.chart-container, #chart-container, [data-name="chart"]', { timeout: 30000 }) console.log(`โœ… Successfully navigated to layout ${layoutId}`) return true } catch (error) { console.error(`โŒ Failed to navigate to layout ${layoutId}:`, error) return false } } async navigateToSymbol(symbol: string, timeframe?: string): Promise { if (!this.page) throw new Error('Page not initialized') try { console.log(`๐ŸŽฏ Navigating to symbol: ${symbol}`) // Construct TradingView URL const baseUrl = 'https://www.tradingview.com/chart/' const params = new URLSearchParams() params.set('symbol', symbol) if (timeframe) { params.set('interval', timeframe) } const url = `${baseUrl}?${params.toString()}` console.log(`๐Ÿ“ Navigating to: ${url}`) await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }) // Wait for chart to load await sleep(5000) // Wait for chart container await this.page.waitForSelector('.chart-container, #chart-container, [data-name="chart"]', { timeout: 30000 }) console.log('โœ… Chart loaded successfully') return true } catch (error) { console.error('โŒ Failed to navigate to symbol:', error) throw error } } async takeScreenshot(options: { filename?: string, fullPage?: boolean } = {}): Promise { if (!this.page) throw new Error('Page not initialized') try { const timestamp = Date.now() const filename = options.filename || `screenshot_${timestamp}.png` const filepath = path.join(process.cwd(), 'screenshots', filename) // Ensure screenshots directory exists await fs.mkdir(path.dirname(filepath), { recursive: true }) await this.page.screenshot({ path: filepath as `${string}.png`, fullPage: options.fullPage || false, type: 'png' }) console.log(`๐Ÿ“ธ Screenshot saved: ${filename}`) return filepath } catch (error) { console.error('Error taking screenshot:', error) throw error } } } // Export default instance export const tradingViewAutomation = TradingViewAutomation.getInstance() export default TradingViewAutomation.getInstance()