Files
trading_bot_v3/lib/tradingview-automation.ts
mindesbunister 6e75a7175e Remove demo data fallbacks - use only real Drift account data
- Updated Dashboard.tsx to remove demo data fallbacks
- Updated TradingHistory.tsx to use new Drift trading history endpoint
- Added getTradingHistory method to DriftTradingService
- Created new /api/drift/trading-history endpoint
- Removed fallback demo positions from getPositions method
- All UI components now show only real Drift account data or empty states
- No more hardcoded mock trades or positions
2025-07-13 00:38:24 +02:00

2354 lines
78 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> | 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<void> {
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<void> {
// 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<void> {
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<boolean> {
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<boolean> {
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(`📝
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 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('⚠️ CAPTCHA/Robot verification detected!')
console.log('🚫 This indicates TradingView has flagged this as automated behavior.')
console.log('<27> 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('❌ 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('⚠️ 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<boolean> {
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, checking session persistence options...')
// Before attempting login, check if we have any saved session data
const sessionInfo = await this.testSessionPersistence()
console.log('📊 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('✅ Session restoration successful! Login confirmed.')
this.isAuthenticated = true
await this.saveSession()
return true
} else {
console.log('⚠️ Session restoration failed, saved session may be expired')
}
} catch (e) {
console.log('❌ 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('✅ 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('❌ Automated login failed, this is likely due to captcha protection.')
console.log('⚠️ In Docker environment, manual login is not practical.')
console.log('<27> 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<boolean> {
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<void> {
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<void> {
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<boolean> {
return this.checkLoginStatus()
}
/**
* Wait for chart data to load with enhanced detection
*/
async waitForChartData(): Promise<boolean> {
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<string> {
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<void> {
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<string> {
if (!this.page) return 'about:blank'
return this.page.url()
}
/**
* Enhanced cleanup method
*/
async close(): Promise<void> {
return this.forceCleanup()
}
/**
* Force cleanup of browser resources
*/
async forceCleanup(): Promise<void> {
// 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<void> {
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<void> {
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<void> {
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<boolean> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<string> {
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<boolean> {
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<void> {
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)
}
}
/**
* Mark that a captcha was detected (to implement cooldown)
*/
private async markCaptchaDetected(): Promise<void> {
try {
const captchaMarkerFile = path.join(process.cwd(), 'captcha_detected.json')
const markerData = {
timestamp: new Date().toISOString(),
count: 1
}
// If marker already exists, increment count
if (await this.fileExists(captchaMarkerFile)) {
try {
const existing = JSON.parse(await fs.readFile(captchaMarkerFile, 'utf8'))
markerData.count = (existing.count || 0) + 1
} catch (e) {
// Use defaults if can't read existing file
}
}
await fs.writeFile(captchaMarkerFile, JSON.stringify(markerData, null, 2))
console.log(`📝 Marked captcha detection #${markerData.count} at ${markerData.timestamp}`)
} catch (error) {
console.log('⚠️ Error marking captcha detection:', error)
}
}
/**
* Check if file exists
*/
private async fileExists(filePath: string): Promise<boolean> {
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<void> {
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()