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' ] }) 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) { await this.browser.close() } } catch (e) { console.log('WARNING: Error during browser cleanup:', e) } this.browser = null this.page = null this.isAuthenticated = 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) throw new Error('Page not initialized') console.log('CHECKING: Login status with 5 detection strategies...') try { // Strategy 1: Check for user account indicators (positive indicators) console.log('CHECKING: Strategy 1: 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 2: Check for anonymous/sign-in indicators (negative indicators) console.log('CHECKING: Strategy 2: 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 3: Check URL patterns console.log('CHECKING: Strategy 3: Checking URL patterns...') const currentUrl = this.page.url() if (currentUrl.includes('/signin') || currentUrl.includes('/login')) { console.log('ERROR: On login page - not logged in') return false } // Strategy 4: Check authentication cookies console.log('CHECKING: Strategy 4: 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 5: Check for personal content console.log('CHECKING: Strategy 5: 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 } } // 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 login page console.log('๐Ÿ“„ Navigating to TradingView login page...') await this.page.goto('https://www.tradingview.com/accounts/signin/', { waitUntil: 'domcontentloaded', timeout: 30000 }) await sleep(3000) await this.takeDebugScreenshot('login_page_loaded') // Wait for login form console.log('โณ Waiting for login form...') await sleep(5000) // Look for email login option console.log('CHECKING: Looking for Email login option...') const emailTriggers = [ 'button[data-overflow-tooltip-text="Email"]', 'button:contains("Email")', 'button:contains("email")', '[data-name="email"]' ] let emailFormVisible = false for (const trigger of emailTriggers) { try { const element = await this.page.$(trigger) if (element) { const isVisible = await element.boundingBox() if (isVisible) { console.log("TARGET: Found email trigger: " + trigger) await element.click() console.log('SUCCESS: Clicked email trigger') await sleep(3000) emailFormVisible = true break } } } catch (e) { continue } } // Fill email const emailInputSelectors = [ 'input[type="email"]', 'input[name*="email"]', 'input[name="username"]', 'input[placeholder*="email" i]' ] let emailInput = null 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 } } if (!emailInput) { throw new Error('Could not find email input field') } 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 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 default TradingViewAutomation.getInstance()