import { chromium, Browser, Page, BrowserContext } from 'playwright' 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 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 private static instance: TradingViewAutomation | null = null private initPromise: Promise | null = null private operationLock: boolean = false private lastRequestTime = 0 private requestCount = 0 private sessionFingerprint: string | null = null private humanBehaviorEnabled = true private lastMousePosition = { x: 0, y: 0 } private loginAttempts = 0 private maxLoginAttempts = 3 // Singleton pattern to prevent multiple browser instances static getInstance(): TradingViewAutomation { if (!TradingViewAutomation.instance) { TradingViewAutomation.instance = new TradingViewAutomation() } return TradingViewAutomation.instance } /** * Acquire operation lock to prevent concurrent operations */ private async acquireOperationLock(timeout = 30000): Promise { const startTime = Date.now() while (this.operationLock) { if (Date.now() - startTime > timeout) { throw new Error('Operation lock timeout - another operation is in progress') } await new Promise(resolve => setTimeout(resolve, 100)) } this.operationLock = true } /** * Release operation lock */ private releaseOperationLock(): void { this.operationLock = false } async init(): Promise { // Acquire operation lock await this.acquireOperationLock() try { // Prevent multiple initialization calls if (this.initPromise) { console.log('๐Ÿ”„ Browser initialization already in progress, waiting...') return this.initPromise } if (this.browser && !this.browser.isConnected()) { console.log('๐Ÿ”„ Browser disconnected, cleaning up...') 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 }) // Use a random port to avoid conflicts const debugPort = 9222 + Math.floor(Math.random() * 1000) try { this.browser = await chromium.launch({ headless: true, // Must be true for Docker containers timeout: 60000, // Reduce timeout to 60 seconds 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=${debugPort}`, // 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' ] }) } catch (error) { console.error('ERROR: Failed to launch browser:', error) // Cleanup any partial state await this.forceCleanup() throw new Error('Failed to launch browser: ' + error) } if (!this.browser) { throw new Error('Failed to launch browser') } // Create browser context with enhanced stealth features 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 }, // Enhanced HTTP 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,application/signed-exchange;v=b3;q=0.7', 'Accept-Language': 'en-US,en;q=0.9,de;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Cache-Control': 'max-age=0', 'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"Linux"', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1', 'Dnt': '1' }, // Additional context options for stealth javaScriptEnabled: true, acceptDownloads: false, bypassCSP: false, colorScheme: 'light', deviceScaleFactor: 1, hasTouch: false, isMobile: false, locale: 'en-US', permissions: ['geolocation'], timezoneId: 'America/New_York' }) 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 enhanced stealth measures to reduce bot detection await this.page.addInitScript(() => { // Remove webdriver property completely Object.defineProperty(navigator, 'webdriver', { get: () => undefined, }); // Enhanced plugins simulation Object.defineProperty(navigator, 'plugins', { get: () => { const plugins = [ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' }, { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' }, { name: 'Native Client', filename: 'internal-nacl-plugin' } ]; plugins.length = 3; return plugins; }, }); // Enhanced language simulation Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en', 'de'], }); // Mock hardware concurrency Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8, }); // Mock device memory Object.defineProperty(navigator, 'deviceMemory', { get: () => 8, }); // Mock connection Object.defineProperty(navigator, 'connection', { get: () => ({ effectiveType: '4g', rtt: 50, downlink: 10 }), }); // Override permissions API with realistic responses const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters: any) => { const permission = parameters.name; let state = 'prompt'; if (permission === 'notifications') { state = 'default'; } else if (permission === 'geolocation') { state = 'prompt'; } return Promise.resolve({ state: state, name: permission, onchange: null, addEventListener: () => {}, removeEventListener: () => {}, dispatchEvent: () => false } as PermissionStatus); }; // Mock WebGL fingerprinting resistance const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(parameter: any) { if (parameter === 37445) { // UNMASKED_VENDOR_WEBGL return 'Intel Inc.'; } if (parameter === 37446) { // UNMASKED_RENDERER_WEBGL return 'Intel Iris OpenGL Engine'; } return getParameter.call(this, parameter); }; // Mock screen properties to be consistent Object.defineProperties(screen, { width: { get: () => 1920 }, height: { get: () => 1080 }, availWidth: { get: () => 1920 }, availHeight: { get: () => 1040 }, colorDepth: { get: () => 24 }, pixelDepth: { get: () => 24 } }); // Remove automation detection markers delete (window as any).chrome.runtime.onConnect; delete (window as any).chrome.runtime.onMessage; // Mock battery API Object.defineProperty(navigator, 'getBattery', { get: () => () => Promise.resolve({ charging: true, chargingTime: 0, dischargingTime: Infinity, level: 1 }), }); }) console.log('SUCCESS: 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: 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(5000) } // Take a debug screenshot to see the current state await this.takeDebugScreenshot('login_status_check') // Enhanced login detection with multiple strategies console.log('CHECKING: Strategy 1: Checking for user account indicators...') // Strategy 1: Look for user account elements (more comprehensive) const userAccountSelectors = [ // User menu and profile elements '[data-name="header-user-menu"]', '[data-name="user-menu"]', '.tv-header__user-menu-button:not(.tv-header__user-menu-button--anonymous)', '.tv-header__user-menu', '.js-header-user-menu-button', // Account/profile indicators '[data-name="account-menu"]', '[data-testid="header-user-menu"]', '.tv-header__dropdown-toggle', // Watchlist indicators (user-specific) '[data-name="watchlist-button"]', '.tv-header__watchlist-button', '.tv-watchlist-container', // Personal layout elements 'button:has-text("M")', // Watchlist "M" button '[data-name="watchlist-dropdown"]', // Pro/subscription indicators '.tv-header__pro-button', '[data-name="go-pro-button"]' ] let foundUserElement = false for (const selector of userAccountSelectors) { try { if (await this.page.locator(selector).isVisible({ timeout: 1500 })) { console.log("SUCCESS: Found user account element: " + selector) foundUserElement = true break } } catch (e) { continue } } // Strategy 2: Check for sign-in/anonymous indicators (should NOT be present if logged in) console.log('CHECKING: Strategy 2: Checking for anonymous/sign-in indicators...') const anonymousSelectors = [ // Sign in buttons/links 'a[href*="signin"]', 'a[href*="/accounts/signin"]', 'button:has-text("Sign in")', 'button:has-text("Log in")', 'a:has-text("Sign in")', 'a:has-text("Log in")', // Anonymous user indicators '.tv-header__user-menu-button--anonymous', '[data-name="header-user-menu-sign-in"]', '.tv-header__sign-in', // Guest mode indicators 'text="Continue as guest"', 'text="Sign up"', 'button:has-text("Sign up")' ] let foundAnonymousElement = false for (const selector of anonymousSelectors) { try { if (await this.page.locator(selector).isVisible({ timeout: 1500 })) { console.log(`ERROR: Found anonymous indicator: ${selector} - not logged in`) foundAnonymousElement = true break } } catch (e) { continue } } // Strategy 3: Check page URL patterns for authentication console.log('CHECKING: Strategy 3: Checking URL patterns...') const url = await this.page.url() const isOnLoginPage = url.includes('/accounts/signin') || url.includes('/signin') || url.includes('/login') if (isOnLoginPage) { console.log("ERROR: Currently on login page: " + url) this.isAuthenticated = false return false } // Strategy 4: Check for authentication-specific cookies console.log('CHECKING: Strategy 4: Checking authentication cookies...') let hasAuthCookies = false if (this.context) { const cookies = await this.context.cookies() const authCookieNames = [ 'sessionid', 'auth_token', 'user_token', 'session_token', 'authentication', 'logged_in', 'tv_auth', 'tradingview_auth' ] for (const cookie of cookies) { if (authCookieNames.some(name => cookie.name.toLowerCase().includes(name.toLowerCase()))) { console.log("๐Ÿช Found potential auth cookie: " + cookie.name) hasAuthCookies = true break } } console.log('DATA: Total cookies: ' + cookies.length + ', Auth cookies found: ' + hasAuthCookies) } // Strategy 5: Try to detect personal elements by checking page content console.log('CHECKING: Strategy 5: Checking for personal content...') let hasPersonalContent = false try { // Look for elements that indicate a logged-in user const personalContentSelectors = [ // Watchlist with custom symbols '.tv-screener-table', '.tv-widget-watch-list', // Personal layout elements '.layout-with-border-radius', '.tv-chart-view', // Settings/customization elements available only to logged-in users '[data-name="chart-settings"]', '[data-name="chart-properties"]' ] for (const selector of personalContentSelectors) { try { if (await this.page.locator(selector).isVisible({ timeout: 1000 })) { console.log("SUCCESS: Found personal content: " + selector) hasPersonalContent = true break } } catch (e) { continue } } // Additional check: see if we can access account-specific features const pageText = await this.page.textContent('body') || '' if (pageText.includes('My Watchlist') || pageText.includes('Portfolio') || pageText.includes('Alerts') || pageText.includes('Account')) { console.log('SUCCESS: Found account-specific text content') hasPersonalContent = true } } catch (e) { console.log('WARNING: Error checking personal content:', e) } // Final decision logic console.log('DATA: Login detection summary:') console.log(" User elements found: " + foundUserElement) console.log(" Anonymous elements found: " + foundAnonymousElement) console.log(" On login page: " + isOnLoginPage) console.log(" Has auth cookies: " + hasAuthCookies) console.log(" Has personal content: " + hasPersonalContent) // Determine login status based on multiple indicators const isLoggedIn = (foundUserElement || hasPersonalContent || hasAuthCookies) && !foundAnonymousElement && !isOnLoginPage if (isLoggedIn) { console.log('SUCCESS: User appears to be logged in') this.isAuthenticated = true return true } else { console.log('ERROR: User appears to be NOT logged in') this.isAuthenticated = false return false } } catch (error) { console.error('ERROR: Error checking login status:', error) await this.takeDebugScreenshot('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 with enhanced detection const loggedIn = await this.checkLoginStatus() if (loggedIn) { console.log('SUCCESS: Already logged in, skipping login steps') return true } console.log('๐Ÿ” Starting login process...') // Clear any existing session first to ensure clean login if (this.context) { try { await this.context.clearCookies() console.log('๐Ÿงน Cleared existing cookies for clean login') } catch (e) { console.log('WARNING: Could not clear cookies:', e) } } // Navigate to login page with multiple attempts console.log('๐Ÿ“„ Navigating to TradingView login page...') const loginUrls = [ 'https://www.tradingview.com/accounts/signin/', 'https://www.tradingview.com/' ] let loginPageLoaded = false for (const url of loginUrls) { try { console.log("๐Ÿ”„ Trying URL: " + url) await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }) // Wait for page to settle await this.page.waitForTimeout(3000) 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/') { // Try to find and click sign in button console.log('CHECKING: Looking for Sign In button on main page...') const signInSelectors = [ 'a[href*="signin"]:visible', 'a[href*="/accounts/signin/"]:visible', 'button:has-text("Sign in"):visible', 'a:has-text("Sign in"):visible', '.tv-header__user-menu-button--anonymous:visible', '[data-name="header-user-menu-sign-in"]:visible' ] for (const selector of signInSelectors) { try { console.log("TARGET: Trying sign in selector: " + selector) const element = this.page.locator(selector).first() if (await element.isVisible({ timeout: 3000 })) { await element.click() console.log("SUCCESS: Clicked sign in button: " + selector) // Wait for navigation to login page await this.page.waitForTimeout(3000) const newUrl = await this.page.url() if (newUrl.includes('signin') || newUrl.includes('login')) { console.log('SUCCESS: Successfully navigated to login page') loginPageLoaded = true break } } } catch (e) { console.log("ERROR: Sign in selector failed: " + selector) continue } } if (loginPageLoaded) break } } catch (e) { console.log(`ERROR: Failed to load ${url}:`, e) continue } } if (!loginPageLoaded) { throw new Error('Could not reach TradingView login page') } // Take screenshot of login page await this.takeDebugScreenshot('login_page_loaded') // Wait for login form to be ready console.log('โณ Waiting for login form to be ready...') await this.page.waitForTimeout(5000) // CRITICAL: Look for and click "Email" button if present (TradingView uses this pattern) console.log('CHECKING: Looking for Email login option...') // First try Playwright locator approach const emailTriggers = [ 'button:has-text("Email")', 'button:has-text("email")', 'text="Email"', 'text="Continue with email"', 'text="Sign in with email"', '[data-name="email"]', '[data-testid="email-button"]' ] let emailFormVisible = false for (const trigger of emailTriggers) { try { const element = this.page.locator(trigger).first() if (await element.isVisible({ timeout: 2000 })) { console.log("TARGET: Found email trigger: " + trigger) await element.click() console.log('SUCCESS: Clicked email trigger') // Wait for email form to appear await this.page.waitForTimeout(3000) emailFormVisible = true break } } catch (e) { continue } } // If locator approach failed, use manual button enumeration (like old working code) if (!emailFormVisible) { console.log('๐Ÿ”„ Locator approach failed, trying manual button search...') try { // Wait for buttons to be available await this.page.waitForSelector('button', { timeout: 10000 }) // Get all buttons and check their text content const buttons = await this.page.locator('button').all() console.log(`CHECKING: Found ${buttons.length} buttons to check`) for (let i = 0; i < buttons.length; i++) { try { const button = buttons[i] if (await button.isVisible({ timeout: 1000 })) { const text = await button.textContent() || '' const trimmedText = text.trim().toLowerCase() console.log(`INFO: Button ${i + 1}: "${trimmedText}"`) if (trimmedText.includes('email') || trimmedText.includes('continue with email') || trimmedText.includes('sign in with email')) { console.log('Found email button: ' + trimmedText) await button.click() console.log('SUCCESS: Clicked email button') // Wait for email form to appear await this.page.waitForTimeout(3000) emailFormVisible = true break } } } catch (e) { console.log('WARNING: Error checking button ' + (i + 1) + ':', e) continue } } } catch (e) { console.log('ERROR: Manual button search failed:', e) } } // Check if email input is now visible if (!emailFormVisible) { // Look for email input directly (might already be visible) const emailInputSelectors = [ 'input[type="email"]', 'input[name*="email"]', 'input[name*="username"]', 'input[name="username"]', // TradingView often uses this 'input[placeholder*="email" i]', 'input[placeholder*="username" i]' ] for (const selector of emailInputSelectors) { try { if (await this.page.locator(selector).isVisible({ timeout: 1000 })) { console.log('SUCCESS: Email input already visible: ' + selector) emailFormVisible = true break } } catch (e) { continue } } } if (!emailFormVisible) { await this.takeDebugScreenshot('no_email_form') // Additional debugging: show what elements are available const availableElements = await this.page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button')).map(btn => btn.textContent?.trim()).filter(Boolean) const inputs = Array.from(document.querySelectorAll('input')).map(input => ({ type: input.type, name: input.name, placeholder: input.placeholder })) const forms = Array.from(document.querySelectorAll('form')).length return { buttons, inputs, forms } }) console.log('CHECKING: Available elements:', JSON.stringify(availableElements, null, 2)) throw new Error('Could not find or activate email login form') } // Find and fill email field console.log('๐Ÿ“ง Looking for email input field...') const emailSelectors = [ 'input[name="username"]', // TradingView commonly uses this 'input[type="email"]', 'input[name="email"]', 'input[name="id_username"]', 'input[placeholder*="email" i]', 'input[placeholder*="username" i]', 'input[autocomplete="username"]', 'input[autocomplete="email"]', 'form input[type="text"]:not([type="password"])' ] let emailInput = null for (const selector of emailSelectors) { try { console.log('CHECKING: Trying email selector: ' + selector) if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { emailInput = selector console.log('SUCCESS: Found email input: ' + selector) break } } catch (e) { continue } } if (!emailInput) { // Try manual search like the old code console.log('๐Ÿ”„ Selector approach failed, trying manual input search...') try { const inputs = await this.page.locator('input').all() console.log(`CHECKING: Found ${inputs.length} inputs to check`) for (let i = 0; i < inputs.length; i++) { try { const input = inputs[i] if (await input.isVisible({ timeout: 1000 })) { const type = await input.getAttribute('type') || '' const name = await input.getAttribute('name') || '' const placeholder = await input.getAttribute('placeholder') || '' console.log(`INFO: Input ${i + 1}: type="${type}" name="${name}" placeholder="${placeholder}"`) if (type === 'email' || name.toLowerCase().includes('email') || name.toLowerCase().includes('username') || placeholder.toLowerCase().includes('email') || placeholder.toLowerCase().includes('username')) { console.log(`TARGET: Found email input manually: ${name || type || placeholder}`) emailInput = `input:nth-of-type(${i + 1})` break } } } catch (e) { console.log('WARNING: Error checking input ' + (i + 1) + ':', e) continue } } } catch (e) { console.log('ERROR: Manual input search failed:', e) } } if (!emailInput) { 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) console.log('SUCCESS: Email filled') // Find and fill password field console.log('๐Ÿ”‘ Looking for password input field...') const passwordSelectors = [ 'input[type="password"]', 'input[name="password"]', 'input[name="id_password"]', 'input[placeholder*="password" i]', 'input[autocomplete="current-password"]' ] let passwordInput = null for (const selector of passwordSelectors) { try { console.log("CHECKING: Trying password selector: " + selector) if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { passwordInput = selector console.log("SUCCESS: Found password input: " + selector) break } } catch (e) { continue } } if (!passwordInput) { await this.takeDebugScreenshot('no_password_input') throw new Error('Could not find password input field') } // Fill password console.log('๐Ÿ”‘ Filling password field...') await this.page.fill(passwordInput, password) console.log('SUCCESS: Password filled') // Handle potential captcha console.log('๐Ÿค– Checking for captcha...') try { // Look for different types of captcha and robot confirmation const captchaSelectors = [ 'iframe[src*="recaptcha"]', 'iframe[src*="captcha"]', '.recaptcha-checkbox', '[data-testid="captcha"]', '.captcha-container', 'text="Please confirm that you are not a robot"', 'text="Are you human?"', 'text="Please verify you are human"', 'text="Security check"', '.tv-dialog__error:has-text("robot")', '.alert:has-text("robot")', '.error:has-text("robot")' ] let captchaFound = false let captchaType = '' for (const selector of captchaSelectors) { try { if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { console.log("๐Ÿค– Captcha/Robot check detected: " + selector) captchaFound = true captchaType = selector break } } catch (e) { continue } } if (captchaFound) { console.log('WARNING: CAPTCHA/Robot verification detected!') console.log('๐Ÿšซ This indicates TradingView has flagged this as automated behavior.') console.log('๏ฟฝ In a Docker environment, automated captcha solving is not feasible.') // Take a screenshot for debugging await this.takeDebugScreenshot('captcha_detected') // Instead of waiting, we should fail fast and suggest alternatives console.log('ERROR: Cannot proceed with automated login due to captcha protection.') console.log('๐Ÿ”ง Possible solutions:') console.log(' 1. Use a different IP address or VPN') console.log(' 2. Wait some time before retrying (rate limiting)') console.log(' 3. Use session persistence from a manually authenticated browser') console.log(' 4. Contact TradingView support if this persists') // Mark captcha detection for future reference await this.markCaptchaDetected() // Return false immediately instead of waiting throw new Error(`Captcha detected (${captchaType}) - automated login blocked`) } } catch (captchaError: any) { if (captchaError.message.includes('Captcha detected')) { throw captchaError } console.log('WARNING: Captcha check failed:', captchaError?.message) } // Find and click submit button console.log('๐Ÿ”˜ Looking for submit button...') const submitSelectors = [ 'button[type="submit"]', 'input[type="submit"]', 'button:has-text("Sign in")', 'button:has-text("Log in")', 'button:has-text("Login")', 'button:has-text("Sign In")', '.tv-button--primary', 'form button:not([type="button"])', '[data-testid="signin-button"]', '[data-testid="login-button"]' ] let submitButton = null for (const selector of submitSelectors) { try { console.log("CHECKING: Trying submit selector: " + selector) if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { submitButton = selector console.log("SUCCESS: Found submit button: " + selector) break } } catch (e) { continue } } if (!submitButton) { // Try manual search like the old code console.log('๐Ÿ”„ Selector approach failed, trying manual button search for submit...') try { const buttons = await this.page.locator('button').all() console.log(`CHECKING: Found ${buttons.length} buttons to check for submit`) for (let i = 0; i < buttons.length; i++) { try { const button = buttons[i] if (await button.isVisible({ timeout: 1000 })) { const text = (await button.textContent() || '').toLowerCase() const type = await button.getAttribute('type') || '' console.log(`INFO: Submit Button ${i + 1}: "${text}" type="${type}"`) if (type === 'submit' || text.includes('sign in') || text.includes('login') || text.includes('submit')) { console.log(`TARGET: Found submit button manually: "${text}"`) submitButton = `button:nth-of-type(${i + 1})` break } } } catch (e) { continue } } } catch (e) { console.log('ERROR: Manual submit button search failed:', e) } } if (!submitButton) { await this.takeDebugScreenshot('no_submit_button') throw new Error('Could not find submit button') } // Click submit button console.log('๐Ÿ–ฑ๏ธ Clicking submit button...') await this.page.click(submitButton) console.log('SUCCESS: Submit button clicked') // Wait for login to complete console.log('โณ Waiting for login to complete...') try { // Wait for login completion without using waitForFunction (CSP violation) // Instead, check URL and elements periodically let attempts = 0 let maxAttempts = 15 // Reduced to 15 seconds with 1 second intervals let loginDetected = false while (attempts < maxAttempts && !loginDetected) { await this.page.waitForTimeout(1000) // Wait 1 second attempts++ console.log('Login check attempt ' + attempts + '/' + maxAttempts) // Check if we navigated away from login page const currentUrl = await this.page.url() console.log("๐Ÿ“ Current URL: " + currentUrl) const notOnLoginPage = !currentUrl.includes('/accounts/signin') && !currentUrl.includes('/signin') // Check for user-specific elements let hasUserElements = false try { const userElement = await this.page.locator( '[data-name="watchlist-button"], .tv-header__user-menu-button:not(.tv-header__user-menu-button--anonymous), [data-name="user-menu"]' ).first() hasUserElements = await userElement.isVisible({ timeout: 500 }) if (hasUserElements) { console.log('SUCCESS: Found user-specific elements') } } catch (e) { // Element not found, continue checking } // Check for error messages try { const errorSelectors = [ '.tv-dialog__error', '.error-message', '[data-testid="error"]', '.alert-danger' ] for (const selector of errorSelectors) { if (await this.page.locator(selector).isVisible({ timeout: 500 })) { const errorText = await this.page.locator(selector).textContent() console.log('ERROR: Login error detected: ' + errorText) throw new Error('Login failed: ' + errorText) } } } catch (e) { if (e instanceof Error && e.message.includes('Login failed:')) { throw e } // Continue if just element not found } if (notOnLoginPage || hasUserElements) { loginDetected = true console.log('๐ŸŽ‰ Navigation/elements suggest login success!') break } } if (!loginDetected) { throw new Error('Login verification timeout - no success indicators found') } // Additional wait for page to fully load await this.page.waitForTimeout(5000) // Verify login status with enhanced detection const loginSuccessful = await this.checkLoginStatus() if (loginSuccessful) { console.log('SUCCESS: Login verified successful!') this.isAuthenticated = true // Save session for future use await this.saveSession() console.log('๐Ÿ’พ Session saved for future use') return true } else { console.log('ERROR: Login verification failed') await this.takeDebugScreenshot('login_verification_failed') return false } } catch (error) { console.error('ERROR: Login completion timeout or error:', error) await this.takeDebugScreenshot('login_timeout') return false } } catch (error) { console.error('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('CHECKING: 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, checking session persistence options...') // Before attempting login, check if we have any saved session data const sessionInfo = await this.testSessionPersistence() console.log('DATA: Session persistence check:', sessionInfo) if (sessionInfo.cookiesCount > 0) { console.log('๐Ÿช Found saved session data, attempting to restore...') // Try navigating to TradingView with saved session first try { 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 and check login status await this.page.waitForTimeout(5000) const nowLoggedIn = await this.checkLoginStatus() if (nowLoggedIn) { console.log('SUCCESS: Session restoration successful! Login confirmed.') this.isAuthenticated = true await this.saveSession() return true } else { console.log('WARNING: Session restoration failed, saved session may be expired') } } catch (e) { console.log('ERROR: Session restoration attempt failed:', e) } } // Only attempt automated login if we don't have valid session data console.log('๐Ÿค– Attempting automated login...') try { const autoLoginSuccess = await this.login(credentials) if (autoLoginSuccess) { console.log('SUCCESS: Automated login successful! Saving session for future use.') this.isAuthenticated = true await this.saveSession() return true } } catch (loginError: any) { if (loginError.message.includes('Captcha detected')) { console.log('๐Ÿšซ Captcha protection encountered during automated login.') console.log('๐Ÿ’ก To resolve this issue:') console.log(' 1. Clear any existing session data: docker-compose exec app rm -f /app/session_*.json') console.log(' 2. Wait 1-2 hours before retrying (to clear rate limiting)') console.log(' 3. Consider using a different IP address or VPN') console.log(' 4. Manually log in to TradingView in a browser to establish a valid session') return false } throw loginError } console.log('ERROR: Automated login failed, this is likely due to captcha protection.') console.log('WARNING: In Docker environment, manual login is not practical.') console.log('๏ฟฝ Checking if we can proceed with session persistence...') // Try to check if there are any existing valid session cookies const fallbackSessionInfo = await this.testSessionPersistence() if (fallbackSessionInfo.isValid) { console.log('SUCCESS: Found valid session data, attempting to use it...') try { // Navigate to main TradingView page to test session await this.page.goto('https://www.tradingview.com/', { waitUntil: 'domcontentloaded', timeout: 30000 }) await this.page.waitForTimeout(5000) const nowLoggedIn = await this.checkLoginStatus() if (nowLoggedIn) { console.log('SUCCESS: Session persistence worked! Login successful.') this.isAuthenticated = true await this.saveSession() return true } } catch (e) { console.log('ERROR: Session persistence test failed:', e) } } console.log('ERROR: All login methods failed. This may require manual intervention.') console.log('๐Ÿ’ก To fix: Log in manually in a browser with the same credentials and restart the application.') return false } catch (error) { console.error('ERROR: Smart login failed:', error) return false } } async navigateToChart(options: NavigationOptions = {}): Promise { if (!this.page) throw new Error('Page not initialized') try { // Throttle requests to avoid suspicious patterns await this.throttleRequests() // Validate session integrity before proceeding const sessionValid = await this.validateSessionIntegrity() if (!sessionValid) { console.log('WARNING: Session integrity compromised, may require re-authentication') } const { symbol = 'SOLUSD', timeframe = '5', waitForChart = true } = options console.log('Navigating to chart page...') // Perform human-like interactions before navigation await this.performHumanLikeInteractions() // Generate session fingerprint for tracking await this.generateSessionFingerprint() // 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 }) } // Human-like delay after navigation await this.humanDelay(2000, 4000) // 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 with human-like delay await this.humanDelay(3000, 6000) } // 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 // CRITICAL: For hours, always prioritize minute values to avoid confusion const timeframeMap: { [key: string]: string[] } = { '1': ['1', '1m', '1min'], '1m': ['1', '1m', '1min'], '5': ['5', '5m', '5min'], '5m': ['5', '5m', '5min'], '15': ['15', '15m', '15min'], '15m': ['15', '15m', '15min'], '30': ['30', '30m', '30min'], '30m': ['30', '30m', '30min'], // For 1 hour - prioritize minute values first to avoid confusion '60': ['60', '60m', '1h', '1H'], '1h': ['60', '60m', '1h', '1H'], '1H': ['60', '60m', '1h', '1H'], // For 4 hours - CRITICAL: prioritize 240 minutes to avoid "4min" confusion '240': ['240', '240m', '4h', '4H'], '4h': ['240', '240m', '4h', '4H'], // Always try 240 minutes FIRST '4H': ['240', '240m', '4h', '4H'], // Add other common hour timeframes '2h': ['120', '120m', '2h', '2H'], '2H': ['120', '120m', '2h', '2H'], '6h': ['360', '360m', '6h', '6H'], '6H': ['360', '360m', '6h', '6H'], '12h': ['720', '720m', '12h', '12H'], '12H': ['720', '720m', '12h', '12H'], // Daily and weekly '1D': ['1D', 'D', 'daily', '1d'], '1d': ['1D', 'D', 'daily', '1d'], '1W': ['1W', 'W', 'weekly', '1w'], '1w': ['1W', 'W', 'weekly', '1w'] } // Get possible timeframe values to try const timeframesToTry = timeframeMap[timeframe] || [timeframe] console.log(`๐ŸŽฏ TIMEFRAME MAPPING: "${timeframe}" -> [${timeframesToTry.join(', ')}]`) console.log("Will try these timeframe values in order: " + 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('TARGET: 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("SUCCESS: 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('ERROR: 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('CHECKING: 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("SUCCESS: 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 (only for simple minute timeframes) if (!found) { console.log('๐Ÿ”„ Timeframe options not found, trying keyboard navigation...') // Try pressing specific keys for common timeframes (ONLY for minute-based) const keyMap: { [key: string]: string } = { '1': '1', '5': '5', '15': '1', // Sometimes 15min maps to '1' '30': '3', // Sometimes 30min maps to '3' '1D': 'D' // REMOVED: '240': '4' - this was causing 4h to be interpreted as 4min! // REMOVED: '60': '1' - this was causing 1h to be interpreted as 1min! } // Only use keyboard shortcuts for simple minute timeframes, not hour-based ones if (keyMap[timeframe] && !timeframe.includes('h') && !timeframe.includes('H')) { console.log("๐ŸŽน Trying keyboard shortcut: " + keyMap[timeframe]) await this.page.keyboard.press(keyMap[timeframe]) await this.page.waitForTimeout(1000) found = true } } // PRIORITY FALLBACK: Try custom interval input (for hour-based timeframes) if (!found) { console.log('๐Ÿ”ข Trying custom interval input for hour-based timeframes...') // Convert timeframe to minutes for custom input const minutesMap: { [key: string]: string } = { '4h': '240', '4H': '240', '240': '240', '2h': '120', '2H': '120', '120': '120', '6h': '360', '6H': '360', '360': '360', '12h': '720', '12H': '720', '720': '720', '1h': '60', '1H': '60', '60': '60' } const minutesValue = minutesMap[timeframe] if (minutesValue) { try { console.log(`๐ŸŽฏ PRIORITY: Entering ${minutesValue} minutes for ${timeframe} directly...`) // First, try to click the interval legend again to ensure dialog is open const intervalLegendSelectors = [ '[data-name="legend-source-interval"]', '.intervalTitle-l31H9iuA', '[title="Change interval"]' ] for (const selector of intervalLegendSelectors) { try { const element = this.page.locator(selector).first() if (await element.isVisible({ timeout: 2000 })) { await element.click() await this.page.waitForTimeout(1000) break } } catch (e) { // Continue to next selector } } // Look for the custom interval input field (more comprehensive selectors) const customInputSelectors = [ // TradingView interval dialog input 'input[data-name="text-input-field"]', 'input[placeholder*="interval"]', 'input[placeholder*="minutes"]', '.tv-dialog input[type="text"]', '.tv-dialog input[type="number"]', '.tv-text-input input', 'input[type="text"]', 'input[inputmode="numeric"]', // Look in any visible dialog '[role="dialog"] input', '.tv-dropdown-behavior__body input', // Generic text inputs that might be visible 'input:visible' ] let inputFound = false for (const selector of customInputSelectors) { try { const input = this.page.locator(selector).first() if (await input.isVisible({ timeout: 1000 })) { console.log(`๐Ÿ“ Found interval input field: ${selector}`) // Clear any existing value and enter the minutes value await input.click() await this.page.waitForTimeout(300) // Select all and delete await this.page.keyboard.press('Control+a') await this.page.waitForTimeout(100) await this.page.keyboard.press('Delete') await this.page.waitForTimeout(300) // Type the correct minutes value await input.fill(minutesValue) await this.page.waitForTimeout(500) // Press Enter to confirm await this.page.keyboard.press('Enter') await this.page.waitForTimeout(2000) console.log(`โœ… Successfully entered ${minutesValue} minutes for ${timeframe}`) found = true inputFound = true break } } catch (e) { console.log(`Custom input selector ${selector} not found or not accessible`) } } if (!inputFound) { console.log('โŒ No custom interval input field found') } } catch (error) { console.log('โŒ Error with custom interval input:', error) } } else { console.log(`โ„น๏ธ No minutes mapping found for timeframe: ${timeframe}`) } } if (found) { console.log("SUCCESS: Successfully changed timeframe to " + timeframe) await this.takeDebugScreenshot('after_timeframe_change') } else { console.log(`ERROR: 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') } } /** * Switch between different TradingView layouts (AI, DIY Module, etc.) * Uses the keyboard shortcut '.' to open the layouts dialog, then clicks the specific layout */ async switchLayout(layoutType: string): Promise { if (!this.page) return false try { console.log(`๐ŸŽ›๏ธ Switching to ${layoutType} layout using layouts dialog...`) // Take debug screenshot before switching await this.takeDebugScreenshot(`before_switch_to_${layoutType}`) // Map layout types to the EXACT text that appears in the layouts dialog const layoutMap: { [key: string]: string[] } = { 'ai': ['ai'], // Exact text from dialog: "ai" 'diy': ['Diy module'], // Exact text from dialog: "Diy module" 'default': ['Default'], 'advanced': ['Advanced'] } const searchTerms = layoutMap[layoutType.toLowerCase()] || [layoutType] // First, try the keyboard shortcut method to open layouts dialog console.log(`โŒจ๏ธ Opening layouts dialog with '.' key...`) await this.page.keyboard.press('.') await this.page.waitForTimeout(2000) // Wait for dialog to appear // Take debug screenshot to see the layouts dialog await this.takeDebugScreenshot(`layouts_dialog_opened_for_${layoutType}`) // Look for the layouts dialog and the specific layout within it const layoutsDialogVisible = await this.page.locator('.tv-dialog, .tv-popup, .tv-dropdown-behavior').first().isVisible({ timeout: 3000 }).catch(() => false) if (layoutsDialogVisible) { console.log(`โœ… Layouts dialog is open, checking current selection and navigating to ${layoutType} layout...`) // First, detect which layout is currently selected let currentSelectedIndex = -1 let currentSelectedText = '' try { const selectedInfo = await this.page.evaluate(() => { // Find all layout items in the dialog const items = Array.from(document.querySelectorAll('.tv-dropdown-behavior__item, .tv-list__item')) let selectedIndex = -1 let selectedText = '' items.forEach((item, index) => { const text = item.textContent?.trim() || '' const isSelected = item.classList.contains('tv-dropdown-behavior__item--selected') || item.classList.contains('tv-list__item--selected') || item.getAttribute('aria-selected') === 'true' || item.classList.contains('selected') || getComputedStyle(item).backgroundColor !== 'rgba(0, 0, 0, 0)' if (isSelected && text) { selectedIndex = index selectedText = text } }) return { selectedIndex, selectedText, totalItems: items.length } }) currentSelectedIndex = selectedInfo.selectedIndex currentSelectedText = selectedInfo.selectedText console.log(`๐Ÿ“ Current selection: "${currentSelectedText}" at index ${currentSelectedIndex}`) console.log(`๐Ÿ“‹ Total items in dialog: ${selectedInfo.totalItems}`) } catch (e) { console.log(`โš ๏ธ Could not detect current selection, using default navigation`) } // Define the layout positions based on the dialog structure const layoutPositions: { [key: string]: number } = { 'diy': 0, // "Diy module" is first (index 0) 'ai': 1, // "ai" is second (index 1) 'support': 2, // "support & resistance" would be third 'pi': 3 // "pi cycle top" would be fourth // Add more as needed } const targetIndex = layoutPositions[layoutType.toLowerCase()] if (targetIndex !== undefined && currentSelectedIndex >= 0) { const stepsNeeded = targetIndex - currentSelectedIndex console.log(`๐ŸŽฏ Need to move from index ${currentSelectedIndex} to ${targetIndex} (${stepsNeeded} steps)`) if (stepsNeeded === 0) { console.log(`โœ… Target layout "${layoutType}" is already selected, pressing Enter`) await this.page.keyboard.press('Enter') } else if (stepsNeeded > 0) { console.log(`๐Ÿ”ฝ Pressing ArrowDown ${stepsNeeded} times to reach "${layoutType}"`) for (let i = 0; i < stepsNeeded; i++) { await this.page.keyboard.press('ArrowDown') await this.page.waitForTimeout(200) } await this.page.keyboard.press('Enter') } else { console.log(`๐Ÿ”ผ Pressing ArrowUp ${Math.abs(stepsNeeded)} times to reach "${layoutType}"`) for (let i = 0; i < Math.abs(stepsNeeded); i++) { await this.page.keyboard.press('ArrowUp') await this.page.waitForTimeout(200) } await this.page.keyboard.press('Enter') } } else { // Fallback: Search by text content console.log(`๐Ÿ” Using fallback search method for "${layoutType}"`) const searchTerms = layoutMap[layoutType.toLowerCase()] || [layoutType] let attempts = 0 const maxAttempts = 10 while (attempts < maxAttempts) { try { const currentText = await this.page.evaluate(() => { const selected = document.querySelector('.tv-dropdown-behavior__item--selected, .tv-list__item--selected, [aria-selected="true"]') return selected?.textContent?.trim() || '' }) console.log(` Checking item: "${currentText}"`) if (searchTerms.some(term => currentText.toLowerCase().includes(term.toLowerCase()))) { console.log(`๐ŸŽฏ Found matching layout: "${currentText}"`) await this.page.keyboard.press('Enter') break } } catch (e) { // Continue searching } await this.page.keyboard.press('ArrowDown') await this.page.waitForTimeout(300) attempts++ } if (attempts >= maxAttempts) { console.log(`โš ๏ธ Could not find ${layoutType} layout after ${maxAttempts} attempts`) await this.page.keyboard.press('Escape') await this.page.waitForTimeout(1000) return false } } // Wait for layout to switch await this.page.waitForTimeout(3000) // Take debug screenshot after selection await this.takeDebugScreenshot(`after_select_${layoutType}_layout`) console.log(`โœ… Successfully selected ${layoutType} layout via keyboard navigation`) return true } else { console.log(`โš ๏ธ Layouts dialog did not appear, trying fallback method...`) } // Fallback to the original click-based method if keyboard shortcut didn't work console.log(`๐Ÿ”„ Fallback: Trying direct UI element search for ${layoutType}...`) const fallbackSearchTerms = layoutMap[layoutType.toLowerCase()] || [layoutType] // Enhanced TradingView layout switcher selectors (2024 UI patterns) const layoutSwitcherSelectors = [ // Look for the DIY module toggle specifically (visible in screenshot) 'text=Diy module', 'text=DIY module', '[title="Diy module"]', '[title="DIY module"]', 'button:has-text("Diy")', 'button:has-text("DIY")', // TradingView specific layout/module selectors - more precise matching '[data-name="ai-panel"]', '[data-name="ai-layout"]', '[data-name="ai-module"]', '[data-name="diy-panel"]', '[data-name="diy-layout"]', '[data-name="diy-module"]', '[data-module-name="ai"]', '[data-module-name="diy"]', '[data-layout-name="ai"]', '[data-layout-name="diy"]', // Top toolbar and header elements with specific text content '.tv-header [role="button"]:has-text("AI")', '.tv-header [role="button"]:has-text("DIY")', '.tv-toolbar [role="button"]:has-text("AI")', '.tv-toolbar [role="button"]:has-text("DIY")', '.tv-chart-header [role="button"]:has-text("AI")', '.tv-chart-header [role="button"]:has-text("DIY")', // Module and tab selectors with text '.tv-module-tabs [role="tab"]:has-text("AI")', '.tv-module-tabs [role="tab"]:has-text("DIY")', '.tv-chart-tabs [role="tab"]:has-text("AI")', '.tv-chart-tabs [role="tab"]:has-text("DIY")', // Modern UI component selectors - exact matches '[data-testid="ai-layout"]', '[data-testid="diy-layout"]', '[data-testid="ai-module"]', '[data-testid="diy-module"]', '[data-widget-type="ai"]', '[data-widget-type="diy"]', // Button elements with exact title/aria-label matches 'button[title="AI"]', 'button[title="DIY"]', 'button[title="AI Analysis"]', 'button[title="DIY Module"]', 'button[aria-label="AI"]', 'button[aria-label="DIY"]', 'button[aria-label="AI Analysis"]', 'button[aria-label="DIY Module"]', // Generic selectors (last resort) - but we'll be more selective '[role="tab"]', '[role="button"]', 'button' ] console.log(`๐Ÿ” Searching for ${layoutType} layout using ${fallbackSearchTerms.length} search terms and ${layoutSwitcherSelectors.length} selectors`) // Debug: Log all visible buttons/tabs for inspection if (process.env.NODE_ENV === 'development') { try { const allInteractiveElements = await this.page.locator('button, [role="button"], [role="tab"]').all() console.log(`๐Ÿ” Found ${allInteractiveElements.length} interactive elements on page`) for (const element of allInteractiveElements.slice(0, 20)) { // Limit to first 20 for readability const text = await element.textContent().catch(() => '') const title = await element.getAttribute('title').catch(() => '') const ariaLabel = await element.getAttribute('aria-label').catch(() => '') const dataName = await element.getAttribute('data-name').catch(() => '') if (text || title || ariaLabel || dataName) { console.log(` ๐Ÿ“‹ Element: text="${text}" title="${title}" aria-label="${ariaLabel}" data-name="${dataName}"`) } } } catch (e) { console.log('โš ๏ธ Could not enumerate interactive elements for debugging') } } // First, try to find and click layout switcher elements for (const searchTerm of fallbackSearchTerms) { console.log(`๐ŸŽฏ Searching for layout elements containing: "${searchTerm}"`) for (const selector of layoutSwitcherSelectors) { try { // Look for elements containing the search term const elements = await this.page.locator(selector).all() for (const element of elements) { const text = await element.textContent().catch(() => '') const title = await element.getAttribute('title').catch(() => '') const ariaLabel = await element.getAttribute('aria-label').catch(() => '') const dataTooltip = await element.getAttribute('data-tooltip').catch(() => '') const dataName = await element.getAttribute('data-name').catch(() => '') const combinedText = `${text} ${title} ${ariaLabel} ${dataTooltip} ${dataName}`.toLowerCase() // More precise matching - avoid false positives let isMatch = false if (layoutType.toLowerCase() === 'ai') { // For AI, look for exact matches or clear AI-related terms isMatch = ( text?.trim().toLowerCase() === 'ai' || title?.toLowerCase() === 'ai' || ariaLabel?.toLowerCase() === 'ai' || text?.toLowerCase().includes('ai analysis') || text?.toLowerCase().includes('ai insights') || title?.toLowerCase().includes('ai analysis') || title?.toLowerCase().includes('ai insights') || dataName?.toLowerCase() === 'ai-panel' || dataName?.toLowerCase() === 'ai-module' || dataName?.toLowerCase() === 'ai-layout' ) } else if (layoutType.toLowerCase() === 'diy') { // For DIY, look for exact matches or clear DIY-related terms isMatch = ( text?.trim().toLowerCase() === 'diy' || title?.toLowerCase() === 'diy' || ariaLabel?.toLowerCase() === 'diy' || text?.toLowerCase().includes('diy module') || text?.toLowerCase().includes('diy builder') || title?.toLowerCase().includes('diy module') || title?.toLowerCase().includes('diy builder') || dataName?.toLowerCase() === 'diy-panel' || dataName?.toLowerCase() === 'diy-module' || dataName?.toLowerCase() === 'diy-layout' ) } else { // For other layouts, use the original logic isMatch = combinedText.includes(searchTerm.toLowerCase()) } if (isMatch) { console.log(`๐ŸŽฏ Found potential ${layoutType} layout element:`) console.log(` Selector: ${selector}`) console.log(` Text: "${text}"`) console.log(` Title: "${title}"`) console.log(` Aria-label: "${ariaLabel}"`) console.log(` Data-name: "${dataName}"`) // Additional validation - skip if this looks like a false positive const skipPatterns = [ 'details', 'metrics', 'search', 'symbol', 'chart-', 'interval', 'timeframe', 'indicator', 'alert', 'watchlist', 'compare' ] const shouldSkip = skipPatterns.some(pattern => dataName?.toLowerCase().includes(pattern) || title?.toLowerCase().includes(pattern) || ariaLabel?.toLowerCase().includes(pattern) ) if (shouldSkip) { console.log(`โš ๏ธ Skipping element that looks like a false positive`) continue } if (await element.isVisible({ timeout: 2000 })) { console.log(`โœ… Element is visible, attempting click...`) await element.click() await this.page.waitForTimeout(3000) // Wait longer for layout change // Take debug screenshot after clicking await this.takeDebugScreenshot(`after_click_${layoutType}`) console.log(`โœ… Successfully clicked ${layoutType} layout element`) return true } else { console.log(`โš ๏ธ Element found but not visible`) } } } } catch (e: any) { // Continue to next selector console.log(`โš ๏ธ Error with selector "${selector}": ${e?.message || e}`) } } } // Secondary approach: Try to find layout/module menus and click them console.log(`๐Ÿ” Trying to find ${layoutType} layout via menu navigation...`) const menuSelectors = [ // Look for layout/view menus '[data-name="chart-layout-menu"]', '[data-name="view-menu"]', '[data-name="chart-menu"]', '.tv-menu-button', '.tv-dropdown-button', // Try toolbar dropdown menus '.tv-toolbar .tv-dropdown', '.tv-header .tv-dropdown', '.tv-chart-header .tv-dropdown', // Widget panel menus '.tv-widget-panel .tv-dropdown', '.tv-side-panel .tv-dropdown' ] for (const menuSelector of menuSelectors) { try { const menuButton = this.page.locator(menuSelector).first() if (await menuButton.isVisible({ timeout: 1000 })) { console.log(`๐ŸŽฏ Found potential layout menu: ${menuSelector}`) await menuButton.click() await this.page.waitForTimeout(1000) // Look for layout options in the opened menu for (const searchTerm of searchTerms) { const menuItems = await this.page.locator('.tv-dropdown-behavior__item, .tv-menu__item, .tv-popup__item').all() for (const item of menuItems) { const itemText = await item.textContent().catch(() => '') if (itemText && itemText.toLowerCase().includes(searchTerm.toLowerCase())) { console.log(`๐ŸŽฏ Found ${layoutType} in menu: ${itemText}`) await item.click() await this.page.waitForTimeout(3000) // Take debug screenshot after menu selection await this.takeDebugScreenshot(`after_menu_select_${layoutType}`) return true } } } // Close menu if we didn't find what we're looking for await this.page.keyboard.press('Escape') await this.page.waitForTimeout(500) } } catch (e: any) { // Continue to next menu selector console.log(`โš ๏ธ Error with menu selector "${menuSelector}": ${e?.message || e}`) } } // Third approach: Try right-click context menu console.log(`๐Ÿ” Trying right-click context menu for ${layoutType} layout...`) try { // Right-click on chart area const chartContainer = this.page.locator('.tv-chart-container, .chart-container, .tv-chart').first() if (await chartContainer.isVisible({ timeout: 2000 })) { await chartContainer.click({ button: 'right' }) await this.page.waitForTimeout(1000) // Look for layout options in context menu for (const searchTerm of searchTerms) { const contextMenuItems = await this.page.locator('.tv-context-menu__item, .tv-dropdown-behavior__item').all() for (const item of contextMenuItems) { const itemText = await item.textContent().catch(() => '') if (itemText && itemText.toLowerCase().includes(searchTerm.toLowerCase())) { console.log(`๐ŸŽฏ Found ${layoutType} in context menu: ${itemText}`) await item.click() await this.page.waitForTimeout(3000) // Take debug screenshot after context menu selection await this.takeDebugScreenshot(`after_context_menu_${layoutType}`) return true } } } // Close context menu await this.page.keyboard.press('Escape') } } catch (e: any) { console.log(`โš ๏ธ Error with context menu: ${e?.message || e}`) } // Fallback: Try keyboard shortcuts const keyboardShortcuts: { [key: string]: string } = { 'ai': 'Alt+A', 'diy': 'Alt+D', 'default': 'Alt+1', 'advanced': 'Alt+2' } const shortcut = keyboardShortcuts[layoutType.toLowerCase()] if (shortcut) { console.log(`โŒจ๏ธ Trying keyboard shortcut for ${layoutType}: ${shortcut}`) await this.page.keyboard.press(shortcut) await this.page.waitForTimeout(3000) // Take debug screenshot after keyboard shortcut await this.takeDebugScreenshot(`after_shortcut_${layoutType}`) console.log(`โœ… Attempted ${layoutType} layout switch via keyboard shortcut`) return true } console.log(`โŒ Could not find ${layoutType} layout switcher with any method`) // Take final debug screenshot await this.takeDebugScreenshot(`failed_switch_to_${layoutType}`) return false } catch (error) { console.error(`Error switching to ${layoutType} layout:`, error) return false } } /** * Wait for layout to fully load and verify the layout change occurred */ async waitForLayoutLoad(layoutType: string): Promise { if (!this.page) return false try { console.log(`โณ Waiting for ${layoutType} layout to load...`) // Take debug screenshot to verify layout state await this.takeDebugScreenshot(`waiting_for_${layoutType}_load`) // Wait for layout-specific elements to appear const layoutIndicators: { [key: string]: string[] } = { 'ai': [ // AI-specific panels and widgets '[data-name="ai-panel"]', '[data-name*="ai"]', '.ai-analysis', '.ai-module', '.ai-widget', '.ai-insights', '[title*="AI"]', '[class*="ai"]', '[data-widget-type*="ai"]', // AI content indicators 'text=AI Analysis', 'text=AI Insights', 'text=Smart Money', // TradingView AI specific '.tv-ai-panel', '.tv-ai-widget', '.tv-ai-analysis' ], 'diy': [ // DIY-specific panels and widgets '[data-name="diy-panel"]', '[data-name*="diy"]', '.diy-module', '.diy-builder', '.diy-widget', '[title*="DIY"]', '[class*="diy"]', '[data-widget-type*="diy"]', // DIY content indicators 'text=DIY Builder', 'text=DIY Module', 'text=Custom Layout', // TradingView DIY specific '.tv-diy-panel', '.tv-diy-widget', '.tv-diy-builder' ], 'default': [ // Default layout indicators '.tv-chart-container', '.chart-container', '.tv-chart' ] } const indicators = layoutIndicators[layoutType.toLowerCase()] || [] let layoutDetected = false console.log(`๐Ÿ” Checking ${indicators.length} layout indicators for ${layoutType}`) // Try each indicator with reasonable timeout for (const indicator of indicators) { try { console.log(` ๐ŸŽฏ Checking indicator: ${indicator}`) await this.page.locator(indicator).first().waitFor({ state: 'visible', timeout: 3000 }) console.log(`โœ… ${layoutType} layout indicator found: ${indicator}`) layoutDetected = true break } catch (e) { // Continue to next indicator console.log(` โš ๏ธ Indicator not found: ${indicator}`) } } if (layoutDetected) { // Take success screenshot await this.takeDebugScreenshot(`${layoutType}_layout_loaded`) // Additional wait for content to stabilize await this.page.waitForTimeout(2000) console.log(`โœ… ${layoutType} layout loaded successfully`) return true } // If no specific indicators found, try generic layout change detection console.log(`๐Ÿ” No specific indicators found, checking for general layout changes...`) // Wait for any visual changes in common layout areas const layoutAreas = [ '.tv-chart-container', '.tv-widget-panel', '.tv-side-panel', '.chart-container', '.tv-chart' ] for (const area of layoutAreas) { try { const areaElement = this.page.locator(area).first() if (await areaElement.isVisible({ timeout: 2000 })) { console.log(`โœ… Layout area visible: ${area}`) layoutDetected = true break } } catch (e) { // Continue checking } } if (layoutDetected) { // Fallback: wait for general layout changes await this.page.waitForTimeout(3000) // Take fallback screenshot await this.takeDebugScreenshot(`${layoutType}_layout_fallback_loaded`) console.log(`โš ๏ธ ${layoutType} layout load detection uncertain, but proceeding...`) return true } console.log(`โŒ ${layoutType} layout load could not be verified`) // Take failure screenshot await this.takeDebugScreenshot(`${layoutType}_layout_load_failed`) return false } catch (error) { console.error(`Error waiting for ${layoutType} layout:`, error) // Take error screenshot await this.takeDebugScreenshot(`${layoutType}_layout_load_error`) return false } } /** * Check if user is logged in (alias for checkLoginStatus) */ async isLoggedIn(): Promise { return this.checkLoginStatus() } /** * Wait for chart data to load with enhanced detection */ async waitForChartData(): Promise { if (!this.page) return false try { console.log('Waiting for chart data to load...') // Wait for various chart loading indicators const chartLoadingSelectors = [ 'canvas', '.tv-lightweight-charts', '[data-name="chart"]', '.chart-container canvas', '.tv-chart canvas' ] // Wait for at least one chart element let chartFound = false for (const selector of chartLoadingSelectors) { try { await this.page.waitForSelector(selector, { timeout: 10000 }) chartFound = true break } catch (e) { continue } } if (!chartFound) { console.log('WARNING: No chart elements found') return false } // Additional wait for chart data to load await this.humanDelay(3000, 6000) // Check if chart appears to have data (not just loading screen) const hasData = await this.page.evaluate(() => { const canvases = document.querySelectorAll('canvas') for (let i = 0; i < canvases.length; i++) { const canvas = canvases[i] const rect = canvas.getBoundingClientRect() if (rect.width > 100 && rect.height > 100) { return true } } return false }) console.log('Chart data loaded successfully') return hasData } catch (error) { console.error('ERROR: Error waiting for chart data:', error) return false } } /** * Take screenshot with anti-detection measures */ async takeScreenshot(filename: string): Promise { if (!this.page) throw new Error('Page not initialized') try { const screenshotsDir = path.join(process.cwd(), 'screenshots') await fs.mkdir(screenshotsDir, { recursive: true }) const filePath = path.join(screenshotsDir, filename) // Perform human-like interaction before screenshot await this.simulateHumanScrolling() await this.humanDelay(1000, 2000) console.log("Taking screenshot: " + filename) // Try to find and focus on the main chart area first const chartSelectors = [ '#tv-chart-container', '.layout__area--center', '.chart-container-border', '.tv-chart-area-container', '.chart-area', '[data-name="chart-area"]', '.tv-chart-area' ] let chartElement = null for (const selector of chartSelectors) { try { chartElement = await this.page.locator(selector).first() if (await chartElement.isVisible({ timeout: 2000 })) { console.log(`๐Ÿ“ธ Found chart area with selector: ${selector}`) break } } catch (e) { // Continue to next selector } } if (chartElement && await chartElement.isVisible()) { // Take screenshot of the chart area specifically await chartElement.screenshot({ path: filePath, type: 'png' }) console.log("๐Ÿ“ธ Chart area screenshot saved: " + filename) } else { // Fallback to full page screenshot console.log("โš ๏ธ Chart area not found, taking full page screenshot") await this.page.screenshot({ path: filePath as `${string}.png`, fullPage: true, type: 'png' }) console.log("๐Ÿ“ธ Full page screenshot saved: " + filename) } return filePath } catch (error) { console.error('ERROR: Error taking screenshot:', error) throw error } } /** * Take a debug screenshot for troubleshooting */ private async takeDebugScreenshot(prefix: string): Promise { if (!this.page) return try { const timestamp = Date.now() const filename = `debug_${prefix}_${timestamp}.png` const filePath = path.join(process.cwd(), 'screenshots', filename) // Ensure 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) } catch (error) { console.log('WARNING: Error taking debug screenshot:', error) } } /** * Get current URL */ async getCurrentUrl(): Promise { if (!this.page) return 'about:blank' return this.page.url() } /** * Enhanced cleanup method */ async close(): Promise { return this.forceCleanup() } /** * Force cleanup of browser resources */ async forceCleanup(): Promise { // Don't use operation lock here to avoid deadlocks during cleanup try { if (this.page) { try { await this.page.close() } catch (e) { console.log('WARNING: Error closing page:', e) } this.page = null } if (this.context) { try { await this.context.close() } catch (e) { console.log('WARNING: Error closing context:', e) } this.context = null } if (this.browser) { try { await this.browser.close() } catch (e) { console.log('WARNING: Error closing browser:', e) } this.browser = null } // Reset flags this.isAuthenticated = false this.operationLock = false this.initPromise = null } catch (error) { console.error('ERROR: Error during force cleanup:', error) } } /** * Reset the singleton instance (useful for testing or forcing recreation) */ static resetInstance(): void { if (TradingViewAutomation.instance) { TradingViewAutomation.instance.forceCleanup().catch(console.error) TradingViewAutomation.instance = 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(`SUCCESS: Loaded ${cookies.length} cookies from saved session`) } // Note: Session storage will be loaded after page navigation } catch (error) { console.log('WARNING: 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(`SUCCESS: 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('SUCCESS: Saved session storage and localStorage') } catch (error) { console.error('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('SUCCESS: Restored session storage and localStorage') } catch (error) { console.log('WARNING: 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('SUCCESS: Session refreshed successfully') await this.saveSession() // Save refreshed session return true } else { console.log('ERROR: Session expired during refresh') this.isAuthenticated = false return false } } catch (error) { console.error('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('SUCCESS: Cleared cookies file') } if (await this.fileExists(SESSION_STORAGE_FILE)) { await fs.unlink(SESSION_STORAGE_FILE) console.log('SUCCESS: Cleared session storage file') } // Clear browser context storage if available if (this.context) { await this.context.clearCookies() console.log('SUCCESS: Cleared browser context cookies') } this.isAuthenticated = false console.log('SUCCESS: Session data cleared successfully') } catch (error) { console.error('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 } } /** * Get lightweight session status without triggering navigation */ async getQuickSessionStatus(): Promise<{ isAuthenticated: boolean hasSavedCookies: boolean hasSavedStorage: boolean cookiesCount: number currentUrl: string browserActive: boolean }> { const sessionInfo = await this.getSessionInfo() return { ...sessionInfo, browserActive: !!(this.browser && this.page) } } /** * Add random delay to mimic human behavior */ private async humanDelay(min: number = 500, max: number = 1500): Promise { if (!this.humanBehaviorEnabled) return const delay = Math.floor(Math.random() * (max - min + 1)) + min // Add micro-pauses to make it even more realistic const microPauses = Math.floor(Math.random() * 3) + 1 for (let i = 0; i < microPauses; i++) { await this.page?.waitForTimeout(delay / microPauses) if (i < microPauses - 1) { await this.page?.waitForTimeout(Math.floor(Math.random() * 100) + 50) } } } /** * Simulate human-like mouse movement before clicks */ private async humanMouseMove(targetElement?: any): Promise { if (!this.page || !this.humanBehaviorEnabled) return try { // Get current viewport size const viewport = this.page.viewportSize() || { width: 1920, height: 1080 } if (targetElement) { // Move to target element with slight randomization const boundingBox = await targetElement.boundingBox() if (boundingBox) { const targetX = boundingBox.x + boundingBox.width / 2 + (Math.random() - 0.5) * 20 const targetY = boundingBox.y + boundingBox.height / 2 + (Math.random() - 0.5) * 20 // Create intermediate points for more natural movement const steps = Math.floor(Math.random() * 3) + 2 for (let i = 1; i <= steps; i++) { const progress = i / steps const currentX = this.lastMousePosition.x + (targetX - this.lastMousePosition.x) * progress const currentY = this.lastMousePosition.y + (targetY - this.lastMousePosition.y) * progress await this.page.mouse.move(currentX, currentY) await this.humanDelay(50, 150) } this.lastMousePosition = { x: targetX, y: targetY } } } else { // Random mouse movement within viewport const randomX = Math.floor(Math.random() * viewport.width) const randomY = Math.floor(Math.random() * viewport.height) await this.page.mouse.move(randomX, randomY) this.lastMousePosition = { x: randomX, y: randomY } } } catch (error) { console.log('WARNING: Mouse movement simulation failed:', error) } } /** * Human-like typing with realistic delays and occasional typos */ private async humanType(selector: string, text: string): Promise { if (!this.page || !this.humanBehaviorEnabled) { await this.page?.fill(selector, text) return } try { // Clear the field first await this.page.click(selector) await this.page.keyboard.press('Control+a') await this.humanDelay(100, 300) // Type character by character with realistic delays for (let i = 0; i < text.length; i++) { const char = text[i] // Simulate occasional brief pauses (thinking) if (Math.random() < 0.1) { await this.humanDelay(300, 800) } // Simulate occasional typos and corrections (5% chance) if (Math.random() < 0.05 && i > 0) { // Type wrong character const wrongChars = 'abcdefghijklmnopqrstuvwxyz' const wrongChar = wrongChars[Math.floor(Math.random() * wrongChars.length)] await this.page.keyboard.type(wrongChar) await this.humanDelay(200, 500) // Correct the typo await this.page.keyboard.press('Backspace') await this.humanDelay(100, 300) } // Type the actual character await this.page.keyboard.type(char) // Realistic typing speed variation const baseDelay = 80 const variation = Math.random() * 120 await this.page.waitForTimeout(baseDelay + variation) } // Brief pause after typing await this.humanDelay(200, 500) } catch (error) { console.log('WARNING: Human typing failed, falling back to fill:', error) await this.page.fill(selector, text) } } /** * Human-like clicking with mouse movement and realistic delays */ private async humanClick(element: any): Promise { if (!this.page || !this.humanBehaviorEnabled) { await element.click() return } try { // Move mouse to element first await this.humanMouseMove(element) await this.humanDelay(200, 500) // Hover for a brief moment await element.hover() await this.humanDelay(100, 300) // Click with slight randomization const boundingBox = await element.boundingBox() if (boundingBox) { const clickX = boundingBox.x + boundingBox.width / 2 + (Math.random() - 0.5) * 10 const clickY = boundingBox.y + boundingBox.height / 2 + (Math.random() - 0.5) * 10 await this.page.mouse.click(clickX, clickY) } else { await element.click() } // Brief pause after click await this.humanDelay(300, 700) } catch (error) { console.log('WARNING: Human clicking failed, falling back to normal click:', error) await element.click() await this.humanDelay(300, 700) } } /** * Simulate reading behavior with eye movement patterns */ private async simulateReading(): Promise { if (!this.page || !this.humanBehaviorEnabled) return try { // Simulate reading by scrolling and pausing const viewport = this.page.viewportSize() || { width: 1920, height: 1080 } // Random scroll amount const scrollAmount = Math.floor(Math.random() * 200) + 100 await this.page.mouse.wheel(0, scrollAmount) await this.humanDelay(800, 1500) // Scroll back await this.page.mouse.wheel(0, -scrollAmount) await this.humanDelay(500, 1000) } catch (error) { console.log('WARNING: Reading simulation failed:', error) } } /** * Enhanced anti-detection measures */ private async applyAdvancedStealth(): Promise { if (!this.page) return try { await this.page.addInitScript(() => { // Advanced fingerprint resistance // Override canvas fingerprinting const originalToDataURL = HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL = function(...args) { const context = this.getContext('2d'); if (context) { // Add noise to canvas const imageData = context.getImageData(0, 0, this.width, this.height); for (let i = 0; i < imageData.data.length; i += 4) { if (Math.random() < 0.01) { imageData.data[i] = Math.floor(Math.random() * 256); imageData.data[i + 1] = Math.floor(Math.random() * 256); imageData.data[i + 2] = Math.floor(Math.random() * 256); } } context.putImageData(imageData, 0, 0); } return originalToDataURL.apply(this, args); }; // Override audio fingerprinting const audioContext = window.AudioContext || (window as any).webkitAudioContext; if (audioContext) { const originalCreateAnalyser = audioContext.prototype.createAnalyser; audioContext.prototype.createAnalyser = function() { const analyser = originalCreateAnalyser.call(this); const originalGetFloatFrequencyData = analyser.getFloatFrequencyData; analyser.getFloatFrequencyData = function(array: Float32Array) { originalGetFloatFrequencyData.call(this, array); // Add subtle noise for (let i = 0; i < array.length; i++) { array[i] += (Math.random() - 0.5) * 0.001; } }; return analyser; }; } // Override timezone detection Object.defineProperty(Intl.DateTimeFormat.prototype, 'resolvedOptions', { value: function() { return { ...Intl.DateTimeFormat.prototype.resolvedOptions.call(this), timeZone: 'America/New_York' }; } }); // Randomize performance.now() slightly const originalNow = performance.now; performance.now = function() { return originalNow.call(this) + Math.random() * 0.1; }; // Mock more realistic viewport Object.defineProperty(window, 'outerWidth', { get: () => 1920 + Math.floor(Math.random() * 100) }); Object.defineProperty(window, 'outerHeight', { get: () => 1080 + Math.floor(Math.random() * 100) }); }); } catch (error) { console.log('WARNING: Advanced stealth measures failed:', error) } } /** * Check if file exists */ private async fileExists(filePath: string): Promise { try { await fs.access(filePath) return true } catch { return false } } /** * Mark CAPTCHA as detected (stub) */ private async markCaptchaDetected(): Promise { console.log('๐Ÿค– CAPTCHA detected') } /** * Throttle requests (stub) */ private async throttleRequests(): Promise { // Rate limiting logic could go here await new Promise(resolve => setTimeout(resolve, 100)) } /** * Validate session integrity (stub) */ private async validateSessionIntegrity(): Promise { return true // Simplified implementation } /** * Perform human-like interactions (stub) */ private async performHumanLikeInteractions(): Promise { // Human-like behavior could go here } /** * Generate session fingerprint (stub) */ private async generateSessionFingerprint(): Promise { this.sessionFingerprint = `fp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` } /** * Simulate human scrolling (stub) */ private async simulateHumanScrolling(): Promise { if (!this.page) return // Simple scroll simulation await this.page.mouse.wheel(0, 100) await this.page.waitForTimeout(500) await this.page.mouse.wheel(0, -50) } /** * Test session persistence and return session information */ async testSessionPersistence(): Promise<{ isValid: boolean cookiesCount: number hasStorage: boolean details?: string }> { try { let cookiesCount = 0 let hasStorage = false let details = '' // Check if session files exist if (await this.fileExists(COOKIES_FILE)) { const cookiesData = await fs.readFile(COOKIES_FILE, 'utf-8') const cookies = JSON.parse(cookiesData) cookiesCount = cookies.length || 0 } if (await this.fileExists(SESSION_STORAGE_FILE)) { const storageData = await fs.readFile(SESSION_STORAGE_FILE, 'utf-8') const storage = JSON.parse(storageData) hasStorage = Object.keys(storage).length > 0 } const isValid = cookiesCount > 0 && hasStorage details = `Cookies: ${cookiesCount}, Storage: ${hasStorage ? 'Yes' : 'No'}` return { isValid, cookiesCount, hasStorage, details } } catch (error) { console.error('Error testing session persistence:', error) return { isValid: false, cookiesCount: 0, hasStorage: false, details: 'Session test failed' } } } } /** * Add process cleanup handlers to ensure browser instances are properly cleaned up */ process.on('SIGTERM', async () => { console.log('๐Ÿ”„ SIGTERM received, cleaning up browser...') await TradingViewAutomation.getInstance().forceCleanup() process.exit(0) }) process.on('SIGINT', async () => { console.log('๐Ÿ”„ SIGINT received, cleaning up browser...') await TradingViewAutomation.getInstance().forceCleanup() process.exit(0) }) process.on('uncaughtException', async (error) => { console.error('๐Ÿ’ฅ Uncaught exception, cleaning up browser:', error) await TradingViewAutomation.getInstance().forceCleanup() process.exit(1) }) process.on('unhandledRejection', async (reason, promise) => { console.error('๐Ÿ’ฅ Unhandled rejection, cleaning up browser:', reason) await TradingViewAutomation.getInstance().forceCleanup() process.exit(1) }) export const tradingViewAutomation = TradingViewAutomation.getInstance()