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 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 // 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('โœ… 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('โŒ 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('โœ… 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(5000) } // Take a debug screenshot to see the current state await this.takeDebugScreenshot('login_status_check') // Enhanced login detection with multiple strategies console.log('๐Ÿ” 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(`โœ… 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('๐Ÿ” 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(`โŒ Found anonymous indicator: ${selector} - not logged in`) foundAnonymousElement = true break } } catch (e) { continue } } // Strategy 3: Check page URL patterns for authentication console.log('๐Ÿ” 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(`โŒ Currently on login page: ${url}`) this.isAuthenticated = false return false } // Strategy 4: Check for authentication-specific cookies console.log('๐Ÿ” 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(`๐Ÿ“Š Total cookies: ${cookies.length}, Auth cookies found: ${hasAuthCookies}`) } // Strategy 5: Try to detect personal elements by checking page content console.log('๐Ÿ” 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(`โœ… 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('โœ… Found account-specific text content') hasPersonalContent = true } } catch (e) { console.log('โš ๏ธ Error checking personal content:', e) } // Final decision logic console.log('๐Ÿ“Š 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('โœ… User appears to be logged in') this.isAuthenticated = true return true } else { console.log('โŒ User appears to be NOT logged in') this.isAuthenticated = false return false } } catch (error) { console.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('โœ… 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('โš ๏ธ 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('๐Ÿ” 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(`๐ŸŽฏ Trying sign in selector: ${selector}`) const element = this.page.locator(selector).first() if (await element.isVisible({ timeout: 3000 })) { await element.click() console.log(`โœ… 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('โœ… Successfully navigated to login page') loginPageLoaded = true break } } } catch (e) { console.log(`โŒ Sign in selector failed: ${selector}`) continue } } if (loginPageLoaded) break } } catch (e) { console.log(`โŒ 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('๐Ÿ” 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(`๐ŸŽฏ Found email trigger: ${trigger}`) await element.click() console.log('โœ… 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(`๐Ÿ” 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(`๐Ÿ“ 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('โœ… Clicked email button') // Wait for email form to appear await this.page.waitForTimeout(3000) emailFormVisible = true break } } } catch (e) { console.log(`โš ๏ธ Error checking button ${i + 1}:`, e) continue } } } catch (e) { console.log('โŒ 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(`โœ… 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('๐Ÿ” 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(`๐Ÿ” Trying email selector: ${selector}`) if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { emailInput = selector console.log(`โœ… 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(`๐Ÿ” 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(`๐Ÿ“ 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(`๐ŸŽฏ Found email input manually: ${name || type || placeholder}`) emailInput = `input:nth-of-type(${i + 1})` break } } } catch (e) { continue } } } catch (e) { console.log('โŒ 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('โœ… 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(`๐Ÿ” Trying password selector: ${selector}`) if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { passwordInput = selector console.log(`โœ… 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('โœ… Password filled') // Handle potential captcha console.log('๐Ÿค– Checking for captcha...') try { // Look for different types of captcha const captchaSelectors = [ 'iframe[src*="recaptcha"]', 'iframe[src*="captcha"]', '.recaptcha-checkbox', '[data-testid="captcha"]', '.captcha-container' ] let captchaFound = false for (const selector of captchaSelectors) { try { if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { console.log(`๐Ÿค– Captcha detected: ${selector}`) captchaFound = true break } } catch (e) { continue } } if (captchaFound) { console.log('โš ๏ธ Captcha detected - this requires manual intervention') console.log('๐Ÿ–ฑ๏ธ Please solve the captcha manually within 30 seconds...') // Wait for captcha to be solved await this.page.waitForTimeout(30000) console.log('โณ Proceeding after captcha wait period') } } catch (captchaError: any) { console.log('โš ๏ธ 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(`๐Ÿ” Trying submit selector: ${selector}`) if (await this.page.locator(selector).isVisible({ timeout: 2000 })) { submitButton = selector console.log(`โœ… 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(`๐Ÿ” 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(`๐Ÿ“ Submit Button ${i + 1}: "${text}" type="${type}"`) if (type === 'submit' || text.includes('sign in') || text.includes('login') || text.includes('submit')) { console.log(`๐ŸŽฏ Found submit button manually: "${text}"`) submitButton = `button:nth-of-type(${i + 1})` break } } } catch (e) { continue } } } catch (e) { console.log('โŒ 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('โœ… 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('โœ… 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(`โŒ 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('โœ… 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('โŒ Login verification failed') await this.takeDebugScreenshot('login_verification_failed') return false } } catch (error) { console.error('โŒ Login completion timeout or error:', error) await this.takeDebugScreenshot('login_timeout') 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, starting automated login process...') // Try automated login first console.log('๐Ÿค– Attempting automated login...') const autoLoginSuccess = await this.login(credentials) if (autoLoginSuccess) { console.log('โœ… Automated login successful! Saving session for future use.') this.isAuthenticated = true await this.saveSession() return true } console.log('โŒ Automated login failed, this is likely due to captcha protection.') console.log('โš ๏ธ 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 sessionInfo = await this.testSessionPersistence() if (sessionInfo.isValid) { console.log('โœ… 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('โœ… Session persistence worked! Login successful.') this.isAuthenticated = true await this.saveSession() return true } } catch (e) { console.log('โŒ Session persistence test failed:', e) } } console.log('โŒ 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('โŒ 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('โš ๏ธ 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 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') } } /** * Test if session persistence is working and valid */ async testSessionPersistence(): Promise<{ isValid: boolean; cookiesCount: number; hasStorage: boolean; currentUrl: string }> { if (!this.page) { return { isValid: false, cookiesCount: 0, hasStorage: false, currentUrl: 'about:blank' } } try { console.log('๐Ÿงช Testing session persistence...') // Count cookies and check storage const cookies = await this.context?.cookies() || [] const hasLocalStorage = await this.page.evaluate(() => { try { return localStorage.length > 0 } catch { return false } }) const currentUrl = await this.page.url() const result = { isValid: cookies.length > 0 && hasLocalStorage, cookiesCount: cookies.length, hasStorage: hasLocalStorage, currentUrl } console.log('๐Ÿ“Š Current session info:', result) return result } catch (error) { console.error('โŒ Error testing session persistence:', error) return { isValid: false, cookiesCount: 0, hasStorage: false, currentUrl: 'about:blank' } } } /** * 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('โš ๏ธ 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 (const canvas of canvases) { 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 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) // Take screenshot console.log(`Taking screenshot: ${filename}`) await this.page.screenshot({ path: filePath, fullPage: false, type: 'png' }) console.log(`Screenshot saved: ${filename}`) return filePath } catch (error) { console.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, fullPage: true, type: 'png' }) console.log(`Screenshot saved: ${filename}`) } catch (error) { console.log('โš ๏ธ 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('โš ๏ธ Error closing page:', e) } this.page = null } if (this.context) { try { await this.context.close() } catch (e) { console.log('โš ๏ธ Error closing context:', e) } this.context = null } if (this.browser) { try { await this.browser.close() } catch (e) { console.log('โš ๏ธ Error closing browser:', e) } this.browser = null } // Reset flags this.isAuthenticated = false this.operationLock = false this.initPromise = null } catch (error) { console.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(`โœ… 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 } } /** * 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(minMs = 500, maxMs = 2000): Promise { if (!this.humanBehaviorEnabled) return const delay = Math.random() * (maxMs - minMs) + minMs console.log(`โฑ๏ธ Human-like delay: ${Math.round(delay)}ms`) await new Promise(resolve => setTimeout(resolve, delay)) } /** * Simulate human-like mouse movements */ private async simulateHumanMouseMovement(): Promise { if (!this.page || !this.humanBehaviorEnabled) return try { // Random mouse movements const movements = Math.floor(Math.random() * 3) + 2 // 2-4 movements for (let i = 0; i < movements; i++) { const x = Math.random() * 1920 const y = Math.random() * 1080 await this.page.mouse.move(x, y, { steps: Math.floor(Math.random() * 10) + 5 }) await new Promise(resolve => setTimeout(resolve, Math.random() * 300 + 100)) } } catch (error) { console.log('โš ๏ธ Error simulating mouse movement:', error) } } /** * Simulate human-like scrolling */ private async simulateHumanScrolling(): Promise { if (!this.page || !this.humanBehaviorEnabled) return try { const scrollCount = Math.floor(Math.random() * 3) + 1 // 1-3 scrolls for (let i = 0; i < scrollCount; i++) { const direction = Math.random() > 0.5 ? 1 : -1 const distance = (Math.random() * 500 + 200) * direction await this.page.mouse.wheel(0, distance) await new Promise(resolve => setTimeout(resolve, Math.random() * 800 + 300)) } } catch (error) { console.log('โš ๏ธ Error simulating scrolling:', error) } } /** * Throttle requests to avoid suspicious patterns */ private async throttleRequests(): Promise { const now = Date.now() const timeSinceLastRequest = now - this.lastRequestTime const minInterval = 10000 + (this.requestCount * 2000) // Increase delay with request count if (timeSinceLastRequest < minInterval) { const waitTime = minInterval - timeSinceLastRequest console.log(`๐Ÿšฆ Throttling request: waiting ${Math.round(waitTime / 1000)}s before next request (request #${this.requestCount + 1})`) await new Promise(resolve => setTimeout(resolve, waitTime)) } this.lastRequestTime = now this.requestCount++ // Reset request count periodically to avoid indefinite delays if (this.requestCount > 10) { console.log('๐Ÿ”„ Resetting request count for throttling') this.requestCount = 0 } } /** * Generate and store session fingerprint for validation */ private async generateSessionFingerprint(): Promise { if (!this.page) throw new Error('Page not initialized') try { const fingerprint = await this.page.evaluate(() => { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') if (ctx) { ctx.textBaseline = 'top' ctx.font = '14px Arial' ctx.fillText('Session fingerprint', 2, 2) } return JSON.stringify({ userAgent: navigator.userAgent, language: navigator.language, platform: navigator.platform, cookieEnabled: navigator.cookieEnabled, onLine: navigator.onLine, screen: { width: screen.width, height: screen.height, colorDepth: screen.colorDepth }, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, canvas: canvas.toDataURL(), timestamp: Date.now() }) }) this.sessionFingerprint = fingerprint return fingerprint } catch (error) { console.error('โŒ Error generating session fingerprint:', error) return `fallback-${Date.now()}` } } /** * Enhanced session validation using fingerprinting */ private async validateSessionIntegrity(): Promise { if (!this.page) return false try { // Check if TradingView shows any session invalidation indicators const invalidationIndicators = [ 'text="Are you human?"', 'text="Please verify you are human"', 'text="Security check"', '[data-name="captcha"]', '.captcha-container', 'iframe[src*="captcha"]', 'text="Session expired"', 'text="Please log in again"' ] for (const indicator of invalidationIndicators) { try { if (await this.page.locator(indicator).isVisible({ timeout: 1000 })) { console.log(`โš ๏ธ Session invalidation detected: ${indicator}`) return false } } catch (e) { // Ignore timeout errors, continue checking } } // Check if current fingerprint matches stored one if (this.sessionFingerprint) { const currentFingerprint = await this.generateSessionFingerprint() const stored = JSON.parse(this.sessionFingerprint) const current = JSON.parse(currentFingerprint) // Allow some variation in timestamp but check core properties if (stored.userAgent !== current.userAgent || stored.platform !== current.platform || stored.language !== current.language) { console.log('โš ๏ธ Session fingerprint mismatch detected') return false } } return true } catch (error) { console.error('โŒ Error validating session integrity:', error) return false } } /** * Perform human-like interactions before automation */ private async performHumanLikeInteractions(): Promise { if (!this.page || !this.humanBehaviorEnabled) return console.log('๐Ÿค– Performing human-like interactions...') try { // Random combination of human-like behaviors const behaviors = [ () => this.simulateHumanMouseMovement(), () => this.simulateHumanScrolling(), () => this.humanDelay(1000, 3000) ] // Perform 1-2 random behaviors const behaviorCount = Math.floor(Math.random() * 2) + 1 for (let i = 0; i < behaviorCount; i++) { const behavior = behaviors[Math.floor(Math.random() * behaviors.length)] await behavior() } // Wait a bit longer to let the page settle await this.humanDelay(2000, 4000) } catch (error) { console.log('โš ๏ธ Error performing human-like interactions:', error) } } /** * Check if file exists */ private async fileExists(filePath: string): Promise { try { await fs.access(filePath) return true } catch (error) { return false } } /** * Check if browser is healthy and connected */ isBrowserHealthy(): boolean { return !!(this.browser && this.browser.isConnected()) } /** * Ensure browser is ready for operations */ async ensureBrowserReady(): Promise { if (!this.isBrowserHealthy()) { console.log('๐Ÿ”„ Browser not healthy, reinitializing...') await this.forceCleanup() await this.init() } } } // 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()