Files
trading_bot_v3/lib/tradingview-automation.ts
mindesbunister 451e6c87b3 fix: resolve TradingView authentication and screenshot automation
- Fixed authentication detection logic in checkLoginStatus method
- Resolved screenshot automation to properly capture TradingView charts
- Enhanced debugging output for authentication variable detection
- Improved session persistence and login flow
- Fixed weird screenshot issue - automation now properly navigates to charts

The automation now successfully:
- Authenticates with TradingView using proper session management
- Navigates to specific symbol charts correctly
- Captures clean screenshots instead of landing pages
- Maintains session persistence to avoid captchas
2025-07-18 09:49:04 +02:00

1052 lines
37 KiB
TypeScript

import puppeteer, { Browser, Page } from 'puppeteer'
import { promises as fs } from 'fs'
import * as path from 'path'
export interface TradingViewCredentials {
email: string
password: string
}
// Environment variables fallback
const TRADINGVIEW_EMAIL = process.env.TRADINGVIEW_EMAIL
const TRADINGVIEW_PASSWORD = process.env.TRADINGVIEW_PASSWORD
// Utility function to replace Puppeteer's waitForTimeout
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// Helper function to check if element is visible using Puppeteer APIs
async function isElementVisible(page: Page, selector: string, timeout: number = 1000): Promise<boolean> {
try {
await page.waitForSelector(selector, { timeout, visible: true })
return true
} catch {
return false
}
}
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 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 acquireOperationLock(): void {
if (this.operationLock) {
throw new Error('Another operation is already in progress. Please wait.')
}
this.operationLock = true
}
private releaseOperationLock(): void {
this.operationLock = false
}
// Singleton pattern
static getInstance(): TradingViewAutomation {
if (!TradingViewAutomation.instance) {
TradingViewAutomation.instance = new TradingViewAutomation()
}
return TradingViewAutomation.instance
}
async init(forceCleanup: boolean = false): Promise<void> {
this.acquireOperationLock()
try {
if (this.initPromise) {
console.log('🔄 Initialization already in progress, waiting...')
await this.initPromise
return
}
if (forceCleanup && this.browser) {
console.log('🧹 Force cleanup requested')
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 })
try {
this.browser = await puppeteer.launch({
headless: true,
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || '/usr/bin/chromium',
timeout: 60000,
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',
'--window-size=1920,1080',
'--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
]
})
this.page = await this.browser.newPage()
// Set viewport
await this.page.setViewport({ width: 1920, height: 1080 })
// Load saved session if available
await this.loadSession()
console.log('✅ Browser initialized successfully')
} catch (error) {
console.error('❌ Failed to initialize browser:', error)
await this.forceCleanup()
throw error
}
}
async forceCleanup(): Promise<void> {
console.log('🧹 Force cleanup: Closing browser and resetting state...')
try {
if (this.browser) {
await this.browser.close()
}
} catch (e) {
console.log('WARNING: Error during browser cleanup:', e)
}
this.browser = null
this.page = null
this.isAuthenticated = false
console.log('✅ Cleanup completed')
}
private async loadSession(): Promise<void> {
if (!this.page) return
try {
// Load cookies
if (await fs.access(COOKIES_FILE).then(() => true).catch(() => false)) {
const cookiesData = await fs.readFile(COOKIES_FILE, 'utf8')
const cookies = JSON.parse(cookiesData)
await this.page.setCookie(...cookies)
console.log('✅ Loaded saved cookies')
}
} catch (e) {
console.log('WARNING: Could not load session:', e)
}
}
private async saveSession(): Promise<void> {
if (!this.page) return
try {
// Save cookies
const cookies = await this.page.cookies()
await fs.writeFile(COOKIES_FILE, JSON.stringify(cookies, null, 2))
console.log('✅ Session saved')
} catch (e) {
console.log('WARNING: Could not save session:', e)
}
}
async checkLoginStatus(): Promise<boolean> {
if (!this.page) return false
try {
console.log('🔍 CHECKING LOGIN STATUS - Starting comprehensive authentication detection...')
// Strategy 1: Check JavaScript authentication variables
console.log('CHECKING: Strategy 1: JavaScript authentication variables...')
const authStatus = await this.page.evaluate(() => {
console.log('EVAL: Starting JavaScript authentication check...')
// Check window.is_authenticated
console.log('EVAL: Checking window.is_authenticated...')
const isAuthVar = (window as any).is_authenticated
console.log('EVAL: window.is_authenticated =', isAuthVar, typeof isAuthVar)
// Check window.user
console.log('EVAL: Checking window.user...')
const userVar = (window as any).user
console.log('EVAL: window.user =', userVar, typeof userVar)
// Check if user object has meaningful data
if (userVar && typeof userVar === 'object') {
console.log('EVAL: User object keys:', Object.keys(userVar))
console.log('EVAL: User object values preview:', JSON.stringify(userVar).substring(0, 200))
}
// Return authentication status
const result = isAuthVar === true || (userVar && typeof userVar === 'object' && Object.keys(userVar).length > 0)
console.log('EVAL: Final Strategy 1 result:', result)
return result
})
console.log('CHECKING: Strategy 1 JavaScript result:', authStatus)
if (authStatus) {
console.log('✅ Strategy 1: Authenticated via JavaScript variables')
return true
}
// Strategy 1.5: Check for user avatar/profile elements (indicates logged in state)
console.log('CHECKING: Strategy 1.5: User interface elements...')
const userElements = await this.page.$$eval('[data-testid="header-user-menu"], .tv-header__user-menu, [class*="user-menu"], [class*="avatar"], [data-name="header-user-menu"]', elements => {
console.log('EVAL: Found', elements.length, 'potential user elements')
return elements.length > 0
}).catch(() => false)
if (userElements) {
console.log('Strategy 1.5: Found user interface elements - likely authenticated')
return true
}
// Strategy 2: Check for absence of login forms (indicates already logged in)
console.log('CHECKING: Strategy 2: Absence of login forms...')
const pageUrl = this.page.url()
console.log('CHECKING: Current URL:', pageUrl)
// If we're not on a login page and no login forms exist, we're likely logged in
const hasLoginForms = await this.page.$('input[type="email"], input[name="username"], input[name="email"], input[type="password"]').then(el => !!el).catch(() => false)
const isOnLoginPage = pageUrl.includes('/signin') || pageUrl.includes('/login') || pageUrl.includes('/accounts/signin')
console.log('CHECKING: Has login forms:', hasLoginForms)
console.log('CHECKING: Is on login page:', isOnLoginPage)
if (!hasLoginForms && !isOnLoginPage) {
console.log('Strategy 2: No login forms and not on login page - likely authenticated')
return true
}
// Strategy 3: Check for "Sign In" vs "User Menu" buttons
console.log('CHECKING: Strategy 3: Page action buttons...')
const hasSignInButton = await this.page.$('[data-testid="header-signin-button"], a[href*="signin"], button:has-text("Sign in")').then(el => !!el).catch(() => false)
const hasUserMenu = await this.page.$('[data-testid="header-user-menu"], .tv-header__user-menu, [class*="user-menu"]').then(el => !!el).catch(() => false)
console.log('CHECKING: Has sign in button:', hasSignInButton)
console.log('CHECKING: Has user menu:', hasUserMenu)
if (!hasSignInButton && hasUserMenu) {
console.log('Strategy 3: User menu present, no sign in button - authenticated')
return true
}
if (!hasSignInButton && !isOnLoginPage) {
console.log('Strategy 3: No sign in button and not on login page - likely authenticated')
return true
}
// Strategy 2: Check for user account indicators (positive indicators)
console.log('CHECKING: Strategy 2: Checking for user account indicators...')
await this.takeDebugScreenshot('login_status_check')
const userIndicators = [
'.js-header-user-menu-button', // TradingView's main user button
'[data-name="header-user-menu"]',
'.tv-header__user-menu-button:not(.tv-header__user-menu-button--anonymous)',
'.tv-header__user-menu-wrap'
]
for (const selector of userIndicators) {
try {
const element = await this.page.$(selector)
if (element) {
const isVisible = await element.boundingBox()
if (isVisible) {
console.log('SUCCESS: Found user account element: ' + selector)
return true
}
}
} catch (e) {
continue
}
}
// Strategy 3: Check for anonymous/sign-in indicators (negative indicators)
console.log('CHECKING: Strategy 3: Checking for anonymous/sign-in indicators...')
const anonymousIndicators = [
'.tv-header__user-menu-button--anonymous',
'[data-name="header-user-menu-sign-in"]',
'button:contains("Sign in")',
'a:contains("Sign in")'
]
for (const selector of anonymousIndicators) {
try {
const element = await this.page.$(selector)
if (element) {
const isVisible = await element.boundingBox()
if (isVisible) {
console.log('ERROR: Found anonymous indicator: ' + selector + ' - not logged in')
return false
}
}
} catch (e) {
continue
}
}
// Strategy 4: Check URL patterns
console.log('CHECKING: Strategy 4: Checking URL patterns...')
const urlCheck = this.page.url()
if (urlCheck.includes('/signin') || urlCheck.includes('/login')) {
console.log('ERROR: On login page - not logged in')
return false
}
// Strategy 5: Check authentication cookies
console.log('CHECKING: Strategy 5: Checking authentication cookies...')
const cookies = await this.page.cookies()
const authCookies = cookies.filter(cookie =>
cookie.name.includes('auth') ||
cookie.name.includes('session') ||
cookie.name.includes('token')
)
if (authCookies.length === 0) {
console.log('WARNING: No authentication cookies found')
}
// Strategy 6: Check for personal content
console.log('CHECKING: Strategy 6: Checking for personal content...')
const personalContentSelectors = [
'[data-name="watchlist"]',
'.tv-header__watchlist',
'.js-backtesting-head'
]
for (const selector of personalContentSelectors) {
try {
const element = await this.page.$(selector)
if (element) {
console.log('SUCCESS: Found personal content: ' + selector)
return true
}
} catch (e) {
continue
}
}
// Strategy 7: Check for absence of login interface (final check)
console.log('CHECKING: Strategy 7: Final absence of login interface check...')
const finalUrlCheck = this.page.url()
const hasAnyLoginElements = await this.page.$('input[type="email"], input[name="username"], input[name="email"], input[type="password"], [href*="signin"], [href*="login"], button:has-text("Sign in")').then(el => !!el).catch(() => false)
const isOnMainTradingViewDomain = finalUrlCheck.includes('tradingview.com') && !finalUrlCheck.includes('/signin') && !finalUrlCheck.includes('/login')
console.log('CHECKING: Has any login elements:', hasAnyLoginElements)
console.log('CHECKING: Is on main TradingView domain (not login page):', isOnMainTradingViewDomain)
if (isOnMainTradingViewDomain && !hasAnyLoginElements) {
console.log('✅ Strategy 7: On main TradingView domain with no login elements - authenticated')
return true
}
// If we can't determine status clearly, assume not logged in to be safe
console.log('WARNING: Could not determine login status clearly, assuming not logged in')
return false
} catch (e) {
console.log('ERROR: Error checking login status:', e)
return false
}
}
async login(credentials?: TradingViewCredentials): Promise<boolean> {
if (!this.page) throw new Error('Page not initialized')
const email = credentials?.email || TRADINGVIEW_EMAIL
const password = credentials?.password || TRADINGVIEW_PASSWORD
if (!email || !password) {
throw new Error('TradingView credentials not provided')
}
try {
// Check if already logged in
const loggedIn = await this.checkLoginStatus()
if (loggedIn) {
console.log('SUCCESS: Already logged in, skipping login steps')
return true
}
console.log('🔐 Starting login process...')
// Navigate to main TradingView page first, then find login
console.log('📄 Navigating to TradingView main page...')
try {
await this.page.goto('https://www.tradingview.com/', {
waitUntil: 'domcontentloaded',
timeout: 30000
})
await sleep(3000)
// Look for sign in buttons on main page
console.log('🔍 Looking for Sign In button on main page...')
const signinButtons = [
'a[href*="signin"]',
'a[href*="accounts/signin"]',
'[data-name="header-user-menu-sign-in"]',
'.tv-header__user-menu-button',
'.tv-header__user-menu-button--anonymous',
'.js-signin-switch',
'button[data-role="button"]',
'button.tv-button',
'a.tv-button'
]
let foundSignin = false
for (const selector of signinButtons) {
try {
const elements = await this.page.$$(selector)
for (const element of elements) {
const isVisible = await element.boundingBox()
if (isVisible) {
const text = await element.evaluate(el => {
const elem = el as HTMLElement
return elem.textContent || elem.getAttribute('title') || elem.getAttribute('href') || ''
})
console.log(`Found signin element: ${selector} with text: "${text}"`) // Check if this looks like a sign in button
const lowerText = text.toLowerCase()
if (lowerText.includes('sign in') || lowerText.includes('login') ||
lowerText.includes('signin') || selector.includes('signin') ||
selector.includes('user-menu-button')) {
await element.click()
console.log('✅ Clicked sign in button')
await sleep(5000) // Wait for login page/modal to load
foundSignin = true
break
}
}
}
if (foundSignin) break
} catch (e) {
const error = e as Error
console.log(`❌ Error with selector ${selector}: ${error.message}`)
continue
}
}
// If no selector-based approach works, try finding by text content
if (!foundSignin) {
console.log('🔍 Trying to find sign-in button by text content...')
const clickableElements = await this.page.$$('button, a, div[role="button"], span[role="button"]')
for (const element of clickableElements) {
try {
const isVisible = await element.boundingBox()
if (isVisible) {
const text = await element.evaluate(el => {
const elem = el as HTMLElement
return (elem.textContent || '').toLowerCase().trim()
})
if (text.includes('sign in') || text.includes('signin') || text.includes('log in') || text.includes('login')) {
console.log(`🎯 Found sign-in button with text: "${text}"`)
await element.click()
console.log('✅ Clicked sign-in button by text')
await sleep(5000)
foundSignin = true
break
}
}
} catch (e) {
continue
}
}
}
if (!foundSignin) {
console.log('⚠️ No sign in button found on main page, trying direct navigation...')
await this.page.goto('https://www.tradingview.com/accounts/signin/', {
waitUntil: 'domcontentloaded',
timeout: 30000
})
}
} catch (e) {
const error = e as Error
console.log(`❌ Navigation error: ${error.message}`)
// Fallback to direct login URL
await this.page.goto('https://www.tradingview.com/accounts/signin/', {
waitUntil: 'domcontentloaded',
timeout: 30000
})
}
await sleep(3000)
await this.takeDebugScreenshot('login_page_loaded')
// Check current URL and page state
const currentUrl = await this.page.url()
const pageTitle = await this.page.title()
console.log(`🌐 Current URL: ${currentUrl}`)
console.log(`📋 Page title: ${pageTitle}`)
// Wait for login form
console.log('⏳ Waiting for login form...')
await sleep(5000)
// Look for email login option with comprehensive approach
console.log('CHECKING: Looking for Email login option...')
// Try to find email trigger button using XPath and text content
let emailFormVisible = false
try {
// First try standard selectors
const emailTriggers = [
'button[data-overflow-tooltip-text="Email"]',
'[data-name="email"]',
'.tv-signin-dialog__toggle-email',
'.js-signin__email',
'[data-testid="email-signin"]',
'button[class*="email"]',
'.signin-form__toggle'
]
for (const trigger of emailTriggers) {
try {
const elements = await this.page.$$(trigger)
for (const element of elements) {
const isVisible = await element.boundingBox()
if (isVisible) {
const text = await element.evaluate(el => {
const elem = el as HTMLElement
return elem.textContent || elem.getAttribute('data-overflow-tooltip-text') || elem.getAttribute('title') || ''
})
console.log(`TARGET: Found email trigger: ${trigger} with text: "${text}"`)
await element.click()
console.log('SUCCESS: Clicked email trigger')
await sleep(3000)
emailFormVisible = true
break
}
}
if (emailFormVisible) break
} catch (e) {
continue
}
}
// If no standard selectors work, try finding buttons by text content
if (!emailFormVisible) {
console.log('🔍 Trying to find email button by text content...')
const buttons = await this.page.$$('button, span, div[role="button"], a')
for (const button of buttons) {
try {
const isVisible = await button.boundingBox()
if (isVisible) {
const text = await button.evaluate(el => {
const elem = el as HTMLElement
return (elem.textContent || '').toLowerCase().trim()
})
if (text.includes('email') || text.includes('continue with email')) {
console.log(`🎯 Found email button with text: "${text}"`)
await button.click()
console.log('✅ Clicked email button by text')
await sleep(3000)
emailFormVisible = true
break
}
}
} catch (e) {
continue
}
}
}
} catch (e) {
console.log('⚠️ Error finding email trigger:', e)
}
// If no email trigger found, check if we're in a modal or different login flow
if (!emailFormVisible) {
console.log('INFO: No email trigger found, checking for alternative login flows...')
// Wait a bit more for any modals to fully load
await sleep(5000)
await this.takeDebugScreenshot('checking_modal_login')
// Check for modal dialogs or different login containers
const modalSelectors = [
'[role="dialog"]',
'.tv-dialog',
'.tv-dialog__wrapper',
'.js-dialog',
'.signin-dialog',
'.auth-modal',
'.modal',
'[data-testid="auth-modal"]'
]
for (const modalSelector of modalSelectors) {
try {
const modal = await this.page.$(modalSelector)
if (modal) {
console.log(`📋 Found modal: ${modalSelector}`)
// Look for email options within the modal
const modalEmailButtons = await modal.$$('button, a, div[role="button"]')
for (const button of modalEmailButtons) {
try {
const isVisible = await button.boundingBox()
if (isVisible) {
const text = await button.evaluate(el => {
const elem = el as HTMLElement
return (elem.textContent || '').toLowerCase().trim()
})
console.log(`🎯 Modal button text: "${text}"`)
if (text.includes('email') || text.includes('continue with email') ||
text.includes('sign in with email') || text.includes('use email')) {
console.log(`✅ Found modal email button: "${text}"`)
await button.click()
console.log('✅ Clicked modal email button')
await sleep(3000)
emailFormVisible = true
break
}
}
} catch (e) {
continue
}
}
if (emailFormVisible) break
}
} catch (e) {
continue
}
}
}
if (!emailFormVisible) {
console.log('INFO: Still no email form, assuming email form is already visible')
}
await this.takeDebugScreenshot('after_email_trigger')
// Fill email - updated selectors for current TradingView
const emailInputSelectors = [
'input[type="email"]',
'input[name*="email"]',
'input[name="username"]',
'input[name="id_username"]',
'input[placeholder*="email" i]',
'input[placeholder*="Email" i]',
'input[autocomplete="username"]',
'input[data-name="email"]',
'input[class*="email"]',
'#id_username',
'#email',
'[data-testid="email-input"]',
'[data-testid="username-input"]'
]
// Debug: Check what's actually on the page
await this.takeDebugScreenshot('before_email_search')
// Get all input elements to debug
const allInputs = await this.page.$$('input')
console.log(`DEBUG: Found ${allInputs.length} input elements on page`)
for (let i = 0; i < Math.min(allInputs.length, 10); i++) {
try {
const inputType = await allInputs[i].evaluate(el => el.type)
const inputName = await allInputs[i].evaluate(el => el.name)
const inputPlaceholder = await allInputs[i].evaluate(el => el.placeholder)
const inputId = await allInputs[i].evaluate(el => el.id)
const inputClass = await allInputs[i].evaluate(el => el.className)
const inputValue = await allInputs[i].evaluate(el => el.value)
const isVisible = await allInputs[i].boundingBox()
console.log(`INPUT ${i}: type="${inputType}", name="${inputName}", placeholder="${inputPlaceholder}", id="${inputId}", class="${inputClass}", value="${inputValue}", visible=${!!isVisible}`)
// Check if this looks like an email/username field even if it doesn't match selectors
if (isVisible && (inputType === 'text' || inputType === 'email' || !inputType)) {
const lowerPlaceholder = (inputPlaceholder || '').toLowerCase()
const lowerName = (inputName || '').toLowerCase()
const lowerClass = (inputClass || '').toLowerCase()
if (lowerPlaceholder.includes('email') || lowerPlaceholder.includes('username') ||
lowerName.includes('email') || lowerName.includes('username') ||
lowerClass.includes('email') || lowerClass.includes('username')) {
console.log(`🎯 INPUT ${i} looks like email field based on attributes`)
}
}
} catch (e) {
const error = e as Error
console.log(`INPUT ${i}: Error reading attributes - ${error.message}`)
}
}
let emailInput = null
// First try specific selectors
for (const selector of emailInputSelectors) {
try {
emailInput = await this.page.$(selector)
if (emailInput) {
const isVisible = await emailInput.boundingBox()
if (isVisible) {
console.log('SUCCESS: Found email input: ' + selector)
break
}
}
} catch (e) {
continue
}
emailInput = null // Reset if not visible
}
// If no specific selector worked, try to find the first visible text input
if (!emailInput) {
console.log('🔍 No specific email input found, trying first visible text input...')
for (const input of allInputs) {
try {
const isVisible = await input.boundingBox()
const inputType = await input.evaluate(el => el.type)
if (isVisible && (inputType === 'text' || inputType === 'email' || !inputType)) {
console.log('🎯 Found first visible text input, assuming it\'s email field')
emailInput = input
break
}
} catch (e) {
continue
}
}
}
// Last resort: try to find inputs by looking for labels or surrounding text
if (!emailInput) {
console.log('🔍 Last resort: searching for inputs near email-related text...')
// Look for text that says "email" and find nearby inputs
const emailTexts = await this.page.evaluate(() => {
const allElements = Array.from(document.querySelectorAll('*'))
return allElements
.filter(el => {
const text = (el.textContent || '').toLowerCase()
return text.includes('email') || text.includes('username') || text.includes('sign in')
})
.map(el => {
const rect = el.getBoundingClientRect()
return {
text: (el.textContent || '').trim(),
x: rect.x,
y: rect.y,
tag: el.tagName
}
})
})
console.log('📋 Found email-related text elements:', emailTexts.slice(0, 5))
// Try to find any input that's not already found
for (const input of allInputs) {
try {
const isVisible = await input.boundingBox()
if (isVisible) {
console.log('🎯 Using any visible input as potential email field')
emailInput = input
break
}
} catch (e) {
continue
}
}
}
if (!emailInput) {
// Additional debug info
const currentUrl = await this.page.url()
const title = await this.page.title()
console.log(`❌ Email input not found on: ${currentUrl}`)
console.log(`❌ Page title: ${title}`)
await this.takeDebugScreenshot('email_input_not_found')
throw new Error(`Could not find email input field on ${currentUrl}. Found ${allInputs.length} inputs total.`)
}
await emailInput.click()
await emailInput.type(email)
console.log('✅ Filled email field')
// Fill password
const passwordInputSelectors = [
'input[type="password"]',
'input[name*="password"]'
]
let passwordInput = null
for (const selector of passwordInputSelectors) {
try {
passwordInput = await this.page.$(selector)
if (passwordInput) {
const isVisible = await passwordInput.boundingBox()
if (isVisible) {
console.log('SUCCESS: Found password input: ' + selector)
break
}
}
} catch (e) {
continue
}
}
if (!passwordInput) {
throw new Error('Could not find password input field')
}
await passwordInput.click()
await passwordInput.type(password)
console.log('✅ Filled password field')
// Submit form
const submitSelectors = [
'button[type="submit"]',
'button:contains("Sign in")',
'button:contains("Log in")',
'button:contains("Login")'
]
let submitted = false
for (const selector of submitSelectors) {
try {
const button = await this.page.$(selector)
if (button) {
const isVisible = await button.boundingBox()
if (isVisible) {
console.log('SUCCESS: Found submit button: ' + selector)
await button.click()
submitted = true
break
}
}
} catch (e) {
continue
}
}
if (!submitted) {
// Try pressing Enter on password field
await passwordInput.press('Enter')
console.log('INFO: Pressed Enter on password field')
}
console.log('⏳ Waiting for login completion...')
await sleep(5000)
// Check for errors
const errorSelectors = [
'.tv-alert-dialog__text',
'.tv-dialog__error',
'[data-name="auth-error-message"]',
'.error-message'
]
for (const selector of errorSelectors) {
try {
const errorElement = await this.page.$(selector)
if (errorElement) {
const errorText = await this.page.evaluate(el => el.textContent, errorElement)
if (errorText && errorText.trim()) {
await this.takeDebugScreenshot('login_error')
throw new Error('Login failed: ' + errorText.trim())
}
}
} catch (e) {
continue
}
}
// Verify login success
await sleep(3000)
const loginSuccess = await this.checkLoginStatus()
if (loginSuccess) {
console.log('✅ Login successful!')
this.isAuthenticated = true
await this.saveSession()
return true
} else {
await this.takeDebugScreenshot('login_verification_failed')
throw new Error('Login verification failed - still appears not logged in')
}
} catch (error) {
console.error('❌ Login failed:', error)
await this.takeDebugScreenshot('login_error')
throw error
}
}
async takeDebugScreenshot(prefix: string = 'debug'): Promise<string> {
if (!this.page) throw new Error('Page not initialized')
try {
const timestamp = Date.now()
const filename = `${prefix}_${timestamp}.png`
const filepath = path.join(process.cwd(), 'screenshots', filename)
// Ensure screenshots directory exists
await fs.mkdir(path.dirname(filepath), { recursive: true })
await this.page.screenshot({
path: filepath as `${string}.png`,
fullPage: true,
type: 'png'
})
console.log(`📸 Screenshot saved: ${filename}`)
return filepath
} catch (error) {
console.error('Error taking screenshot:', error)
throw error
}
}
async navigateToLayout(layoutId: string, symbol: string, timeframe?: string): Promise<boolean> {
if (!this.page) throw new Error('Page not initialized')
try {
console.log(`🎯 Navigating to layout ${layoutId} with symbol: ${symbol}`)
// Construct direct layout URL - this bypasses the generic chart URL issue
const directUrl = `https://www.tradingview.com/chart/${layoutId}/?symbol=${symbol}${timeframe ? `&interval=${timeframe}` : ''}`
console.log(`📍 Direct layout navigation to: ${directUrl}`)
await this.page.goto(directUrl, {
waitUntil: 'domcontentloaded',
timeout: 30000
})
// Wait for chart to load
await sleep(5000)
// Wait for chart container
await this.page.waitForSelector('.chart-container, #chart-container, [data-name="chart"]', {
timeout: 30000
})
console.log(`✅ Successfully navigated to layout ${layoutId}`)
return true
} catch (error) {
console.error(`❌ Failed to navigate to layout ${layoutId}:`, error)
return false
}
}
async navigateToSymbol(symbol: string, timeframe?: string): Promise<boolean> {
if (!this.page) throw new Error('Page not initialized')
try {
console.log(`🎯 Navigating to symbol: ${symbol}`)
// Construct TradingView URL
const baseUrl = 'https://www.tradingview.com/chart/'
const params = new URLSearchParams()
params.set('symbol', symbol)
if (timeframe) {
params.set('interval', timeframe)
}
const url = `${baseUrl}?${params.toString()}`
console.log(`📍 Navigating to: ${url}`)
await this.page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30000
})
// Wait for chart to load
await sleep(5000)
// Wait for chart container
await this.page.waitForSelector('.chart-container, #chart-container, [data-name="chart"]', {
timeout: 30000
})
console.log('✅ Chart loaded successfully')
return true
} catch (error) {
console.error('❌ Failed to navigate to symbol:', error)
throw error
}
}
async takeScreenshot(options: { filename?: string, fullPage?: boolean } = {}): Promise<string> {
if (!this.page) throw new Error('Page not initialized')
try {
const timestamp = Date.now()
const filename = options.filename || `screenshot_${timestamp}.png`
const filepath = path.join(process.cwd(), 'screenshots', filename)
// Ensure screenshots directory exists
await fs.mkdir(path.dirname(filepath), { recursive: true })
await this.page.screenshot({
path: filepath as `${string}.png`,
fullPage: options.fullPage || false,
type: 'png'
})
console.log(`📸 Screenshot saved: ${filename}`)
return filepath
} catch (error) {
console.error('Error taking screenshot:', error)
throw error
}
}
}
// Export default instance
export const tradingViewAutomation = TradingViewAutomation.getInstance()
export default TradingViewAutomation.getInstance()