Files
trading_bot_v3/lib/tradingview-automation.ts
mindesbunister 836455d1a4 feat: implement real Drift trading history using SDK methods
- Updated getTradingHistory to fetch actual Drift order records
- Added fallback to local database for trade history
- Enhanced executeTrade to store trades in database for history tracking
- Fixed hydration issues in AutoTradingPanel and TradingHistory components
- Improved error handling and logging for trading history retrieval
2025-07-13 02:24:12 +02:00

2354 lines
78 KiB
TypeScript
Raw Blame History

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('SUCCESS: Browser already initialized and connected')
return
}
this.initPromise = this._doInit()
try {
await this.initPromise
} finally {
this.initPromise = null
}
} finally {
this.releaseOperationLock()
}
}
private async _doInit(): Promise<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('ERROR: Failed to launch browser:', error)
// Cleanup any partial state
await this.forceCleanup()
throw new Error('Failed to launch browser: ' + error)
}
if (!this.browser) {
throw new Error('Failed to launch browser')
}
// Create browser context with enhanced stealth features
this.context = await this.browser.newContext({
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
viewport: { width: 1920, height: 1080 },
// Enhanced HTTP headers to appear more human-like
extraHTTPHeaders: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'en-US,en;q=0.9,de;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'max-age=0',
'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Linux"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
'Dnt': '1'
},
// Additional context options for stealth
javaScriptEnabled: true,
acceptDownloads: false,
bypassCSP: false,
colorScheme: 'light',
deviceScaleFactor: 1,
hasTouch: false,
isMobile: false,
locale: 'en-US',
permissions: ['geolocation'],
timezoneId: 'America/New_York'
})
if (!this.context) {
throw new Error('Failed to create browser context')
}
// Load saved session if available
await this.loadSession()
this.page = await this.context.newPage()
if (!this.page) {
throw new Error('Failed to create new page')
}
// Add enhanced stealth measures to reduce bot detection
await this.page.addInitScript(() => {
// Remove webdriver property completely
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
// Enhanced plugins simulation
Object.defineProperty(navigator, 'plugins', {
get: () => {
const plugins = [
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
{ name: 'Native Client', filename: 'internal-nacl-plugin' }
];
plugins.length = 3;
return plugins;
},
});
// Enhanced language simulation
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en', 'de'],
});
// Mock hardware concurrency
Object.defineProperty(navigator, 'hardwareConcurrency', {
get: () => 8,
});
// Mock device memory
Object.defineProperty(navigator, 'deviceMemory', {
get: () => 8,
});
// Mock connection
Object.defineProperty(navigator, 'connection', {
get: () => ({
effectiveType: '4g',
rtt: 50,
downlink: 10
}),
});
// Override permissions API with realistic responses
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters: any) => {
const permission = parameters.name;
let state = 'prompt';
if (permission === 'notifications') {
state = 'default';
} else if (permission === 'geolocation') {
state = 'prompt';
}
return Promise.resolve({
state: state,
name: permission,
onchange: null,
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false
} as PermissionStatus);
};
// Mock WebGL fingerprinting resistance
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter: any) {
if (parameter === 37445) { // UNMASKED_VENDOR_WEBGL
return 'Intel Inc.';
}
if (parameter === 37446) { // UNMASKED_RENDERER_WEBGL
return 'Intel Iris OpenGL Engine';
}
return getParameter.call(this, parameter);
};
// Mock screen properties to be consistent
Object.defineProperties(screen, {
width: { get: () => 1920 },
height: { get: () => 1080 },
availWidth: { get: () => 1920 },
availHeight: { get: () => 1040 },
colorDepth: { get: () => 24 },
pixelDepth: { get: () => 24 }
});
// Remove automation detection markers
delete (window as any).chrome.runtime.onConnect;
delete (window as any).chrome.runtime.onMessage;
// Mock battery API
Object.defineProperty(navigator, 'getBattery', {
get: () => () => Promise.resolve({
charging: true,
chargingTime: 0,
dischargingTime: Infinity,
level: 1
}),
});
})
console.log('SUCCESS: Browser and session initialized successfully')
}
/**
* Check if user is already logged in to TradingView
*/
async checkLoginStatus(): Promise<boolean> {
if (!this.page) return false
try {
console.log('CHECKING: Checking login status...')
// Navigate to TradingView if not already there
const currentUrl = await this.page.url()
if (!currentUrl.includes('tradingview.com')) {
console.log('📄 Navigating to TradingView...')
await this.page.goto('https://www.tradingview.com', {
waitUntil: 'domcontentloaded',
timeout: 30000
})
// Restore session storage after navigation
await this.restoreSessionStorage()
// Wait for page to settle
await this.page.waitForTimeout(5000)
}
// Take a debug screenshot to see the current state
await this.takeDebugScreenshot('login_status_check')
// Enhanced login detection with multiple strategies
console.log('CHECKING: Strategy 1: Checking for user account indicators...')
// Strategy 1: Look for user account elements (more comprehensive)
const userAccountSelectors = [
// User menu and profile elements
'[data-name="header-user-menu"]',
'[data-name="user-menu"]',
'.tv-header__user-menu-button:not(.tv-header__user-menu-button--anonymous)',
'.tv-header__user-menu',
'.js-header-user-menu-button',
// Account/profile indicators
'[data-name="account-menu"]',
'[data-testid="header-user-menu"]',
'.tv-header__dropdown-toggle',
// Watchlist indicators (user-specific)
'[data-name="watchlist-button"]',
'.tv-header__watchlist-button',
'.tv-watchlist-container',
// Personal layout elements
'button:has-text("M")', // Watchlist "M" button
'[data-name="watchlist-dropdown"]',
// Pro/subscription indicators
'.tv-header__pro-button',
'[data-name="go-pro-button"]'
]
let foundUserElement = false
for (const selector of userAccountSelectors) {
try {
if (await this.page.locator(selector).isVisible({ timeout: 1500 })) {
console.log("SUCCESS: Found user account element: " + selector) + ")"
foundUserElement = true
break
}
} catch (e) {
continue
}
}
// Strategy 2: Check for sign-in/anonymous indicators (should NOT be present if logged in)
console.log('CHECKING: Strategy 2: Checking for anonymous/sign-in indicators...')
const anonymousSelectors = [
// Sign in buttons/links
'a[href*="signin"]',
'a[href*="/accounts/signin"]',
'button:has-text("Sign in")',
'button:has-text("Log in")',
'a:has-text("Sign in")',
'a:has-text("Log in")',
// Anonymous user indicators
'.tv-header__user-menu-button--anonymous',
'[data-name="header-user-menu-sign-in"]',
'.tv-header__sign-in',
// Guest mode indicators
'text="Continue as guest"',
'text="Sign up"',
'button:has-text("Sign up")'
]
let foundAnonymousElement = false
for (const selector of anonymousSelectors) {
try {
if (await this.page.locator(selector).isVisible({ timeout: 1500 })) {
console.log(`ERROR: Found anonymous indicator: ${selector} - not logged in`)
foundAnonymousElement = true
break
}
} catch (e) {
continue
}
}
// Strategy 3: Check page URL patterns for authentication
console.log('CHECKING: Strategy 3: Checking URL patterns...')
const url = await this.page.url()
const isOnLoginPage = url.includes('/accounts/signin') ||
url.includes('/signin') ||
url.includes('/login')
if (isOnLoginPage) {
console.log("ERROR: Currently on login page: " + url) + ")"
this.isAuthenticated = false
return false
}
// Strategy 4: Check for authentication-specific cookies
console.log('CHECKING: Strategy 4: Checking authentication cookies...')
let hasAuthCookies = false
if (this.context) {
const cookies = await this.context.cookies()
const authCookieNames = [
'sessionid',
'auth_token',
'user_token',
'session_token',
'authentication',
'logged_in',
'tv_auth',
'tradingview_auth'
]
for (const cookie of cookies) {
if (authCookieNames.some(name => cookie.name.toLowerCase().includes(name.toLowerCase()))) {
console.log("🍪 Found potential auth cookie: " + cookie.name) + ")"
hasAuthCookies = true
break
}
}
console.log('DATA: Total cookies: ' + cookies.length + ', Auth cookies found: ' + hasAuthCookies)
}
// Strategy 5: Try to detect personal elements by checking page content
console.log('CHECKING: Strategy 5: Checking for personal content...')
let hasPersonalContent = false
try {
// Look for elements that indicate a logged-in user
const personalContentSelectors = [
// Watchlist with custom symbols
'.tv-screener-table',
'.tv-widget-watch-list',
// Personal layout elements
'.layout-with-border-radius',
'.tv-chart-view',
// Settings/customization elements available only to logged-in users
'[data-name="chart-settings"]',
'[data-name="chart-properties"]'
]
for (const selector of personalContentSelectors) {
try {
if (await this.page.locator(selector).isVisible({ timeout: 1000 })) {
console.log("SUCCESS: Found personal content: " + selector) + ")"
hasPersonalContent = true
break
}
} catch (e) {
continue
}
}
// Additional check: see if we can access account-specific features
const pageText = await this.page.textContent('body') || ''
if (pageText.includes('My Watchlist') ||
pageText.includes('Portfolio') ||
pageText.includes('Alerts') ||
pageText.includes('Account')) {
console.log('SUCCESS: Found account-specific text content')
hasPersonalContent = true
}
} catch (e) {
console.log('WARNING: Error checking personal content:', e)
}
// Final decision logic
console.log('DATA: Login detection summary:')
console.log(" User elements found: " + foundUserElement) + ")"
console.log(" Anonymous elements found: " + foundAnonymousElement) + ")"
console.log(" On login page: " + isOnLoginPage) + ")"
console.log(" Has auth cookies: " + hasAuthCookies) + ")"
console.log(" Has personal content: " + hasPersonalContent) + ")"
// Determine login status based on multiple indicators
const isLoggedIn = (foundUserElement || hasPersonalContent || hasAuthCookies) &&
!foundAnonymousElement &&
!isOnLoginPage
if (isLoggedIn) {
console.log('SUCCESS: User appears to be logged in')
this.isAuthenticated = true
return true
} else {
console.log('ERROR: User appears to be NOT logged in')
this.isAuthenticated = false
return false
}
} catch (error) {
console.error('ERROR: Error checking login status:', error)
await this.takeDebugScreenshot('login_status_error')
this.isAuthenticated = false
return false
}
}
async login(credentials?: TradingViewCredentials): Promise<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('SUCCESS: Already logged in, skipping login steps')
return true
}
console.log('🔐 Starting login process...')
// Clear any existing session first to ensure clean login
if (this.context) {
try {
await this.context.clearCookies()
console.log('🧹 Cleared existing cookies for clean login')
} catch (e) {
console.log('WARNING: Could not clear cookies:', e)
}
}
// Navigate to login page with multiple attempts
console.log('📄 Navigating to TradingView login page...')
const loginUrls = [
'https://www.tradingview.com/accounts/signin/',
'https://www.tradingview.com/'
]
let loginPageLoaded = false
for (const url of loginUrls) {
try {
console.log("🔄 Trying URL: " + url) + ")"
await this.page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30000
})
// Wait for page to settle
await this.page.waitForTimeout(3000)
const currentUrl = await this.page.url()
console.log("📍 Current URL after navigation: " + currentUrl) + ")"
if (currentUrl.includes('signin') || currentUrl.includes('login')) {
loginPageLoaded = true
break
} else if (url === 'https://www.tradingview.com/') {
// Try to find and click sign in button
console.log('CHECKING: Looking for Sign In button on main page...')
const signInSelectors = [
'a[href*="signin"]:visible',
'a[href*="/accounts/signin/"]:visible',
'button:has-text("Sign in"):visible',
'a:has-text("Sign in"):visible',
'.tv-header__user-menu-button--anonymous:visible',
'[data-name="header-user-menu-sign-in"]:visible'
]
for (const selector of signInSelectors) {
try {
console.log("TARGET: Trying sign in selector: " + selector) + ")"
const element = this.page.locator(selector).first()
if (await element.isVisible({ timeout: 3000 })) {
await element.click()
console.log("SUCCESS: Clicked sign in button: " + selector) + ")"
// Wait for navigation to login page
await this.page.waitForTimeout(3000)
const newUrl = await this.page.url()
if (newUrl.includes('signin') || newUrl.includes('login')) {
console.log('SUCCESS: Successfully navigated to login page')
loginPageLoaded = true
break
}
}
} catch (e) {
console.log("ERROR: Sign in selector failed: " + selector) + ")"
continue
}
}
if (loginPageLoaded) break
}
} catch (e) {
console.log(`ERROR: Failed to load ${url}:`, e)
continue
}
}
if (!loginPageLoaded) {
throw new Error('Could not reach TradingView login page')
}
// Take screenshot of login page
await this.takeDebugScreenshot('login_page_loaded')
// Wait for login form to be ready
console.log('⏳ Waiting for login form to be ready...')
await this.page.waitForTimeout(5000)
// CRITICAL: Look for and click "Email" button if present (TradingView uses this pattern)
console.log('CHECKING: Looking for Email login option...')
// First try Playwright locator approach
const emailTriggers = [
'button:has-text("Email")',
'button:has-text("email")',
'text="Email"',
'text="Continue with email"',
'text="Sign in with email"',
'[data-name="email"]',
'[data-testid="email-button"]'
]
let emailFormVisible = false
for (const trigger of emailTriggers) {
try {
const element = this.page.locator(trigger).first()
if (await element.isVisible({ timeout: 2000 })) {
console.log("TARGET: Found email trigger: " + trigger) + ")"
await element.click()
console.log('SUCCESS: Clicked email trigger')
// Wait for email form to appear
await this.page.waitForTimeout(3000)
emailFormVisible = true
break
}
} catch (e) {
continue
}
}
// If locator approach failed, use manual button enumeration (like old working code)
if (!emailFormVisible) {
console.log('🔄 Locator approach failed, trying manual button search...')
try {
// Wait for buttons to be available
await this.page.waitForSelector('button', { timeout: 10000 })
// Get all buttons and check their text content
const buttons = await this.page.locator('button').all()
console.log(`CHECKING: Found ${buttons.length} buttons to check`)
for (let i = 0; i < buttons.length; i++) {
try {
const button = buttons[i]
if (await button.isVisible({ timeout: 1000 })) {
const text = await button.textContent() || ''
const trimmedText = text.trim().toLowerCase()
console.log(`INFO: Button ${i + 1}: "${trimmedText}"`)
if (trimmedText.includes('email') ||
trimmedText.includes('continue with email') ||
trimmedText.includes('sign in with email')) {
console.log('Found email button: ' + trimmedText)
await button.click()
console.log('SUCCESS: Clicked email button')
// Wait for email form to appear
await this.page.waitForTimeout(3000)
emailFormVisible = true
break
}
}
} catch (e) {
console.log('WARNING: Error checking button ' + (i + 1) + ':', e)
continue
}
}
} catch (e) {
console.log('ERROR: Manual button search failed:', e)
}
}
// Check if email input is now visible
if (!emailFormVisible) {
// Look for email input directly (might already be visible)
const emailInputSelectors = [
'input[type="email"]',
'input[name*="email"]',
'input[name*="username"]',
'input[name="username"]', // TradingView often uses this
'input[placeholder*="email" i]',
'input[placeholder*="username" i]'
]
for (const selector of emailInputSelectors) {
try {
if (await this.page.locator(selector).isVisible({ timeout: 1000 })) {
console.log('SUCCESS: Email input already visible: ' + selector)
emailFormVisible = true
break
}
} catch (e) {
continue
}
}
}
if (!emailFormVisible) {
await this.takeDebugScreenshot('no_email_form')
// Additional debugging: show what elements are available
const availableElements = await this.page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button')).map(btn => btn.textContent?.trim()).filter(Boolean)
const inputs = Array.from(document.querySelectorAll('input')).map(input => ({
type: input.type,
name: input.name,
placeholder: input.placeholder
}))
const forms = Array.from(document.querySelectorAll('form')).length
return { buttons, inputs, forms }
})
console.log('CHECKING: Available elements:', JSON.stringify(availableElements, null, 2))
throw new Error('Could not find or activate email login form')
}
// Find and fill email field
console.log('📧 Looking for email input field...')
const emailSelectors = [
'input[name="username"]', // TradingView commonly uses this
'input[type="email"]',
'input[name="email"]',
'input[name="id_username"]',
'input[placeholder*="email" i]',
'input[placeholder*="username" i]',
'input[autocomplete="username"]',
'input[autocomplete="email"]',
'form input[type="text"]:not([type="password"])'
]
let emailInput = null
for (const selector of emailSelectors) {
try {
console.log('CHECKING: Trying email selector: ' + selector)
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
emailInput = selector
console.log('SUCCESS: Found email input: ' + selector)
break
}
} catch (e) {
continue
}
}
if (!emailInput) {
// Try manual search like the old code
console.log('🔄 Selector approach failed, trying manual input search...')
try {
const inputs = await this.page.locator('input').all()
console.log(`CHECKING: Found ${inputs.length} inputs to check`)
for (let i = 0; i < inputs.length; i++) {
try {
const input = inputs[i]
if (await input.isVisible({ timeout: 1000 })) {
const type = await input.getAttribute('type') || ''
const name = await input.getAttribute('name') || ''
const placeholder = await input.getAttribute('placeholder') || ''
console.log(`INFO: Input ${i + 1}: type="${type}" name="${name}" placeholder="${placeholder}"`)
if (type === 'email' ||
name.toLowerCase().includes('email') ||
name.toLowerCase().includes('username') ||
placeholder.toLowerCase().includes('email') ||
placeholder.toLowerCase().includes('username')) {
console.log(`TARGET: Found email input manually: ${name || type || placeholder}`)
emailInput = `input:nth-of-type(${i + 1})`
break
}
}
} catch (e) {
continue
}
}
} catch (e) {
console.log('ERROR: Manual input search failed:', e)
}
}
if (!emailInput) {
await this.takeDebugScreenshot('no_email_input')
throw new Error('Could not find email input field')
}
// Fill email
console.log('📧 Filling email field...')
await this.page.fill(emailInput, email)
console.log('SUCCESS: Email filled')
// Find and fill password field
console.log('🔑 Looking for password input field...')
const passwordSelectors = [
'input[type="password"]',
'input[name="password"]',
'input[name="id_password"]',
'input[placeholder*="password" i]',
'input[autocomplete="current-password"]'
]
let passwordInput = null
for (const selector of passwordSelectors) {
try {
console.log("CHECKING: Trying password selector: " + selector) + ")"
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
passwordInput = selector
console.log("SUCCESS: Found password input: " + selector) + ")"
break
}
} catch (e) {
continue
}
}
if (!passwordInput) {
await this.takeDebugScreenshot('no_password_input')
throw new Error('Could not find password input field')
}
// Fill password
console.log('🔑 Filling password field...')
await this.page.fill(passwordInput, password)
console.log('SUCCESS: Password filled')
// Handle potential captcha
console.log('🤖 Checking for captcha...')
try {
// Look for different types of captcha and robot confirmation
const captchaSelectors = [
'iframe[src*="recaptcha"]',
'iframe[src*="captcha"]',
'.recaptcha-checkbox',
'[data-testid="captcha"]',
'.captcha-container',
'text="Please confirm that you are not a robot"',
'text="Are you human?"',
'text="Please verify you are human"',
'text="Security check"',
'.tv-dialog__error:has-text("robot")',
'.alert:has-text("robot")',
'.error:has-text("robot")'
]
let captchaFound = false
let captchaType = ''
for (const selector of captchaSelectors) {
try {
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
console.log("🤖 Captcha/Robot check detected: " + selector) + ")"
captchaFound = true
captchaType = selector
break
}
} catch (e) {
continue
}
}
if (captchaFound) {
console.log('WARNING: CAPTCHA/Robot verification detected!')
console.log('🚫 This indicates TradingView has flagged this as automated behavior.')
console.log('<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('ERROR: Cannot proceed with automated login due to captcha protection.')
console.log('🔧 Possible solutions:')
console.log(' 1. Use a different IP address or VPN')
console.log(' 2. Wait some time before retrying (rate limiting)')
console.log(' 3. Use session persistence from a manually authenticated browser')
console.log(' 4. Contact TradingView support if this persists')
// Mark captcha detection for future reference
await this.markCaptchaDetected()
// Return false immediately instead of waiting
throw new Error(`Captcha detected (${captchaType}) - automated login blocked`)
}
} catch (captchaError: any) {
if (captchaError.message.includes('Captcha detected')) {
throw captchaError
}
console.log('WARNING: Captcha check failed:', captchaError?.message)
}
// Find and click submit button
console.log('🔘 Looking for submit button...')
const submitSelectors = [
'button[type="submit"]',
'input[type="submit"]',
'button:has-text("Sign in")',
'button:has-text("Log in")',
'button:has-text("Login")',
'button:has-text("Sign In")',
'.tv-button--primary',
'form button:not([type="button"])',
'[data-testid="signin-button"]',
'[data-testid="login-button"]'
]
let submitButton = null
for (const selector of submitSelectors) {
try {
console.log("CHECKING: Trying submit selector: " + selector) + ")"
if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
submitButton = selector
console.log("SUCCESS: Found submit button: " + selector) + ")"
break
}
} catch (e) {
continue
}
}
if (!submitButton) {
// Try manual search like the old code
console.log('🔄 Selector approach failed, trying manual button search for submit...')
try {
const buttons = await this.page.locator('button').all()
console.log(`CHECKING: Found ${buttons.length} buttons to check for submit`)
for (let i = 0; i < buttons.length; i++) {
try {
const button = buttons[i]
if (await button.isVisible({ timeout: 1000 })) {
const text = (await button.textContent() || '').toLowerCase()
const type = await button.getAttribute('type') || ''
console.log(`INFO: Submit Button ${i + 1}: "${text}" type="${type}"`)
if (type === 'submit' ||
text.includes('sign in') ||
text.includes('login') ||
text.includes('submit')) {
console.log(`TARGET: Found submit button manually: "${text}"`)
submitButton = `button:nth-of-type(${i + 1})`
break
}
}
} catch (e) {
continue
}
}
} catch (e) {
console.log('ERROR: Manual submit button search failed:', e)
}
}
if (!submitButton) {
await this.takeDebugScreenshot('no_submit_button')
throw new Error('Could not find submit button')
}
// Click submit button
console.log('🖱️ Clicking submit button...')
await this.page.click(submitButton)
console.log('SUCCESS: Submit button clicked')
// Wait for login to complete
console.log('⏳ Waiting for login to complete...')
try {
// Wait for login completion without using waitForFunction (CSP violation)
// Instead, check URL and elements periodically
let attempts = 0
let maxAttempts = 15 // Reduced to 15 seconds with 1 second intervals
let loginDetected = false
while (attempts < maxAttempts && !loginDetected) {
await this.page.waitForTimeout(1000) // Wait 1 second
attempts++
console.log('Login check attempt ' + attempts + '/' + maxAttempts)
// Check if we navigated away from login page
const currentUrl = await this.page.url()
console.log("📍 Current URL: " + currentUrl) + ")"
const notOnLoginPage = !currentUrl.includes('/accounts/signin') && !currentUrl.includes('/signin')
// Check for user-specific elements
let hasUserElements = false
try {
const userElement = await this.page.locator(
'[data-name="watchlist-button"], .tv-header__user-menu-button:not(.tv-header__user-menu-button--anonymous), [data-name="user-menu"]'
).first()
hasUserElements = await userElement.isVisible({ timeout: 500 })
if (hasUserElements) {
console.log('SUCCESS: Found user-specific elements')
}
} catch (e) {
// Element not found, continue checking
}
// Check for error messages
try {
const errorSelectors = [
'.tv-dialog__error',
'.error-message',
'[data-testid="error"]',
'.alert-danger'
]
for (const selector of errorSelectors) {
if (await this.page.locator(selector).isVisible({ timeout: 500 })) {
const errorText = await this.page.locator(selector).textContent()
console.log('ERROR: Login error detected: ' + errorText)
throw new Error('Login failed: ' + errorText)
}
}
} catch (e) {
if (e instanceof Error && e.message.includes('Login failed:')) {
throw e
}
// Continue if just element not found
}
if (notOnLoginPage || hasUserElements) {
loginDetected = true
console.log('🎉 Navigation/elements suggest login success!')
break
}
}
if (!loginDetected) {
throw new Error('Login verification timeout - no success indicators found')
}
// Additional wait for page to fully load
await this.page.waitForTimeout(5000)
// Verify login status with enhanced detection
const loginSuccessful = await this.checkLoginStatus()
if (loginSuccessful) {
console.log('SUCCESS: Login verified successful!')
this.isAuthenticated = true
// Save session for future use
await this.saveSession()
console.log('💾 Session saved for future use')
return true
} else {
console.log('ERROR: Login verification failed')
await this.takeDebugScreenshot('login_verification_failed')
return false
}
} catch (error) {
console.error('ERROR: Login completion timeout or error:', error)
await this.takeDebugScreenshot('login_timeout')
return false
}
} catch (error) {
console.error('ERROR: Login failed:', error)
await this.takeDebugScreenshot('login_error')
return false
}
}
/**
* Smart login that prioritizes session persistence to avoid captchas
*/
async smartLogin(credentials?: TradingViewCredentials): Promise<boolean> {
if (!this.page) throw new Error('Page not initialized')
try {
console.log('CHECKING: Attempting smart login with session persistence...')
// First check if already logged in
const alreadyLoggedIn = await this.checkLoginStatus()
if (alreadyLoggedIn) {
console.log('🎉 Already logged in to TradingView! Skipping login process.')
await this.saveSession() // Save current session
return true
}
console.log('🔐 Not logged in, checking session persistence options...')
// Before attempting login, check if we have any saved session data
const sessionInfo = await this.testSessionPersistence()
console.log('DATA: Session persistence check:', sessionInfo)
if (sessionInfo.cookiesCount > 0) {
console.log('🍪 Found saved session data, attempting to restore...')
// Try navigating to TradingView with saved session first
try {
await this.page.goto('https://www.tradingview.com/', {
waitUntil: 'domcontentloaded',
timeout: 30000
})
// Restore session storage after navigation
await this.restoreSessionStorage()
// Wait for page to settle and check login status
await this.page.waitForTimeout(5000)
const nowLoggedIn = await this.checkLoginStatus()
if (nowLoggedIn) {
console.log('SUCCESS: Session restoration successful! Login confirmed.')
this.isAuthenticated = true
await this.saveSession()
return true
} else {
console.log('WARNING: Session restoration failed, saved session may be expired')
}
} catch (e) {
console.log('ERROR: Session restoration attempt failed:', e)
}
}
// Only attempt automated login if we don't have valid session data
console.log('🤖 Attempting automated login...')
try {
const autoLoginSuccess = await this.login(credentials)
if (autoLoginSuccess) {
console.log('SUCCESS: Automated login successful! Saving session for future use.')
this.isAuthenticated = true
await this.saveSession()
return true
}
} catch (loginError: any) {
if (loginError.message.includes('Captcha detected')) {
console.log('🚫 Captcha protection encountered during automated login.')
console.log('💡 To resolve this issue:')
console.log(' 1. Clear any existing session data: docker-compose exec app rm -f /app/session_*.json')
console.log(' 2. Wait 1-2 hours before retrying (to clear rate limiting)')
console.log(' 3. Consider using a different IP address or VPN')
console.log(' 4. Manually log in to TradingView in a browser to establish a valid session')
return false
}
throw loginError
}
console.log('ERROR: Automated login failed, this is likely due to captcha protection.')
console.log('WARNING: In Docker environment, manual login is not practical.')
console.log('<27> Checking if we can proceed with session persistence...')
// Try to check if there are any existing valid session cookies
const fallbackSessionInfo = await this.testSessionPersistence()
if (fallbackSessionInfo.isValid) {
console.log('SUCCESS: Found valid session data, attempting to use it...')
try {
// Navigate to main TradingView page to test session
await this.page.goto('https://www.tradingview.com/', {
waitUntil: 'domcontentloaded',
timeout: 30000
})
await this.page.waitForTimeout(5000)
const nowLoggedIn = await this.checkLoginStatus()
if (nowLoggedIn) {
console.log('SUCCESS: Session persistence worked! Login successful.')
this.isAuthenticated = true
await this.saveSession()
return true
}
} catch (e) {
console.log('ERROR: Session persistence test failed:', e)
}
}
console.log('ERROR: All login methods failed. This may require manual intervention.')
console.log('💡 To fix: Log in manually in a browser with the same credentials and restart the application.')
return false
} catch (error) {
console.error('ERROR: Smart login failed:', error)
return false
}
}
async navigateToChart(options: NavigationOptions = {}): Promise<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('WARNING: Session integrity compromised, may require re-authentication')
}
const { symbol = 'SOLUSD', timeframe = '5', waitForChart = true } = options
console.log('Navigating to chart page...')
// Perform human-like interactions before navigation
await this.performHumanLikeInteractions()
// Generate session fingerprint for tracking
await this.generateSessionFingerprint()
// Navigate to chart page with more flexible waiting strategy
try {
await this.page.goto('https://www.tradingview.com/chart/', {
waitUntil: 'domcontentloaded', // More lenient than 'networkidle'
timeout: 45000 // Increased timeout
})
} catch (error) {
console.log('Standard navigation failed, trying fallback...')
// Fallback: navigate without waiting for full network idle
await this.page.goto('https://www.tradingview.com/chart/', {
waitUntil: 'load',
timeout: 30000
})
}
// Human-like delay after navigation
await this.humanDelay(2000, 4000)
// Wait for chart to load
if (waitForChart) {
console.log('Waiting for chart container...')
try {
await this.page.waitForSelector('.chart-container, #tv_chart_container, .tv-layout', { timeout: 30000 })
console.log('Chart container found')
} catch (error) {
console.log('Chart container not found with standard selectors, trying alternatives...')
// Try alternative selectors for chart elements
const chartSelectors = [
'.chart-widget',
'.tradingview-widget-container',
'[data-name="chart"]',
'canvas',
'.tv-chart'
]
let chartFound = false
for (const selector of chartSelectors) {
try {
await this.page.waitForSelector(selector, { timeout: 5000 })
console.log("Chart found with selector: " + selector) + ")"
chartFound = true
break
} catch (e) {
continue
}
}
if (!chartFound) {
console.log('No chart container found, proceeding anyway...')
}
}
// Additional wait for chart initialization with human-like delay
await this.humanDelay(3000, 6000)
}
// Change symbol if not BTC
if (symbol !== 'BTCUSD') {
console.log(`Changing symbol to ${symbol}...`)
await this.changeSymbol(symbol)
}
// Change timeframe if specified
if (timeframe) {
console.log(`Setting timeframe to ${timeframe}...`)
await this.changeTimeframe(timeframe)
}
console.log(`Successfully navigated to ${symbol} chart with ${timeframe} timeframe`)
return true
} catch (error) {
console.error('Navigation to chart failed:', error)
await this.takeDebugScreenshot('navigation_failed')
return false
}
}
private async changeSymbol(symbol: string): Promise<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('TARGET: Looking for interval legend to open timeframe selector...')
const intervalLegendSelectors = [
'[data-name="legend-source-interval"]',
'.intervalTitle-l31H9iuA',
'[title="Change interval"]',
'.intervalTitle-l31H9iuA button',
'[data-name="legend-source-interval"] button'
]
let intervalLegendClicked = false
for (const selector of intervalLegendSelectors) {
try {
console.log("Trying interval legend selector: " + selector) + ")"
const element = this.page.locator(selector).first()
if (await element.isVisible({ timeout: 3000 })) {
console.log("SUCCESS: Found interval legend: " + selector) + ")"
await element.click()
await this.page.waitForTimeout(2000)
console.log('🖱️ Clicked interval legend - timeframe selector should be open')
intervalLegendClicked = true
break
}
} catch (e) {
console.log(`Interval legend selector ${selector} not found`)
}
}
if (!intervalLegendClicked) {
console.log('ERROR: Could not find interval legend to click')
await this.takeDebugScreenshot('no_interval_legend')
return
}
// Now look for timeframe options in the opened selector
console.log('CHECKING: Looking for timeframe options in selector...')
for (const tf of timeframesToTry) {
const timeframeSelectors = [
// After clicking interval legend, look for options
`[data-value="${tf}"]`,
`button:has-text("${tf}")`,
`.tv-dropdown__item:has-text("${tf}")`,
`.tv-interval-item:has-text("${tf}")`,
`[title="${tf}"]`,
`[aria-label*="${tf}"]`,
// Look in the opened dropdown/menu
`.tv-dropdown-behavior__body [data-value="${tf}"]`,
`.tv-dropdown-behavior__body button:has-text("${tf}")`,
// Look for list items or menu items
`li:has-text("${tf}")`,
`div[role="option"]:has-text("${tf}")`,
`[role="menuitem"]:has-text("${tf}")`,
// TradingView specific interval selectors
`.tv-screener-table__row:has-text("${tf}")`,
`.tv-interval-tabs button:has-text("${tf}")`,
`.intervals-GwQQdU8S [data-value="${tf}"]`,
// Generic selectors in visible containers
`.tv-dialog [data-value="${tf}"]`,
`.tv-dialog button:has-text("${tf}")`
]
for (const selector of timeframeSelectors) {
try {
console.log("Trying timeframe option selector: " + selector) + ")"
const element = this.page.locator(selector).first()
// Check if element exists and is visible
const isVisible = await element.isVisible({ timeout: 2000 })
if (isVisible) {
console.log("SUCCESS: Found timeframe option: " + selector) + ")"
await element.click()
await this.page.waitForTimeout(2000)
console.log("🎉 Successfully clicked timeframe option for " + tf) + ")"
found = true
break
}
} catch (e) {
console.log(`Timeframe option selector ${selector} not found or not clickable`)
}
}
if (found) break
}
// Fallback: Try keyboard navigation
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("SUCCESS: Successfully changed timeframe to " + timeframe) + ")"
await this.takeDebugScreenshot('after_timeframe_change')
} else {
console.log(`ERROR: Could not change timeframe to ${timeframe} - timeframe options not found`)
// Take a debug screenshot to see current state
await this.takeDebugScreenshot('timeframe_change_failed')
// Log all visible elements that might be timeframe related
try {
const visibleElements = await this.page.$$eval('[data-value], button, [role="option"], [role="menuitem"], li', (elements: Element[]) =>
elements
.filter((el: Element) => {
const style = window.getComputedStyle(el)
return style.display !== 'none' && style.visibility !== 'hidden'
})
.slice(0, 20)
.map((el: Element) => ({
tagName: el.tagName,
text: el.textContent?.trim().substring(0, 20),
className: el.className.substring(0, 50),
dataValue: el.getAttribute('data-value'),
role: el.getAttribute('role'),
outerHTML: el.outerHTML.substring(0, 150)
}))
)
console.log('Visible interactive elements:', JSON.stringify(visibleElements, null, 2))
} catch (e) {
console.log('Could not analyze visible elements')
}
}
} catch (error) {
console.error('Failed to change timeframe:', error)
await this.takeDebugScreenshot('timeframe_change_error')
}
}
/**
* 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('DATA: Current session info:', result)
return result
} catch (error) {
console.error('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('WARNING: No chart elements found')
return false
}
// Additional wait for chart data to load
await this.humanDelay(3000, 6000)
// Check if chart appears to have data (not just loading screen)
const hasData = await this.page.evaluate(() => {
const canvases = document.querySelectorAll('canvas')
for (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: 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: 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('WARNING: 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('WARNING: Error closing page:', e)
}
this.page = null
}
if (this.context) {
try {
await this.context.close()
} catch (e) {
console.log('WARNING: Error closing context:', e)
}
this.context = null
}
if (this.browser) {
try {
await this.browser.close()
} catch (e) {
console.log('WARNING: Error closing browser:', e)
}
this.browser = null
}
// Reset flags
this.isAuthenticated = false
this.operationLock = false
this.initPromise = null
} catch (error) {
console.error('ERROR: Error during force cleanup:', error)
}
}
/**
* Reset the singleton instance (useful for testing or forcing recreation)
*/
static resetInstance(): void {
if (TradingViewAutomation.instance) {
TradingViewAutomation.instance.forceCleanup().catch(console.error)
TradingViewAutomation.instance = null
}
}
/**
* Load saved session data (cookies, localStorage, etc.)
*/
private async loadSession(): Promise<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(`SUCCESS: Loaded ${cookies.length} cookies from saved session`)
}
// Note: Session storage will be loaded after page navigation
} catch (error) {
console.log('WARNING: Could not load session data (starting fresh):', error)
}
}
/**
* Save current session data for future use
*/
private async saveSession(): Promise<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(`SUCCESS: Saved ${cookies.length} cookies`)
// Save session storage and localStorage
const sessionData = await this.page.evaluate(() => {
const localStorage: { [key: string]: string | null } = {}
const sessionStorage: { [key: string]: string | null } = {}
// Extract localStorage
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i)
if (key) {
localStorage[key] = window.localStorage.getItem(key)
}
}
// Extract sessionStorage
for (let i = 0; i < window.sessionStorage.length; i++) {
const key = window.sessionStorage.key(i)
if (key) {
sessionStorage[key] = window.sessionStorage.getItem(key)
}
}
return { localStorage, sessionStorage }
})
await fs.writeFile(SESSION_STORAGE_FILE, JSON.stringify(sessionData, null, 2))
console.log('SUCCESS: Saved session storage and localStorage')
} catch (error) {
console.error('ERROR: Failed to save session data:', error)
}
}
/**
* Restore session storage and localStorage
*/
private async restoreSessionStorage(): Promise<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('SUCCESS: Restored session storage and localStorage')
} catch (error) {
console.log('WARNING: Could not restore session storage:', error)
}
}
/**
* Refresh session to keep it alive
*/
async refreshSession(): Promise<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('SUCCESS: Session refreshed successfully')
await this.saveSession() // Save refreshed session
return true
} else {
console.log('ERROR: Session expired during refresh')
this.isAuthenticated = false
return false
}
} catch (error) {
console.error('ERROR: Failed to refresh session:', error)
return false
}
}
/**
* Clear all saved session data
*/
async clearSession(): Promise<void> {
try {
console.log('🗑️ Clearing saved session data...')
if (await this.fileExists(COOKIES_FILE)) {
await fs.unlink(COOKIES_FILE)
console.log('SUCCESS: Cleared cookies file')
}
if (await this.fileExists(SESSION_STORAGE_FILE)) {
await fs.unlink(SESSION_STORAGE_FILE)
console.log('SUCCESS: Cleared session storage file')
}
// Clear browser context storage if available
if (this.context) {
await this.context.clearCookies()
console.log('SUCCESS: Cleared browser context cookies')
}
this.isAuthenticated = false
console.log('SUCCESS: Session data cleared successfully')
} catch (error) {
console.error('ERROR: Failed to clear session data:', error)
}
}
/**
* Get session status information
*/
async getSessionInfo(): Promise<{
isAuthenticated: boolean
hasSavedCookies: boolean
hasSavedStorage: boolean
cookiesCount: number
currentUrl: string
}> {
const hasSavedCookies = await this.fileExists(COOKIES_FILE)
const hasSavedStorage = await this.fileExists(SESSION_STORAGE_FILE)
let cookiesCount = 0
if (hasSavedCookies) {
try {
const cookiesData = await fs.readFile(COOKIES_FILE, 'utf8')
const cookies = JSON.parse(cookiesData)
cookiesCount = cookies.length
} catch (e) {
// Ignore error
}
}
const currentUrl = this.page ? await this.page.url() : ''
return {
isAuthenticated: this.isAuthenticated,
hasSavedCookies,
hasSavedStorage,
cookiesCount,
currentUrl
}
}
/**
* Get lightweight session status without triggering navigation
*/
async getQuickSessionStatus(): Promise<{
isAuthenticated: boolean
hasSavedCookies: boolean
hasSavedStorage: boolean
cookiesCount: number
currentUrl: string
browserActive: boolean
}> {
const sessionInfo = await this.getSessionInfo()
return {
...sessionInfo,
browserActive: !!(this.browser && this.page)
}
}
/**
* Add random delay to mimic human behavior
*/
private async humanDelay(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('WARNING: 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('WARNING: 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: 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("WARNING: 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('WARNING: Session fingerprint mismatch detected')
return false
}
}
return true
} catch (error) {
console.error('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('WARNING: 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('INFO: Marked captcha detection #' + markerData.count + ' at ' + markerData.timestamp)
} catch (error) {
console.log('WARNING: 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()