import { chromium, Browser, Page, BrowserContext } from 'playwright' import fs from 'fs/promises' import 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 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 context: BrowserContext | null = null private page: Page | null = null private isAuthenticated: boolean = false async init(): Promise { console.log('๐Ÿš€ Initializing TradingView automation with session persistence...') // Ensure session directory exists await fs.mkdir(SESSION_DATA_DIR, { recursive: true }) this.browser = await chromium.launch({ headless: true, // Must be true for Docker containers 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', '--metrics-recording-only', '--safebrowsing-disable-auto-update', '--disable-component-extensions-with-background-pages', '--disable-background-networking', '--disable-software-rasterizer', '--remote-debugging-port=9222', // Additional args to reduce captcha detection '--disable-blink-features=AutomationControlled', '--disable-features=VizDisplayCompositor,VizHitTestSurfaceLayer', '--disable-features=ScriptStreaming', '--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' ] }) if (!this.browser) { throw new Error('Failed to launch browser') } // Create browser context with session persistence this.context = await this.browser.newContext({ userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', viewport: { width: 1920, height: 1080 }, // Add additional headers to appear more human-like extraHTTPHeaders: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'gzip, deflate, br', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Upgrade-Insecure-Requests': '1' } }) if (!this.context) { throw new Error('Failed to create browser context') } // Load saved session if available await this.loadSession() this.page = await this.context.newPage() if (!this.page) { throw new Error('Failed to create new page') } // Add stealth measures to reduce bot detection await this.page.addInitScript(() => { // Override the navigator.webdriver property Object.defineProperty(navigator, 'webdriver', { get: () => undefined, }); // Mock plugins Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5], }); // Mock languages Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'], }); // Override permissions API to avoid detection const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters: any) => { if (parameters.name === 'notifications') { return Promise.resolve({ state: Notification.permission, name: parameters.name, onchange: null, addEventListener: () => {}, removeEventListener: () => {}, dispatchEvent: () => false } as PermissionStatus); } return originalQuery.call(window.navigator.permissions, parameters); }; }) console.log('โœ… Browser and session initialized successfully') } /** * Check if user is already logged in to TradingView */ async checkLoginStatus(): Promise { if (!this.page) return false try { console.log('๐Ÿ” Checking login status...') // Navigate to TradingView if not already there const currentUrl = await this.page.url() if (!currentUrl.includes('tradingview.com')) { console.log('๐Ÿ“„ Navigating to TradingView...') await this.page.goto('https://www.tradingview.com', { waitUntil: 'domcontentloaded', timeout: 30000 }) // Restore session storage after navigation await this.restoreSessionStorage() // Wait for page to settle await this.page.waitForTimeout(3000) } // Check for login indicators const loginIndicators = [ '[data-name="watchlist-button"]', '.tv-header__watchlist-button', '.tv-header__user-menu-button', 'button:has-text("M")', '.js-header-user-menu-button', '[data-name="user-menu"]' ] for (const selector of loginIndicators) { try { if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { console.log(`โœ… Found login indicator: ${selector}`) this.isAuthenticated = true return true } } catch (e) { continue } } // Additional check: look for sign-in buttons (indicates not logged in) const signInSelectors = [ 'a[href*="signin"]', 'button:has-text("Sign in")', '.tv-header__user-menu-button--anonymous' ] for (const selector of signInSelectors) { try { if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { console.log(`โŒ Found sign-in button: ${selector} - not logged in`) this.isAuthenticated = false return false } } catch (e) { continue } } console.log('๐Ÿค” Login status unclear, will attempt login') this.isAuthenticated = false return false } catch (error) { console.error('โŒ Error checking login status:', error) this.isAuthenticated = false return false } } async login(credentials?: TradingViewCredentials): Promise { if (!this.page) throw new Error('Page not initialized') // Use provided credentials or fall back to environment variables const email = credentials?.email || TRADINGVIEW_EMAIL const password = credentials?.password || TRADINGVIEW_PASSWORD if (!email || !password) { throw new Error('TradingView credentials not provided. Either pass credentials parameter or set TRADINGVIEW_EMAIL and TRADINGVIEW_PASSWORD in .env file') } try { // Check if already logged in const loggedIn = await this.checkLoginStatus() if (loggedIn) { console.log('โœ… Already logged in, skipping login steps') return true } console.log('Navigating to TradingView login page...') // Try different login URLs that TradingView might use const loginUrls = [ 'https://www.tradingview.com/accounts/signin/', 'https://www.tradingview.com/sign-in/', 'https://www.tradingview.com/' ] let loginPageLoaded = false for (const url of loginUrls) { try { console.log(`Trying login URL: ${url}`) await this.page.goto(url, { waitUntil: 'networkidle', timeout: 30000 }) // Check if we're on the login page or need to navigate to it const currentUrl = await this.page.url() console.log('Current URL after navigation:', currentUrl) if (currentUrl.includes('signin') || currentUrl.includes('login')) { loginPageLoaded = true break } else if (url === 'https://www.tradingview.com/') { // If we're on the main page, try to find and click the Sign In button console.log('On main page, looking for Sign In button...') const signInSelectors = [ 'a[href*="signin"]', 'a:has-text("Sign in")', 'button:has-text("Sign in")', '.tv-header__user-menu-button--anonymous', '[data-name="header-user-menu-sign-in"]', '.js-signin-button' ] for (const selector of signInSelectors) { try { console.log(`Trying sign in selector: ${selector}`) await this.page.waitForSelector(selector, { timeout: 3000 }) await this.page.click(selector) await this.page.waitForLoadState('networkidle', { timeout: 10000 }) const newUrl = await this.page.url() if (newUrl.includes('signin') || newUrl.includes('login')) { console.log('Successfully navigated to login page via sign in button') loginPageLoaded = true break } } catch (e) { console.log(`Sign in selector ${selector} not found or failed`) } } if (loginPageLoaded) break } } catch (e) { console.log(`Failed to load ${url}:`, e) } } if (!loginPageLoaded) { console.log('Could not reach login page, trying to proceed anyway...') } // Take a screenshot to debug the current page await this.takeDebugScreenshot('page_loaded') // Wait for page to settle and dynamic content to load await this.page.waitForTimeout(5000) // Log current URL and page title for debugging const currentUrl = await this.page.url() const pageTitle = await this.page.title() console.log('Current URL:', currentUrl) console.log('Page title:', pageTitle) // Check if we got redirected or are on an unexpected page if (!currentUrl.includes('tradingview.com')) { console.log('WARNING: Not on TradingView domain!') await this.takeDebugScreenshot('wrong_domain') } // Log page content length and check for common elements const bodyContent = await this.page.textContent('body') console.log('Page content length:', bodyContent?.length || 0) console.log('Page content preview:', bodyContent?.substring(0, 500) || 'No content') // Check for iframes that might contain the login form const iframes = await this.page.$$('iframe') console.log('Number of iframes found:', iframes.length) if (iframes.length > 0) { for (let i = 0; i < iframes.length; i++) { const src = await iframes[i].getAttribute('src') console.log(`Iframe ${i} src:`, src) } } // Wait for any dynamic content to load try { // Wait for form or login-related elements to appear await Promise.race([ this.page.waitForSelector('form', { timeout: 10000 }), this.page.waitForSelector('input[type="email"]', { timeout: 10000 }), this.page.waitForSelector('input[type="text"]', { timeout: 10000 }), this.page.waitForSelector('input[name*="email"]', { timeout: 10000 }), this.page.waitForSelector('input[name*="username"]', { timeout: 10000 }) ]) console.log('Form elements detected, proceeding...') } catch (e) { console.log('No form elements detected within timeout, continuing anyway...') } // Check for common login-related elements const loginElements = await this.page.$$eval('*', (elements: Element[]) => { const found = [] for (const el of elements) { const text = el.textContent?.toLowerCase() || '' if (text.includes('login') || text.includes('sign in') || text.includes('email') || text.includes('username')) { found.push({ tagName: el.tagName, text: text.substring(0, 100), className: el.className, id: el.id }) } } return found.slice(0, 10) // Limit to first 10 matches }) console.log('Login-related elements found:', JSON.stringify(loginElements, null, 2)) // CRITICAL FIX: TradingView requires clicking "Email" button to show login form console.log('๐Ÿ” Looking for Email login trigger button...') try { // Wait for the "Email" button to appear and click it const emailButton = this.page.locator('text="Email"').first() await emailButton.waitFor({ state: 'visible', timeout: 10000 }) console.log('โœ… Found Email button, clicking...') await emailButton.click() console.log('๐Ÿ–ฑ๏ธ Clicked Email button successfully') // Wait for login form to appear after clicking await this.page.waitForTimeout(3000) console.log('โณ Waiting for login form to appear...') } catch (error) { console.log(`โŒ Could not find or click Email button: ${error}`) // Fallback: try other possible email triggers const emailTriggers = [ 'button:has-text("Email")', 'button:has-text("email")', '[data-name="email"]', 'text="Sign in with email"', 'text="Continue with email"' ] let triggerFound = false for (const trigger of emailTriggers) { try { const element = this.page.locator(trigger).first() if (await element.isVisible({ timeout: 2000 })) { console.log(`๐Ÿ”„ Trying fallback trigger: ${trigger}`) await element.click() await this.page.waitForTimeout(2000) triggerFound = true break } } catch (e) { continue } } if (!triggerFound) { await this.takeDebugScreenshot('no_email_trigger') throw new Error('Could not find Email button or trigger to show login form') } } // Try to find email input with various selectors console.log('Looking for email input field...') const emailSelectors = [ // TradingView specific selectors (discovered through debugging) - PRIORITY 'input[name="id_username"]', // Standard selectors 'input[name="username"]', 'input[type="email"]', 'input[data-name="email"]', 'input[placeholder*="email" i]', 'input[placeholder*="username" i]', 'input[id*="email" i]', 'input[id*="username" i]', 'input[class*="email" i]', 'input[class*="username" i]', 'input[data-testid*="email" i]', 'input[data-testid*="username" i]', 'input[name*="email" i]', 'input[name*="user" i]', 'form input[type="text"]', 'form input:not([type="password"]):not([type="hidden"])', '.signin-form input[type="text"]', '.login-form input[type="text"]', '[data-role="email"] input', '[data-role="username"] input', // More TradingView specific selectors 'input[autocomplete="username"]', 'input[autocomplete="email"]', '.tv-signin-dialog input[type="text"]', '.tv-signin-dialog input[type="email"]', '#id_username', '#email', '#username', 'input[data-test="username"]', 'input[data-test="email"]' ] let emailInput = null // First pass: Try selectors with timeout for (const selector of emailSelectors) { try { console.log(`Trying email selector: ${selector}`) await this.page.waitForSelector(selector, { timeout: 2000 }) const isVisible = await this.page.isVisible(selector) if (isVisible) { emailInput = selector console.log(`Found email input with selector: ${selector}`) break } } catch (e) { console.log(`Email selector ${selector} not found or not visible`) } } // Second pass: If no input found, check all visible inputs if (!emailInput) { console.log('No email input found with standard selectors. Checking all visible inputs...') try { const visibleInputs = await this.page.$$eval('input', (inputs: HTMLInputElement[]) => inputs .filter((input: HTMLInputElement) => { const style = window.getComputedStyle(input) return style.display !== 'none' && style.visibility !== 'hidden' && input.offsetParent !== null }) .map((input: HTMLInputElement, index: number) => ({ index, type: input.type, name: input.name, id: input.id, className: input.className, placeholder: input.placeholder, 'data-name': input.getAttribute('data-name'), 'data-testid': input.getAttribute('data-testid'), 'autocomplete': input.getAttribute('autocomplete'), outerHTML: input.outerHTML.substring(0, 300) })) ) console.log('Visible inputs found:', JSON.stringify(visibleInputs, null, 2)) // Try to find the first visible text or email input if (visibleInputs.length > 0) { const usernameInput = visibleInputs.find((input: any) => input.type === 'email' || input.type === 'text' || input.name?.toLowerCase().includes('user') || input.name?.toLowerCase().includes('email') || input.placeholder?.toLowerCase().includes('email') || input.placeholder?.toLowerCase().includes('user') ) if (usernameInput) { // Create selector for this input if (usernameInput.id) { emailInput = `#${usernameInput.id}` } else if (usernameInput.name) { emailInput = `input[name="${usernameInput.name}"]` } else { emailInput = `input:nth-of-type(${usernameInput.index + 1})` } console.log(`Using detected email input: ${emailInput}`) } } } catch (e) { console.log('Error analyzing visible inputs:', e) } } if (!emailInput) { console.log('No email input found. Logging all input elements on page...') const allInputs = await this.page.$$eval('input', (inputs: HTMLInputElement[]) => inputs.map((input: HTMLInputElement) => ({ type: input.type, name: input.name, id: input.id, className: input.className, placeholder: input.placeholder, 'data-name': input.getAttribute('data-name'), 'data-testid': input.getAttribute('data-testid'), outerHTML: input.outerHTML.substring(0, 200) })) ) console.log('All inputs found:', JSON.stringify(allInputs, null, 2)) await this.takeDebugScreenshot('no_email_input') throw new Error('Could not find email input field') } // Fill email console.log('Filling email field...') await this.page.fill(emailInput, email) // Try to find password input with various selectors console.log('Looking for password input field...') const passwordSelectors = [ // TradingView specific selectors (discovered through debugging) - PRIORITY 'input[name="id_password"]', // Standard selectors 'input[name="password"]', 'input[type="password"]', 'input[data-name="password"]', 'input[placeholder*="password" i]', 'input[id*="password" i]', 'input[class*="password" i]', 'input[data-testid*="password" i]', 'form input[type="password"]', '.signin-form input[type="password"]', '.login-form input[type="password"]', '[data-role="password"] input', // More TradingView specific selectors 'input[autocomplete="current-password"]', '.tv-signin-dialog input[type="password"]', '#id_password', '#password', 'input[data-test="password"]' ] let passwordInput = null // First pass: Try selectors with timeout for (const selector of passwordSelectors) { try { console.log(`Trying password selector: ${selector}`) await this.page.waitForSelector(selector, { timeout: 2000 }) const isVisible = await this.page.isVisible(selector) if (isVisible) { passwordInput = selector console.log(`Found password input with selector: ${selector}`) break } } catch (e) { console.log(`Password selector ${selector} not found or not visible`) } } // Second pass: If no password input found, look for any visible password field if (!passwordInput) { console.log('No password input found with standard selectors. Checking all password inputs...') try { const passwordInputs = await this.page.$$eval('input[type="password"]', (inputs: HTMLInputElement[]) => inputs .filter((input: HTMLInputElement) => { const style = window.getComputedStyle(input) return style.display !== 'none' && style.visibility !== 'hidden' && input.offsetParent !== null }) .map((input: HTMLInputElement, index: number) => ({ index, name: input.name, id: input.id, className: input.className, placeholder: input.placeholder, outerHTML: input.outerHTML.substring(0, 300) })) ) console.log('Password inputs found:', JSON.stringify(passwordInputs, null, 2)) if (passwordInputs.length > 0) { const firstPassword = passwordInputs[0] if (firstPassword.id) { passwordInput = `#${firstPassword.id}` } else if (firstPassword.name) { passwordInput = `input[name="${firstPassword.name}"]` } else { passwordInput = `input[type="password"]:nth-of-type(${firstPassword.index + 1})` } console.log(`Using detected password input: ${passwordInput}`) } } catch (e) { console.log('Error analyzing password inputs:', e) } } if (!passwordInput) { console.log('No password input found. Taking debug screenshot...') await this.takeDebugScreenshot('no_password_field') throw new Error('Could not find password input field') } // Fill password console.log('Filling password field...') await this.page.fill(passwordInput, password) // Handle potential captcha console.log('Checking for captcha...') try { const captchaFrame = this.page.frameLocator('iframe[src*="recaptcha"]').first() const captchaCheckbox = captchaFrame.locator('div.recaptcha-checkbox-border') if (await captchaCheckbox.isVisible({ timeout: 3000 })) { console.log('Captcha detected, clicking checkbox...') await captchaCheckbox.click() // Wait a bit for captcha to process await this.page.waitForTimeout(5000) // Check if captcha is solved const isSolved = await captchaFrame.locator('.recaptcha-checkbox-checked').isVisible({ timeout: 10000 }) if (!isSolved) { console.log('Captcha may require manual solving. Waiting 15 seconds...') await this.page.waitForTimeout(15000) } } } catch (captchaError: any) { console.log('No captcha found or captcha handling failed:', captchaError?.message || 'Unknown error') } // Find and click sign in button console.log('Looking for sign in button...') const submitSelectors = [ 'button[type="submit"]', 'button:has-text("Sign in")', 'button:has-text("Sign In")', 'button:has-text("Log in")', 'button:has-text("Log In")', 'button:has-text("Login")', '.tv-button--primary', 'input[type="submit"]', '[data-testid="signin-button"]', '[data-testid="login-button"]', '.signin-button', '.login-button', 'form button', 'button[class*="submit"]', 'button[class*="signin"]', 'button[class*="login"]' ] let submitButton = null for (const selector of submitSelectors) { try { console.log(`Trying submit selector: ${selector}`) const element = this.page.locator(selector) if (await element.isVisible({ timeout: 2000 })) { submitButton = selector console.log(`Found submit button with selector: ${selector}`) break } } catch (e) { console.log(`Submit selector ${selector} not found`) } } if (!submitButton) { console.log('No submit button found. Taking debug screenshot...') await this.takeDebugScreenshot('no_submit_button') // Log all buttons on the page const allButtons = await this.page.$$eval('button', (buttons: HTMLButtonElement[]) => buttons.map((button: HTMLButtonElement) => ({ type: button.type, textContent: button.textContent?.trim(), className: button.className, id: button.id, outerHTML: button.outerHTML.substring(0, 200) })) ) console.log('All buttons found:', JSON.stringify(allButtons, null, 2)) throw new Error('Could not find submit button') } console.log('Clicking sign in button...') await this.page.click(submitButton) // Wait for successful login - look for the "M" watchlist indicator console.log('Waiting for login success indicators...') try { // Wait for any of these success indicators await Promise.race([ this.page.waitForSelector('[data-name="watchlist-button"], .tv-header__watchlist-button, button:has-text("M")', { timeout: 20000 }), this.page.waitForSelector('.tv-header__user-menu-button, .js-header-user-menu-button', { timeout: 20000 }), this.page.waitForSelector('.tv-header__logo', { timeout: 20000 }) ]) // Additional check - make sure we're not still on login page await this.page.waitForFunction( () => !window.location.href.includes('/accounts/signin/'), { timeout: 10000 } ) console.log('Login successful!') this.isAuthenticated = true // Save session after successful login await this.saveSession() return true } catch (error) { console.error('Login verification failed:', error) // Take a debug screenshot await this.takeDebugScreenshot('login_failed') return false } } catch (error) { console.error('Login failed:', error) await this.takeDebugScreenshot('login_error') return false } } /** * Smart login that prioritizes session persistence to avoid captchas */ async smartLogin(credentials?: TradingViewCredentials): Promise { if (!this.page) throw new Error('Page not initialized') try { console.log('๐Ÿ” Attempting smart login with session persistence...') // First check if already logged in const alreadyLoggedIn = await this.checkLoginStatus() if (alreadyLoggedIn) { console.log('๐ŸŽ‰ Already logged in to TradingView! Skipping login process.') await this.saveSession() // Save current session return true } console.log('๐Ÿ” Not logged in, proceeding with manual login...') console.log('โš ๏ธ IMPORTANT: Manual intervention required due to captcha protection.') console.log('๐Ÿ“ฑ Please log in manually in a browser and the session will be saved for future use.') // Navigate to login page for manual login await this.page.goto('https://www.tradingview.com/accounts/signin/', { waitUntil: 'domcontentloaded', timeout: 30000 }) // Wait and give user time to manually complete login console.log('โณ Waiting for manual login completion...') console.log('๐Ÿ’ก You have 2 minutes to complete login manually in the browser.') // Check every 10 seconds for login completion let attempts = 0 const maxAttempts = 12 // 2 minutes while (attempts < maxAttempts) { await this.page.waitForTimeout(10000) // Wait 10 seconds attempts++ console.log(`๐Ÿ”„ Checking login status (attempt ${attempts}/${maxAttempts})...`) const loggedIn = await this.checkLoginStatus() if (loggedIn) { console.log('โœ… Manual login detected! Saving session for future use.') this.isAuthenticated = true await this.saveSession() return true } } console.log('โฐ Timeout waiting for manual login.') return false } catch (error) { console.error('โŒ Smart login failed:', error) return false } } async navigateToChart(options: NavigationOptions = {}): Promise { if (!this.page) throw new Error('Page not initialized') try { const { symbol = 'SOLUSD', timeframe = '5', waitForChart = true } = options console.log('Navigating to chart page...') // Wait a bit after login before navigating await this.page.waitForTimeout(2000) // Navigate to chart page with more flexible waiting strategy try { await this.page.goto('https://www.tradingview.com/chart/', { waitUntil: 'domcontentloaded', // More lenient than 'networkidle' timeout: 45000 // Increased timeout }) } catch (error) { console.log('Standard navigation failed, trying fallback...') // Fallback: navigate without waiting for full network idle await this.page.goto('https://www.tradingview.com/chart/', { waitUntil: 'load', timeout: 30000 }) } // Wait for chart to load if (waitForChart) { console.log('Waiting for chart container...') try { await this.page.waitForSelector('.chart-container, #tv_chart_container, .tv-layout', { timeout: 30000 }) console.log('Chart container found') } catch (error) { console.log('Chart container not found with standard selectors, trying alternatives...') // Try alternative selectors for chart elements const chartSelectors = [ '.chart-widget', '.tradingview-widget-container', '[data-name="chart"]', 'canvas', '.tv-chart' ] let chartFound = false for (const selector of chartSelectors) { try { await this.page.waitForSelector(selector, { timeout: 5000 }) console.log(`Chart found with selector: ${selector}`) chartFound = true break } catch (e) { continue } } if (!chartFound) { console.log('No chart container found, proceeding anyway...') } } // Additional wait for chart initialization await this.page.waitForTimeout(5000) } // Change symbol if not BTC if (symbol !== 'BTCUSD') { console.log(`Changing symbol to ${symbol}...`) await this.changeSymbol(symbol) } // Change timeframe if specified if (timeframe) { console.log(`Setting timeframe to ${timeframe}...`) await this.changeTimeframe(timeframe) } console.log(`Successfully navigated to ${symbol} chart with ${timeframe} timeframe`) return true } catch (error) { console.error('Navigation to chart failed:', error) await this.takeDebugScreenshot('navigation_failed') return false } } private async changeSymbol(symbol: string): Promise { if (!this.page) return try { // Try multiple selectors for the symbol searcher const symbolSelectors = [ '.tv-symbol-header__short-title', '.js-symbol-title', '[data-name="legend-source-title"]', '.tv-symbol-header', '.tv-chart-header__symbol' ] let symbolElement = null for (const selector of symbolSelectors) { try { await this.page.waitForSelector(selector, { timeout: 3000 }) symbolElement = selector break } catch (e) { console.log(`Symbol selector ${selector} not found, trying next...`) } } if (!symbolElement) { throw new Error('Could not find symbol selector') } await this.page.click(symbolElement) // Wait for search input const searchSelectors = [ 'input[data-role="search"]', '.tv-dialog__body input', '.tv-symbol-search-dialog__input input', 'input[placeholder*="Search"]' ] let searchInput = null for (const selector of searchSelectors) { try { await this.page.waitForSelector(selector, { timeout: 3000 }) searchInput = selector break } catch (e) { console.log(`Search input selector ${selector} not found, trying next...`) } } if (!searchInput) { throw new Error('Could not find search input') } // Clear and type new symbol await this.page.fill(searchInput, symbol) // Wait a bit for search results await this.page.waitForTimeout(2000) // Try to click first result or press Enter const resultSelectors = [ '.tv-screener-table__row', '.js-searchbar-suggestion', '.tv-symbol-search-dialog__item', '.tv-symbol-search-dialog__symbol' ] let clicked = false for (const selector of resultSelectors) { try { const firstResult = this.page.locator(selector).first() if (await firstResult.isVisible({ timeout: 2000 })) { await firstResult.click() clicked = true break } } catch (e) { console.log(`Result selector ${selector} not found, trying next...`) } } if (!clicked) { console.log('No result found, pressing Enter...') await this.page.press(searchInput, 'Enter') } // Wait for symbol to change await this.page.waitForTimeout(3000) } catch (error) { console.error('Failed to change symbol:', error) await this.takeDebugScreenshot('symbol_change_failed') } } private async changeTimeframe(timeframe: string): Promise { if (!this.page) return try { console.log(`Attempting to change timeframe to: ${timeframe}`) // Wait for chart to be ready await this.page.waitForTimeout(3000) // Map common timeframe values to TradingView format const timeframeMap: { [key: string]: string[] } = { '1': ['1', '1m', '1min'], '5': ['5', '5m', '5min'], '15': ['15', '15m', '15min'], '30': ['30', '30m', '30min'], '60': ['1h', '1H', '60', '60m', '60min'], // Prioritize 1h format '240': ['4h', '4H', '240', '240m'], '1D': ['1D', 'D', 'daily'], '1W': ['1W', 'W', 'weekly'] } // Get possible timeframe values to try const timeframesToTry = timeframeMap[timeframe] || [timeframe] console.log(`Will try these timeframe values: ${timeframesToTry.join(', ')}`) let found = false // Take a screenshot to see current timeframe bar await this.takeDebugScreenshot('before_timeframe_change') // CRITICAL: Click the interval legend to open timeframe selector console.log('๐ŸŽฏ Looking for interval legend to open timeframe selector...') const intervalLegendSelectors = [ '[data-name="legend-source-interval"]', '.intervalTitle-l31H9iuA', '[title="Change interval"]', '.intervalTitle-l31H9iuA button', '[data-name="legend-source-interval"] button' ] let intervalLegendClicked = false for (const selector of intervalLegendSelectors) { try { console.log(`Trying interval legend selector: ${selector}`) const element = this.page.locator(selector).first() if (await element.isVisible({ timeout: 3000 })) { console.log(`โœ… Found interval legend: ${selector}`) await element.click() await this.page.waitForTimeout(2000) console.log('๐Ÿ–ฑ๏ธ Clicked interval legend - timeframe selector should be open') intervalLegendClicked = true break } } catch (e) { console.log(`Interval legend selector ${selector} not found`) } } if (!intervalLegendClicked) { console.log('โŒ Could not find interval legend to click') await this.takeDebugScreenshot('no_interval_legend') return } // Now look for timeframe options in the opened selector console.log('๐Ÿ” Looking for timeframe options in selector...') for (const tf of timeframesToTry) { const timeframeSelectors = [ // After clicking interval legend, look for options `[data-value="${tf}"]`, `button:has-text("${tf}")`, `.tv-dropdown__item:has-text("${tf}")`, `.tv-interval-item:has-text("${tf}")`, `[title="${tf}"]`, `[aria-label*="${tf}"]`, // Look in the opened dropdown/menu `.tv-dropdown-behavior__body [data-value="${tf}"]`, `.tv-dropdown-behavior__body button:has-text("${tf}")`, // Look for list items or menu items `li:has-text("${tf}")`, `div[role="option"]:has-text("${tf}")`, `[role="menuitem"]:has-text("${tf}")`, // TradingView specific interval selectors `.tv-screener-table__row:has-text("${tf}")`, `.tv-interval-tabs button:has-text("${tf}")`, `.intervals-GwQQdU8S [data-value="${tf}"]`, // Generic selectors in visible containers `.tv-dialog [data-value="${tf}"]`, `.tv-dialog button:has-text("${tf}")` ] for (const selector of timeframeSelectors) { try { console.log(`Trying timeframe option selector: ${selector}`) const element = this.page.locator(selector).first() // Check if element exists and is visible const isVisible = await element.isVisible({ timeout: 2000 }) if (isVisible) { console.log(`โœ… Found timeframe option: ${selector}`) await element.click() await this.page.waitForTimeout(2000) console.log(`๐ŸŽ‰ Successfully clicked timeframe option for ${tf}`) found = true break } } catch (e) { console.log(`Timeframe option selector ${selector} not found or not clickable`) } } if (found) break } // Fallback: Try keyboard navigation if (!found) { console.log('๐Ÿ”„ Timeframe options not found, trying keyboard navigation...') // Try pressing specific keys for common timeframes const keyMap: { [key: string]: string } = { '60': '1', // Often 1h is mapped to '1' key '1': '1', '5': '5', '15': '1', '30': '3', '240': '4', '1D': 'D' } if (keyMap[timeframe]) { console.log(`๐ŸŽน Trying keyboard shortcut: ${keyMap[timeframe]}`) await this.page.keyboard.press(keyMap[timeframe]) await this.page.waitForTimeout(1000) found = true } } if (found) { console.log(`โœ… Successfully changed timeframe to ${timeframe}`) await this.takeDebugScreenshot('after_timeframe_change') } else { console.log(`โŒ Could not change timeframe to ${timeframe} - timeframe options not found`) // Take a debug screenshot to see current state await this.takeDebugScreenshot('timeframe_change_failed') // Log all visible elements that might be timeframe related try { const visibleElements = await this.page.$$eval('[data-value], button, [role="option"], [role="menuitem"], li', (elements: Element[]) => elements .filter((el: Element) => { const style = window.getComputedStyle(el) return style.display !== 'none' && style.visibility !== 'hidden' }) .slice(0, 20) .map((el: Element) => ({ tagName: el.tagName, text: el.textContent?.trim().substring(0, 20), className: el.className.substring(0, 50), dataValue: el.getAttribute('data-value'), role: el.getAttribute('role'), outerHTML: el.outerHTML.substring(0, 150) })) ) console.log('Visible interactive elements:', JSON.stringify(visibleElements, null, 2)) } catch (e) { console.log('Could not analyze visible elements') } } } catch (error) { console.error('Failed to change timeframe:', error) await this.takeDebugScreenshot('timeframe_change_error') } } async takeScreenshot(filename: string): Promise { if (!this.page) throw new Error('Page not initialized') const screenshotsDir = path.join(process.cwd(), 'screenshots') await fs.mkdir(screenshotsDir, { recursive: true }) const fullPath = path.join(screenshotsDir, filename) await this.page.screenshot({ path: fullPath, fullPage: false, // Only visible area type: 'png' }) console.log(`Screenshot saved: ${filename}`) return filename } private async takeDebugScreenshot(prefix: string): Promise { try { const timestamp = Date.now() const filename = `debug_${prefix}_${timestamp}.png` await this.takeScreenshot(filename) } catch (error) { console.error('Failed to take debug screenshot:', error) } } async close(): Promise { // Save session data before closing if (this.isAuthenticated) { await this.saveSession() } if (this.page) { await this.page.close() this.page = null } if (this.context) { await this.context.close() this.context = null } if (this.browser) { await this.browser.close() this.browser = null } } /** * Load saved session data (cookies, localStorage, etc.) */ private async loadSession(): Promise { try { console.log('๐Ÿ”„ Loading saved session data...') // Load cookies if (await this.fileExists(COOKIES_FILE)) { const cookiesData = await fs.readFile(COOKIES_FILE, 'utf8') const cookies = JSON.parse(cookiesData) await this.context!.addCookies(cookies) console.log(`โœ… Loaded ${cookies.length} cookies from saved session`) } // Note: Session storage will be loaded after page navigation } catch (error) { console.log('โš ๏ธ Could not load session data (starting fresh):', error) } } /** * Save current session data for future use */ private async saveSession(): Promise { try { console.log('๐Ÿ’พ Saving session data...') if (!this.context || !this.page) return // Save cookies const cookies = await this.context.cookies() await fs.writeFile(COOKIES_FILE, JSON.stringify(cookies, null, 2)) console.log(`โœ… Saved ${cookies.length} cookies`) // Save session storage and localStorage const sessionData = await this.page.evaluate(() => { const localStorage: { [key: string]: string | null } = {} const sessionStorage: { [key: string]: string | null } = {} // Extract localStorage for (let i = 0; i < window.localStorage.length; i++) { const key = window.localStorage.key(i) if (key) { localStorage[key] = window.localStorage.getItem(key) } } // Extract sessionStorage for (let i = 0; i < window.sessionStorage.length; i++) { const key = window.sessionStorage.key(i) if (key) { sessionStorage[key] = window.sessionStorage.getItem(key) } } return { localStorage, sessionStorage } }) await fs.writeFile(SESSION_STORAGE_FILE, JSON.stringify(sessionData, null, 2)) console.log('โœ… Saved session storage and localStorage') } catch (error) { console.error('โŒ Failed to save session data:', error) } } /** * Restore session storage and localStorage */ private async restoreSessionStorage(): Promise { try { if (!this.page || !await this.fileExists(SESSION_STORAGE_FILE)) return const sessionData = JSON.parse(await fs.readFile(SESSION_STORAGE_FILE, 'utf8')) await this.page.evaluate((data) => { // Restore localStorage if (data.localStorage) { for (const [key, value] of Object.entries(data.localStorage)) { try { window.localStorage.setItem(key, value as string) } catch (e) { console.log('Could not restore localStorage item:', key) } } } // Restore sessionStorage if (data.sessionStorage) { for (const [key, value] of Object.entries(data.sessionStorage)) { try { window.sessionStorage.setItem(key, value as string) } catch (e) { console.log('Could not restore sessionStorage item:', key) } } } }, sessionData) console.log('โœ… Restored session storage and localStorage') } catch (error) { console.log('โš ๏ธ Could not restore session storage:', error) } } /** * Refresh session to keep it alive */ async refreshSession(): Promise { if (!this.page || !this.isAuthenticated) return false try { console.log('๐Ÿ”„ Refreshing TradingView session...') // Just reload the current page to refresh session await this.page.reload({ waitUntil: 'domcontentloaded', timeout: 30000 }) // Wait for page to settle await this.page.waitForTimeout(2000) // Verify still logged in const stillLoggedIn = await this.checkLoginStatus() if (stillLoggedIn) { console.log('โœ… Session refreshed successfully') await this.saveSession() // Save refreshed session return true } else { console.log('โŒ Session expired during refresh') this.isAuthenticated = false return false } } catch (error) { console.error('โŒ Failed to refresh session:', error) return false } } /** * Clear all saved session data */ async clearSession(): Promise { try { console.log('๐Ÿ—‘๏ธ Clearing saved session data...') if (await this.fileExists(COOKIES_FILE)) { await fs.unlink(COOKIES_FILE) console.log('โœ… Cleared cookies file') } if (await this.fileExists(SESSION_STORAGE_FILE)) { await fs.unlink(SESSION_STORAGE_FILE) console.log('โœ… Cleared session storage file') } // Clear browser context storage if available if (this.context) { await this.context.clearCookies() console.log('โœ… Cleared browser context cookies') } this.isAuthenticated = false console.log('โœ… Session data cleared successfully') } catch (error) { console.error('โŒ Failed to clear session data:', error) } } /** * Get session status information */ async getSessionInfo(): Promise<{ isAuthenticated: boolean hasSavedCookies: boolean hasSavedStorage: boolean cookiesCount: number currentUrl: string }> { const hasSavedCookies = await this.fileExists(COOKIES_FILE) const hasSavedStorage = await this.fileExists(SESSION_STORAGE_FILE) let cookiesCount = 0 if (hasSavedCookies) { try { const cookiesData = await fs.readFile(COOKIES_FILE, 'utf8') const cookies = JSON.parse(cookiesData) cookiesCount = cookies.length } catch (e) { // Ignore error } } const currentUrl = this.page ? await this.page.url() : '' return { isAuthenticated: this.isAuthenticated, hasSavedCookies, hasSavedStorage, cookiesCount, currentUrl } } /** * Test session persistence by checking if saved session data exists and is valid */ async testSessionPersistence(): Promise<{ hasSessionData: boolean isValid: boolean sessionInfo: any }> { try { console.log('๐Ÿงช Testing session persistence...') const sessionInfo = await this.getSessionInfo() console.log('๐Ÿ“Š Current session info:', sessionInfo) if (!sessionInfo.hasSavedCookies && !sessionInfo.hasSavedStorage) { console.log('โŒ No saved session data found') return { hasSessionData: false, isValid: false, sessionInfo } } console.log('โœ… Saved session data found') console.log(`๐Ÿช Cookies: ${sessionInfo.cookiesCount}`) console.log(`๐Ÿ’พ Storage: ${sessionInfo.hasSavedStorage ? 'Yes' : 'No'}`) // Try to use the session if (this.page) { // Navigate to TradingView to test session validity await this.page.goto('https://www.tradingview.com', { waitUntil: 'domcontentloaded', timeout: 30000 }) // Restore session storage await this.restoreSessionStorage() // Check if session is still valid const isLoggedIn = await this.checkLoginStatus() if (isLoggedIn) { console.log('๐ŸŽ‰ Session is valid and user is logged in!') return { hasSessionData: true, isValid: true, sessionInfo } } else { console.log('โš ๏ธ Session data exists but appears to be expired') return { hasSessionData: true, isValid: false, sessionInfo } } } return { hasSessionData: true, isValid: false, sessionInfo } } catch (error) { console.error('โŒ Session persistence test failed:', error) return { hasSessionData: false, isValid: false, sessionInfo: null } } } // Utility method to wait for chart data to load async waitForChartData(timeout: number = 15000): Promise { if (!this.page) return false try { console.log('Waiting for chart data to load...') // Wait for chart canvas or chart elements to be present await Promise.race([ this.page.waitForSelector('canvas', { timeout }), this.page.waitForSelector('.tv-lightweight-charts', { timeout }), this.page.waitForSelector('.tv-chart-view', { timeout }) ]) // Additional wait for data to load await this.page.waitForTimeout(3000) console.log('Chart data loaded successfully') return true } catch (error) { console.error('Chart data loading timeout:', error) await this.takeDebugScreenshot('chart_data_timeout') return false } } // Get current page URL for debugging async getCurrentUrl(): Promise { if (!this.page) return '' return await this.page.url() } // Check if we're logged in async isLoggedIn(): Promise { if (!this.page) return false try { const indicators = [ '[data-name="watchlist-button"]', '.tv-header__watchlist-button', '.tv-header__user-menu-button', 'button:has-text("M")' ] for (const selector of indicators) { try { if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { return true } } catch (e) { continue } } return false } catch (error) { return false } } // Check if file exists private async fileExists(filePath: string): Promise { try { await fs.access(filePath) return true } catch (error) { return false } } } export const tradingViewAutomation = new TradingViewAutomation()