diff --git a/debug-after-click.png b/debug-after-click.png
new file mode 100644
index 0000000..3748fd1
Binary files /dev/null and b/debug-after-click.png differ
diff --git a/debug-initial.png b/debug-initial.png
new file mode 100644
index 0000000..1075b90
Binary files /dev/null and b/debug-initial.png differ
diff --git a/debug-timeframe-initial.png b/debug-timeframe-initial.png
new file mode 100644
index 0000000..a67425f
Binary files /dev/null and b/debug-timeframe-initial.png differ
diff --git a/debug-timeframe.js b/debug-timeframe.js
new file mode 100644
index 0000000..d3c3535
--- /dev/null
+++ b/debug-timeframe.js
@@ -0,0 +1,120 @@
+const { chromium } = require('playwright');
+
+async function debugTimeframeChange() {
+ console.log('🔍 Debugging TradingView timeframe change...');
+
+ const browser = await chromium.launch({
+ headless: false, // Show browser for debugging
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
+ });
+
+ const page = await browser.newPage();
+
+ try {
+ // Navigate to TradingView chart (assuming we're already logged in or using a public chart)
+ console.log('📊 Navigating to TradingView chart...');
+ await page.goto('https://www.tradingview.com/chart/', {
+ waitUntil: 'networkidle',
+ timeout: 30000
+ });
+
+ // Wait for chart to load
+ await page.waitForTimeout(5000);
+
+ // Take screenshot of initial state
+ await page.screenshot({ path: 'debug-timeframe-initial.png', fullPage: true });
+ console.log('📸 Initial screenshot taken');
+
+ // Look for all buttons and elements that might be timeframe related
+ console.log('🔍 Analyzing page for timeframe controls...');
+
+ const timeframeElements = await page.$$eval('*', (elements) => {
+ const found = [];
+
+ for (const el of elements) {
+ const text = el.textContent?.trim() || '';
+ const className = el.className || '';
+ const tagName = el.tagName;
+ const dataset = el.dataset || {};
+ const ariaLabel = el.getAttribute('aria-label') || '';
+ const title = el.getAttribute('title') || '';
+
+ // Look for elements that might be timeframe related
+ if (
+ text.match(/^(1|5|15|30|60|1h|4h|1d|1w)$/i) ||
+ text.match(/^\d+[mhd]$/i) ||
+ (typeof className === 'string' && className.includes('interval')) ||
+ (typeof className === 'string' && className.includes('timeframe')) ||
+ (typeof ariaLabel === 'string' && ariaLabel.includes('timeframe')) ||
+ (typeof ariaLabel === 'string' && ariaLabel.includes('interval')) ||
+ (typeof title === 'string' && title.includes('timeframe')) ||
+ (typeof title === 'string' && title.includes('interval'))
+ ) {
+ found.push({
+ tagName,
+ text: text.substring(0, 20),
+ className: typeof className === 'string' ? className.substring(0, 100) : className,
+ dataset,
+ ariaLabel,
+ title,
+ outerHTML: el.outerHTML.substring(0, 200)
+ });
+ }
+ }
+
+ return found.slice(0, 20); // Limit results
+ });
+
+ console.log('🎯 Potential timeframe elements found:');
+ console.log(JSON.stringify(timeframeElements, null, 2));
+
+ // Try to find the "1h" button specifically
+ console.log('\n🎯 Looking specifically for 1h timeframe...');
+
+ const oneHourSelectors = [
+ 'button:has-text("1h")',
+ 'button:has-text("1H")',
+ '[data-value="1h"]',
+ '[data-value="1H"]',
+ '[data-value="60"]',
+ '[title="1h"]',
+ '[title="1H"]'
+ ];
+
+ for (const selector of oneHourSelectors) {
+ try {
+ console.log(`Trying selector: ${selector}`);
+ const element = page.locator(selector).first();
+ const isVisible = await element.isVisible({ timeout: 2000 });
+ console.log(`Selector ${selector}: ${isVisible ? '✅ VISIBLE' : '❌ Not visible'}`);
+
+ if (isVisible) {
+ console.log('🎉 Found 1h button! Clicking...');
+ await element.click();
+ await page.waitForTimeout(2000);
+ await page.screenshot({ path: 'debug-timeframe-after-click.png', fullPage: true });
+ console.log('📸 After-click screenshot taken');
+ break;
+ }
+ } catch (e) {
+ console.log(`Selector ${selector}: ❌ Error - ${e.message}`);
+ }
+ }
+
+ // Check current URL and title
+ console.log('\n📍 Current page info:');
+ console.log('URL:', await page.url());
+ console.log('Title:', await page.title());
+
+ console.log('\n⏱️ Waiting 10 seconds for manual inspection...');
+ await page.waitForTimeout(10000);
+
+ } catch (error) {
+ console.error('❌ Error during debugging:', error);
+ } finally {
+ await browser.close();
+ console.log('🏁 Debug session completed');
+ }
+}
+
+debugTimeframeChange().catch(console.error);
diff --git a/debug-tradingview-after-wait.png b/debug-tradingview-after-wait.png
new file mode 100644
index 0000000..9eb5b42
Binary files /dev/null and b/debug-tradingview-after-wait.png differ
diff --git a/debug-tradingview-v2.js b/debug-tradingview-v2.js
new file mode 100644
index 0000000..f5018bf
--- /dev/null
+++ b/debug-tradingview-v2.js
@@ -0,0 +1,190 @@
+const { chromium } = require('playwright');
+
+async function debugTradingViewLoginV2() {
+ console.log('🚀 Starting TradingView login debug v2...');
+
+ const browser = await chromium.launch({
+ headless: false, // Show the browser
+ slowMo: 500, // Slow down by 500ms
+ devtools: true // Open devtools
+ });
+
+ const context = await browser.newContext({
+ viewport: { width: 1280, height: 720 },
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
+ });
+
+ const page = await context.newPage();
+
+ try {
+ console.log('📍 Navigating to TradingView login page...');
+ await page.goto('https://www.tradingview.com/accounts/signin/', {
+ waitUntil: 'networkidle',
+ timeout: 30000
+ });
+
+ console.log('📄 Current URL:', page.url());
+ console.log('📋 Page title:', await page.title());
+
+ // Wait for dynamic content to load - try different strategies
+ console.log('⏳ Waiting for dynamic content to load...');
+
+ // Strategy 1: Wait for common login form selectors to appear
+ const possibleSelectors = [
+ 'input[type="email"]',
+ 'input[name="email"]',
+ 'input[placeholder*="email" i]',
+ '[data-name="email"]',
+ 'form input',
+ '.login-form input',
+ '#email',
+ '[name="username"]'
+ ];
+
+ console.log('🔍 Waiting for login form to appear...');
+ let loginFormFound = false;
+ let foundSelector = null;
+
+ // Try waiting for each selector with a reasonable timeout
+ for (const selector of possibleSelectors) {
+ try {
+ console.log(` Checking selector: ${selector}`);
+ await page.waitForSelector(selector, { timeout: 3000, state: 'visible' });
+ console.log(`✅ Found login form with selector: ${selector}`);
+ foundSelector = selector;
+ loginFormFound = true;
+ break;
+ } catch (e) {
+ console.log(` ❌ Selector ${selector} not found or not visible`);
+ }
+ }
+
+ if (!loginFormFound) {
+ console.log('⏰ No standard selectors found, waiting longer for page to fully load...');
+ await page.waitForTimeout(10000); // Wait 10 seconds
+ }
+
+ // Take another screenshot after waiting
+ await page.screenshot({ path: './debug-tradingview-after-wait.png', fullPage: true });
+ console.log('📸 Screenshot saved as debug-tradingview-after-wait.png');
+
+ // Check again for inputs after waiting
+ console.log('🔍 Checking for input elements again...');
+ const allInputs = await page.locator('input').all();
+ console.log(`📝 Found ${allInputs.length} total input elements after waiting`);
+
+ for (let i = 0; i < allInputs.length; i++) {
+ const input = allInputs[i];
+ try {
+ const type = await input.getAttribute('type');
+ const name = await input.getAttribute('name');
+ const id = await input.getAttribute('id');
+ const placeholder = await input.getAttribute('placeholder');
+ const className = await input.getAttribute('class');
+ const isVisible = await input.isVisible();
+ const isEnabled = await input.isEnabled();
+
+ console.log(` Input ${i + 1}: type="${type}", name="${name}", id="${id}", placeholder="${placeholder}", class="${className}", visible=${isVisible}, enabled=${isEnabled}`);
+ } catch (e) {
+ console.log(` Input ${i + 1}: Error getting attributes - ${e.message}`);
+ }
+ }
+
+ // Check for buttons that might trigger login form
+ console.log('🔘 Looking for login buttons or triggers...');
+ const buttonSelectors = [
+ 'button',
+ '[role="button"]',
+ 'a[href*="signin"]',
+ 'a[href*="login"]',
+ '.signin',
+ '.login',
+ '[data-name*="login"]',
+ '[data-name*="signin"]'
+ ];
+
+ for (const selector of buttonSelectors) {
+ try {
+ const buttons = await page.locator(selector).all();
+ for (let i = 0; i < buttons.length; i++) {
+ const button = buttons[i];
+ try {
+ const text = await button.textContent();
+ const isVisible = await button.isVisible();
+ if (isVisible && text && (text.toLowerCase().includes('sign') || text.toLowerCase().includes('log'))) {
+ console.log(` Found potential login button: "${text}" with selector ${selector}`);
+ }
+ } catch (e) {
+ // Continue
+ }
+ }
+ } catch (e) {
+ // Continue
+ }
+ }
+
+ // Look for any clickable elements that might show the login form
+ console.log('🎯 Looking for elements that might trigger login form...');
+ const clickableSelectors = [
+ '[data-qa*="login"]',
+ '[data-qa*="signin"]',
+ '[data-testid*="login"]',
+ '[data-testid*="signin"]',
+ '.js-signin',
+ '.js-login'
+ ];
+
+ for (const selector of clickableSelectors) {
+ try {
+ const element = await page.locator(selector).first();
+ if (await element.isVisible()) {
+ const text = await element.textContent();
+ console.log(` Found clickable element: "${text}" with selector ${selector}`);
+ }
+ } catch (e) {
+ // Continue
+ }
+ }
+
+ // Check the page content for any hidden forms or components
+ console.log('📄 Checking page content for hidden login forms...');
+ const pageContent = await page.content();
+ const hasLoginKeywords = pageContent.toLowerCase().includes('email') ||
+ pageContent.toLowerCase().includes('password') ||
+ pageContent.toLowerCase().includes('username');
+ console.log(`📝 Page contains login keywords: ${hasLoginKeywords}`);
+
+ if (pageContent.toLowerCase().includes('email')) {
+ console.log('📧 Page contains "email" text');
+ }
+ if (pageContent.toLowerCase().includes('password')) {
+ console.log('🔒 Page contains "password" text');
+ }
+
+ // Wait for user interaction
+ console.log('\n🎮 Browser is open for manual inspection. Press Ctrl+C to close when done.');
+ console.log('💡 Look for:');
+ console.log(' - Any buttons or links that show the login form');
+ console.log(' - Login form that appears after clicking something');
+ console.log(' - Network requests that load login form content');
+ console.log(' - JavaScript errors in the console');
+
+ // Keep the browser open for manual inspection
+ await page.waitForTimeout(300000); // Wait 5 minutes
+
+ } catch (error) {
+ console.error('❌ Error during debug:', error);
+ await page.screenshot({ path: './debug-tradingview-error-v2.png', fullPage: true });
+ } finally {
+ console.log('🔚 Closing browser...');
+ await browser.close();
+ }
+}
+
+// Handle Ctrl+C to close gracefully
+process.on('SIGINT', () => {
+ console.log('\n👋 Received SIGINT, closing browser...');
+ process.exit(0);
+});
+
+debugTradingViewLoginV2().catch(console.error);
diff --git a/debug-tradingview-v3.js b/debug-tradingview-v3.js
new file mode 100644
index 0000000..b595f8c
--- /dev/null
+++ b/debug-tradingview-v3.js
@@ -0,0 +1,169 @@
+const { chromium } = require('playwright');
+
+async function debugTradingViewLoginV3() {
+ console.log('🚀 Starting TradingView login debug v3 - Looking for trigger elements...');
+
+ const browser = await chromium.launch({
+ headless: false,
+ slowMo: 1000,
+ devtools: true
+ });
+
+ const context = await browser.newContext({
+ viewport: { width: 1280, height: 720 },
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
+ });
+
+ const page = await context.newPage();
+
+ try {
+ console.log('📍 Navigating to TradingView login page...');
+ await page.goto('https://www.tradingview.com/accounts/signin/', {
+ waitUntil: 'networkidle',
+ timeout: 30000
+ });
+
+ // Wait for page to settle
+ await page.waitForTimeout(5000);
+
+ console.log('📸 Taking initial screenshot...');
+ await page.screenshot({ path: './debug-initial.png', fullPage: true });
+
+ // Look for common TradingView login triggers
+ console.log('🔍 Looking for login triggers...');
+
+ const triggers = [
+ // Email/password login button
+ { selector: 'text="Email"', name: 'Email button' },
+ { selector: 'text="Sign in with email"', name: 'Sign in with email' },
+ { selector: 'text="Continue with email"', name: 'Continue with email' },
+ { selector: '[data-name="email"]', name: 'Email data-name' },
+ { selector: '[data-qa*="email"]', name: 'Email data-qa' },
+ { selector: 'button:has-text("Email")', name: 'Button containing Email' },
+ { selector: 'button:has-text("email")', name: 'Button containing email' },
+ { selector: '.tv-signin-dialog__toggle-email', name: 'TV signin email toggle' },
+ { selector: '.js-show-email', name: 'JS show email' },
+ { selector: '[data-outside-click*="email"]', name: 'Data outside click email' },
+
+ // General buttons that might trigger login
+ { selector: 'button', name: 'Any button' },
+ { selector: '[role="button"]', name: 'Role button' },
+ { selector: 'a[href="#"]', name: 'Anchor with hash' },
+ { selector: '.js-signin', name: 'JS signin class' },
+ ];
+
+ let foundTrigger = null;
+
+ for (const trigger of triggers) {
+ try {
+ console.log(` Checking: ${trigger.name} (${trigger.selector})`);
+
+ if (trigger.selector === 'button' || trigger.selector === '[role="button"]') {
+ // For generic buttons, check each one
+ const buttons = await page.locator(trigger.selector).all();
+ console.log(` Found ${buttons.length} buttons`);
+
+ for (let i = 0; i < Math.min(buttons.length, 10); i++) { // Check first 10 buttons
+ try {
+ const button = buttons[i];
+ const text = await button.textContent();
+ const isVisible = await button.isVisible();
+
+ if (isVisible && text) {
+ console.log(` Button ${i + 1}: "${text.trim()}" (visible: ${isVisible})`);
+
+ // Check if this button might be the email login trigger
+ const lowerText = text.toLowerCase().trim();
+ if (lowerText.includes('email') || lowerText.includes('sign') || lowerText.includes('continue')) {
+ console.log(` ⭐ Potential trigger button: "${text.trim()}"`);
+ foundTrigger = { element: button, text: text.trim(), selector: trigger.selector };
+ }
+ }
+ } catch (e) {
+ console.log(` Button ${i + 1}: Error - ${e.message}`);
+ }
+ }
+ } else {
+ // For specific selectors
+ const element = await page.locator(trigger.selector).first();
+ const isVisible = await element.isVisible({ timeout: 1000 });
+
+ if (isVisible) {
+ const text = await element.textContent();
+ console.log(` ✅ Found: "${text?.trim()}" (visible: ${isVisible})`);
+ foundTrigger = { element, text: text?.trim(), selector: trigger.selector };
+ break; // Use the first specific match
+ } else {
+ console.log(` ❌ Not visible or not found`);
+ }
+ }
+ } catch (e) {
+ console.log(` ❌ Error: ${e.message}`);
+ }
+ }
+
+ if (foundTrigger) {
+ console.log(`\n🎯 Found potential trigger: "${foundTrigger.text}" with selector: ${foundTrigger.selector}`);
+ console.log('🖱️ Clicking the trigger...');
+
+ try {
+ await foundTrigger.element.click();
+ console.log('✅ Clicked successfully!');
+
+ // Wait for login form to appear after clicking
+ await page.waitForTimeout(3000);
+
+ console.log('📸 Taking screenshot after click...');
+ await page.screenshot({ path: './debug-after-click.png', fullPage: true });
+
+ // Now check for inputs again
+ console.log('🔍 Checking for inputs after clicking...');
+ const inputs = await page.locator('input').all();
+ console.log(`📝 Found ${inputs.length} input elements after clicking`);
+
+ for (let i = 0; i < inputs.length; i++) {
+ const input = inputs[i];
+ try {
+ const type = await input.getAttribute('type');
+ const name = await input.getAttribute('name');
+ const placeholder = await input.getAttribute('placeholder');
+ const isVisible = await input.isVisible();
+
+ console.log(` Input ${i + 1}: type="${type}", name="${name}", placeholder="${placeholder}", visible=${isVisible}`);
+ } catch (e) {
+ console.log(` Input ${i + 1}: Error - ${e.message}`);
+ }
+ }
+
+ } catch (e) {
+ console.log(`❌ Error clicking trigger: ${e.message}`);
+ }
+ } else {
+ console.log('\n❌ No login trigger found');
+ }
+
+ // Manual inspection
+ console.log('\n🎮 Browser is open for manual inspection. Look for:');
+ console.log(' - Buttons or links that might show email login form');
+ console.log(' - Modal dialogs or popup forms');
+ console.log(' - JavaScript console errors');
+ console.log(' - Network requests when clicking buttons');
+ console.log('\nPress Ctrl+C to close when done.');
+
+ await page.waitForTimeout(300000); // Wait 5 minutes
+
+ } catch (error) {
+ console.error('❌ Error during debug:', error);
+ await page.screenshot({ path: './debug-error-v3.png', fullPage: true });
+ } finally {
+ console.log('🔚 Closing browser...');
+ await browser.close();
+ }
+}
+
+process.on('SIGINT', () => {
+ console.log('\n👋 Received SIGINT, closing browser...');
+ process.exit(0);
+});
+
+debugTradingViewLoginV3().catch(console.error);
diff --git a/debug-tradingview-visible.png b/debug-tradingview-visible.png
new file mode 100644
index 0000000..6b793fe
Binary files /dev/null and b/debug-tradingview-visible.png differ
diff --git a/debug-tradingview.js b/debug-tradingview.js
new file mode 100644
index 0000000..ee4187d
--- /dev/null
+++ b/debug-tradingview.js
@@ -0,0 +1,148 @@
+const { chromium } = require('playwright');
+
+async function debugTradingViewLogin() {
+ console.log('🚀 Starting TradingView login debug...');
+
+ const browser = await chromium.launch({
+ headless: false, // Show the browser
+ slowMo: 1000, // Slow down by 1000ms
+ devtools: true // Open devtools
+ });
+
+ const context = await browser.newContext({
+ viewport: { width: 1280, height: 720 },
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
+ });
+
+ const page = await context.newPage();
+
+ try {
+ console.log('📍 Navigating to TradingView login page...');
+ await page.goto('https://www.tradingview.com/accounts/signin/', {
+ waitUntil: 'networkidle',
+ timeout: 30000
+ });
+
+ console.log('📄 Current URL:', page.url());
+ console.log('📋 Page title:', await page.title());
+
+ // Wait a bit for dynamic content to load
+ console.log('⏳ Waiting for page to settle...');
+ await page.waitForTimeout(3000);
+
+ // Take a screenshot for debugging
+ await page.screenshot({ path: './debug-tradingview-visible.png', fullPage: true });
+ console.log('📸 Screenshot saved as debug-tradingview-visible.png');
+
+ // Try to find login form elements
+ console.log('🔍 Looking for login form elements...');
+
+ // Check for various email input selectors
+ const emailSelectors = [
+ 'input[type="email"]',
+ 'input[name="email"]',
+ 'input[id*="email"]',
+ 'input[placeholder*="email" i]',
+ 'input[placeholder*="Email" i]',
+ 'input[data-name="email"]',
+ '[data-qa="email-input"]',
+ '[name="id_username"]',
+ '[name="username"]'
+ ];
+
+ let emailInput = null;
+ for (const selector of emailSelectors) {
+ try {
+ emailInput = await page.locator(selector).first();
+ if (await emailInput.isVisible()) {
+ console.log(`✅ Found email input with selector: ${selector}`);
+ break;
+ }
+ } catch (e) {
+ // Continue to next selector
+ }
+ }
+
+ if (!emailInput || !(await emailInput.isVisible())) {
+ console.log('❌ No email input found with any selector');
+
+ // Try to find any input fields
+ const allInputs = await page.locator('input').all();
+ console.log(`📝 Found ${allInputs.length} total input elements`);
+
+ for (let i = 0; i < allInputs.length; i++) {
+ const input = allInputs[i];
+ try {
+ const type = await input.getAttribute('type');
+ const name = await input.getAttribute('name');
+ const id = await input.getAttribute('id');
+ const placeholder = await input.getAttribute('placeholder');
+ const isVisible = await input.isVisible();
+
+ console.log(` Input ${i + 1}: type="${type}", name="${name}", id="${id}", placeholder="${placeholder}", visible=${isVisible}`);
+ } catch (e) {
+ console.log(` Input ${i + 1}: Error getting attributes - ${e.message}`);
+ }
+ }
+ }
+
+ // Check for iframes
+ const frames = page.frames();
+ console.log(`🖼️ Found ${frames.length} frames on the page`);
+
+ for (let i = 0; i < frames.length; i++) {
+ const frame = frames[i];
+ try {
+ const url = frame.url();
+ const name = frame.name();
+ console.log(` Frame ${i + 1}: url="${url}", name="${name}"`);
+
+ // Check for inputs in each iframe
+ if (url && url !== 'about:blank') {
+ try {
+ const frameInputs = await frame.locator('input').all();
+ console.log(` Found ${frameInputs.length} inputs in this frame`);
+
+ for (let j = 0; j < frameInputs.length; j++) {
+ const input = frameInputs[j];
+ try {
+ const type = await input.getAttribute('type');
+ const name = await input.getAttribute('name');
+ const placeholder = await input.getAttribute('placeholder');
+ console.log(` Frame input ${j + 1}: type="${type}", name="${name}", placeholder="${placeholder}"`);
+ } catch (e) {
+ console.log(` Frame input ${j + 1}: Error - ${e.message}`);
+ }
+ }
+ } catch (e) {
+ console.log(` Error checking frame inputs: ${e.message}`);
+ }
+ }
+ } catch (e) {
+ console.log(` Frame ${i + 1}: Error - ${e.message}`);
+ }
+ }
+
+ // Wait for user interaction
+ console.log('\n🎮 Browser is open for manual inspection. Press Ctrl+C to close when done.');
+ console.log('💡 Try to find the login form manually and note down the correct selectors.');
+
+ // Keep the browser open for manual inspection
+ await page.waitForTimeout(300000); // Wait 5 minutes
+
+ } catch (error) {
+ console.error('❌ Error during debug:', error);
+ await page.screenshot({ path: './debug-tradingview-error.png', fullPage: true });
+ } finally {
+ console.log('🔚 Closing browser...');
+ await browser.close();
+ }
+}
+
+// Handle Ctrl+C to close gracefully
+process.on('SIGINT', () => {
+ console.log('\n👋 Received SIGINT, closing browser...');
+ process.exit(0);
+});
+
+debugTradingViewLogin().catch(console.error);
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
new file mode 100644
index 0000000..485752a
--- /dev/null
+++ b/docker-compose.override.yml
@@ -0,0 +1,31 @@
+# Docker Compose override for development
+# This file is automatically merged with docker-compose.yml in development
+# Use: docker compose up (will automatically include this file)
+
+services:
+ app:
+ # Development-specific settings
+ environment:
+ - NODE_ENV=development
+ - NEXT_TELEMETRY_DISABLED=1
+
+ # Enable hot reloading for development
+ volumes:
+ - ./:/app
+ - /app/node_modules
+ - ./screenshots:/app/screenshots
+ - ./videos:/app/videos
+ - ./.env:/app/.env # Mount .env file for development
+
+ # Override command for development
+ command: ["npm", "run", "dev"]
+
+ # Expose additional ports for debugging if needed
+ ports:
+ - "3000:3000"
+ - "9229:9229" # Node.js debugging port
+
+ # Add development labels
+ labels:
+ - "traefik.enable=false"
+ - "dev.local=true"
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..621a38e
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,46 @@
+# Docker Compose for production
+# Use: docker compose -f docker-compose.yml -f docker-compose.prod.yml up
+
+services:
+ app:
+ # Production-specific settings
+ environment:
+ - NODE_ENV=production
+ - DOCKER_ENV=true
+
+ # Production command
+ command: ["npm", "start"]
+
+ # Only expose necessary port
+ ports:
+ - "3000:3000"
+
+ # Production volumes (no source code mounting)
+ volumes:
+ - ./screenshots:/app/screenshots
+ - ./videos:/app/videos
+ - ./.env.production:/app/.env
+
+ # Production labels
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.trading-bot.rule=Host(`trading-bot.local`)"
+
+ # Resource limits for production
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ reservations:
+ memory: 1G
+
+ # Restart policy
+ restart: unless-stopped
+
+ # Health check
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 60s
diff --git a/docker-compose.yml b/docker-compose.yml
index c57ef07..c21d7db 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,20 +1,38 @@
-version: '3.8'
services:
app:
- build: .
- ports:
- - "3000:3000"
- volumes:
- - ./:/app
- - /app/node_modules
- - ./screenshots:/app/screenshots
+ build:
+ context: .
+ dockerfile: Dockerfile
+
+ # Base environment variables (common to all environments)
environment:
- - NODE_ENV=development
+ - DOCKER_ENV=true
- PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
- PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
+ - TRADINGVIEW_RECORD_VIDEO=true
- TZ=Europe/Berlin
+ # Playwright/TradingView automation settings
+ - CHROMIUM_PATH=/usr/bin/chromium
+ - DISABLE_CHROME_SANDBOX=true
+ - DISPLAY=:99
+
+ # Load environment variables from .env file
env_file:
- .env
- # Uncomment for debugging
- # command: ["npm", "run", "dev"]
- # entrypoint: ["/bin/bash"]
+
+ # Default port mapping
+ ports:
+ - "3000:3000"
+
+ # Base volumes
+ volumes:
+ - ./screenshots:/app/screenshots
+ - ./videos:/app/videos
+
+ # Health check
+ healthcheck:
+ test: ["CMD-SHELL", "curl -f http://localhost:3000/ || exit 1"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 30s
diff --git a/lib/ai-analysis.ts b/lib/ai-analysis.ts
index ad35a77..980a5cd 100644
--- a/lib/ai-analysis.ts
+++ b/lib/ai-analysis.ts
@@ -1,6 +1,8 @@
import OpenAI from 'openai'
import fs from 'fs/promises'
import path from 'path'
+import { enhancedScreenshotService, ScreenshotConfig } from './enhanced-screenshot'
+import { TradingViewCredentials } from './tradingview-automation'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
@@ -16,6 +18,27 @@ export interface AnalysisResult {
recommendation: 'BUY' | 'SELL' | 'HOLD'
confidence: number // 0-100
reasoning: string
+ // Enhanced trading analysis (optional)
+ entry?: {
+ price: number
+ buffer?: string
+ rationale: string
+ }
+ stopLoss?: {
+ price: number
+ rationale: string
+ }
+ takeProfits?: {
+ tp1?: { price: number; description: string }
+ tp2?: { price: number; description: string }
+ }
+ riskToReward?: string
+ confirmationTrigger?: string
+ indicatorAnalysis?: {
+ rsi?: string
+ vwap?: string
+ obv?: string
+ }
}
export class AIAnalysisService {
@@ -70,7 +93,27 @@ Return your answer as a JSON object with the following structure:
},
"recommendation": "BUY" | "SELL" | "HOLD",
"confidence": number (0-100),
- "reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers"
+ "reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers",
+ "entry": {
+ "price": number,
+ "buffer": "string describing entry buffer",
+ "rationale": "string explaining entry logic"
+ },
+ "stopLoss": {
+ "price": number,
+ "rationale": "string explaining stop loss placement"
+ },
+ "takeProfits": {
+ "tp1": { "price": number, "description": "string" },
+ "tp2": { "price": number, "description": "string" }
+ },
+ "riskToReward": "string like '1:2.5 - Risking $X to gain $Y'",
+ "confirmationTrigger": "string describing exact signal to wait for",
+ "indicatorAnalysis": {
+ "rsi": "string describing RSI behavior",
+ "vwap": "string describing VWAP behavior",
+ "obv": "string describing OBV behavior"
+ }
}
Be concise but thorough. Only return valid JSON.`
@@ -92,10 +135,34 @@ Be concise but thorough. Only return valid JSON.`
// Extract JSON from response
const match = content.match(/\{[\s\S]*\}/)
if (!match) return null
+
const json = match[0]
+ console.log('Raw JSON from AI:', json)
+
const result = JSON.parse(json)
+ console.log('Parsed result:', result)
+
+ // Sanitize the result to ensure no nested objects cause React issues
+ const sanitizedResult = {
+ summary: typeof result.summary === 'string' ? result.summary : String(result.summary || ''),
+ marketSentiment: result.marketSentiment || 'NEUTRAL',
+ keyLevels: {
+ support: Array.isArray(result.keyLevels?.support) ? result.keyLevels.support : [],
+ resistance: Array.isArray(result.keyLevels?.resistance) ? result.keyLevels.resistance : []
+ },
+ recommendation: result.recommendation || 'HOLD',
+ confidence: typeof result.confidence === 'number' ? result.confidence : 0,
+ reasoning: typeof result.reasoning === 'string' ? result.reasoning : String(result.reasoning || ''),
+ ...(result.entry && { entry: result.entry }),
+ ...(result.stopLoss && { stopLoss: result.stopLoss }),
+ ...(result.takeProfits && { takeProfits: result.takeProfits }),
+ ...(result.riskToReward && { riskToReward: String(result.riskToReward) }),
+ ...(result.confirmationTrigger && { confirmationTrigger: String(result.confirmationTrigger) }),
+ ...(result.indicatorAnalysis && { indicatorAnalysis: result.indicatorAnalysis })
+ }
+
// Optionally: validate result structure here
- return result as AnalysisResult
+ return sanitizedResult as AnalysisResult
} catch (e) {
console.error('AI analysis error:', e)
return null
@@ -114,10 +181,16 @@ Be concise but thorough. Only return valid JSON.`
images.push({ type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } })
}
- const prompt = `You are now a professional trading assistant focused on short-term crypto trading using 5–15min timeframes. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
+ const prompt = `You are an expert crypto trading analyst with advanced vision capabilities. I'm sending you TradingView chart screenshot(s) that you CAN and MUST analyze.
+
+**IMPORTANT: You have full image analysis capabilities. Please analyze the TradingView chart images I'm providing.**
Analyze the attached TradingView chart screenshots (multiple layouts of the same symbol) and provide a comprehensive trading analysis by combining insights from all charts.
+### TRADING ANALYSIS REQUIREMENTS:
+
+You are a professional trading assistant focused on short-term crypto trading using 5–15min timeframes. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
+
### WHEN GIVING A TRADE SETUP:
Be 100% SPECIFIC. Provide:
@@ -149,6 +222,8 @@ Be 100% SPECIFIC. Provide:
Cross-reference all layouts to provide the most accurate analysis. If layouts show conflicting signals, explain which one takes priority and why.
+**CRITICAL: You MUST analyze the actual chart images provided. Do not respond with generic advice.**
+
Return your answer as a JSON object with the following structure:
{
"summary": "Brief market summary combining all layouts",
@@ -159,23 +234,43 @@ Return your answer as a JSON object with the following structure:
},
"recommendation": "BUY" | "SELL" | "HOLD",
"confidence": number (0-100),
- "reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers from all layouts"
+ "reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers from all layouts",
+ "entry": {
+ "price": number,
+ "buffer": "string describing entry buffer",
+ "rationale": "string explaining entry logic"
+ },
+ "stopLoss": {
+ "price": number,
+ "rationale": "string explaining stop loss placement"
+ },
+ "takeProfits": {
+ "tp1": { "price": number, "description": "string" },
+ "tp2": { "price": number, "description": "string" }
+ },
+ "riskToReward": "string like '1:2.5 - Risking $X to gain $Y'",
+ "confirmationTrigger": "string describing exact signal to wait for",
+ "indicatorAnalysis": {
+ "rsi": "string describing RSI behavior",
+ "vwap": "string describing VWAP behavior",
+ "obv": "string describing OBV behavior"
+ }
}
Be concise but thorough. Only return valid JSON.`
const response = await openai.chat.completions.create({
- model: "gpt-4o",
+ model: "gpt-4o", // gpt-4o has better vision capabilities than gpt-4-vision-preview
messages: [
{
- role: "user",
+ role: "user",
content: [
{ type: "text", text: prompt },
...images
]
}
],
- max_tokens: 1500,
+ max_tokens: 2000, // Increased for more detailed analysis
temperature: 0.1
})
@@ -197,18 +292,152 @@ Be concise but thorough. Only return valid JSON.`
const analysis = JSON.parse(jsonMatch[0])
+ // Sanitize the analysis result to ensure no nested objects cause React issues
+ const sanitizedAnalysis = {
+ summary: typeof analysis.summary === 'string' ? analysis.summary : String(analysis.summary || ''),
+ marketSentiment: analysis.marketSentiment || 'NEUTRAL',
+ keyLevels: {
+ support: Array.isArray(analysis.keyLevels?.support) ? analysis.keyLevels.support : [],
+ resistance: Array.isArray(analysis.keyLevels?.resistance) ? analysis.keyLevels.resistance : []
+ },
+ recommendation: analysis.recommendation || 'HOLD',
+ confidence: typeof analysis.confidence === 'number' ? analysis.confidence : 0,
+ reasoning: typeof analysis.reasoning === 'string' ? analysis.reasoning : String(analysis.reasoning || ''),
+ ...(analysis.entry && { entry: analysis.entry }),
+ ...(analysis.stopLoss && { stopLoss: analysis.stopLoss }),
+ ...(analysis.takeProfits && { takeProfits: analysis.takeProfits }),
+ ...(analysis.riskToReward && { riskToReward: String(analysis.riskToReward) }),
+ ...(analysis.confirmationTrigger && { confirmationTrigger: String(analysis.confirmationTrigger) }),
+ ...(analysis.indicatorAnalysis && { indicatorAnalysis: analysis.indicatorAnalysis })
+ }
+
// Validate the structure
- if (!analysis.summary || !analysis.marketSentiment || !analysis.recommendation || !analysis.confidence) {
- console.error('Invalid analysis structure:', analysis)
+ if (!sanitizedAnalysis.summary || !sanitizedAnalysis.marketSentiment || !sanitizedAnalysis.recommendation || typeof sanitizedAnalysis.confidence !== 'number') {
+ console.error('Invalid analysis structure:', sanitizedAnalysis)
throw new Error('Invalid analysis structure')
}
- return analysis
+ return sanitizedAnalysis
} catch (error) {
console.error('AI multi-analysis error:', error)
return null
}
}
+
+ async captureAndAnalyze(
+ symbol: string,
+ timeframe: string,
+ credentials: TradingViewCredentials
+ ): Promise
{
+ try {
+ console.log(`Starting automated capture and analysis for ${symbol} ${timeframe}`)
+
+ // Capture screenshot using automation
+ const screenshot = await enhancedScreenshotService.captureQuick(symbol, timeframe, credentials)
+
+ if (!screenshot) {
+ throw new Error('Failed to capture screenshot')
+ }
+
+ console.log(`Screenshot captured: ${screenshot}`)
+
+ // Analyze the captured screenshot
+ const analysis = await this.analyzeScreenshot(screenshot)
+
+ if (!analysis) {
+ throw new Error('Failed to analyze screenshot')
+ }
+
+ console.log(`Analysis completed for ${symbol} ${timeframe}`)
+ return analysis
+
+ } catch (error) {
+ console.error('Automated capture and analysis failed:', error)
+ return null
+ }
+ }
+
+ async captureAndAnalyzeMultiple(
+ symbols: string[],
+ timeframes: string[],
+ credentials: TradingViewCredentials
+ ): Promise> {
+ const results: Array<{ symbol: string; timeframe: string; analysis: AnalysisResult | null }> = []
+
+ for (const symbol of symbols) {
+ for (const timeframe of timeframes) {
+ try {
+ console.log(`Processing ${symbol} ${timeframe}...`)
+ const analysis = await this.captureAndAnalyze(symbol, timeframe, credentials)
+
+ results.push({
+ symbol,
+ timeframe,
+ analysis
+ })
+
+ // Small delay between captures to avoid overwhelming the system
+ await new Promise(resolve => setTimeout(resolve, 2000))
+
+ } catch (error) {
+ console.error(`Failed to process ${symbol} ${timeframe}:`, error)
+ results.push({
+ symbol,
+ timeframe,
+ analysis: null
+ })
+ }
+ }
+ }
+
+ return results
+ }
+
+ async captureAndAnalyzeWithConfig(config: ScreenshotConfig): Promise<{
+ screenshots: string[]
+ analysis: AnalysisResult | null
+ }> {
+ try {
+ console.log(`Starting automated capture with config for ${config.symbol} ${config.timeframe}`)
+
+ // Capture screenshots using enhanced service
+ const screenshots = await enhancedScreenshotService.captureWithLogin(config)
+
+ if (screenshots.length === 0) {
+ throw new Error('No screenshots captured')
+ }
+
+ console.log(`${screenshots.length} screenshot(s) captured`)
+
+ let analysis: AnalysisResult | null = null
+
+ if (screenshots.length === 1) {
+ // Single screenshot analysis
+ analysis = await this.analyzeScreenshot(screenshots[0])
+ } else {
+ // Multiple screenshots analysis
+ analysis = await this.analyzeMultipleScreenshots(screenshots)
+ }
+
+ if (!analysis) {
+ throw new Error('Failed to analyze screenshots')
+ }
+
+ console.log(`Analysis completed for ${config.symbol} ${config.timeframe}`)
+
+ return {
+ screenshots,
+ analysis
+ }
+
+ } catch (error) {
+ console.error('Automated capture and analysis with config failed:', error)
+ return {
+ screenshots: [],
+ analysis: null
+ }
+ }
+ }
}
export const aiAnalysisService = new AIAnalysisService()
diff --git a/lib/auto-trading.ts b/lib/auto-trading.ts
index 48b6cbe..7ea3d05 100644
--- a/lib/auto-trading.ts
+++ b/lib/auto-trading.ts
@@ -1,4 +1,4 @@
-import { tradingViewCapture } from './tradingview'
+import { enhancedScreenshotService } from './enhanced-screenshot'
import { aiAnalysisService } from './ai-analysis'
import prisma from './prisma'
@@ -40,7 +40,9 @@ export class AutoTradingService {
if ((this.dailyTradeCount[symbol] || 0) >= this.config.maxDailyTrades) continue
// 1. Capture screenshot
const filename = `${symbol}_${Date.now()}.png`
- const screenshotPath = await tradingViewCapture.capture(symbol, filename)
+ const screenshots = await enhancedScreenshotService.capture(symbol, filename)
+ const screenshotPath = screenshots.length > 0 ? screenshots[0] : null
+ if (!screenshotPath) continue
// 2. Analyze screenshot
const analysis = await aiAnalysisService.analyzeScreenshot(filename)
if (!analysis || analysis.confidence < this.config.confidenceThreshold) continue
diff --git a/lib/enhanced-screenshot.ts b/lib/enhanced-screenshot.ts
new file mode 100644
index 0000000..6c884f1
--- /dev/null
+++ b/lib/enhanced-screenshot.ts
@@ -0,0 +1,201 @@
+import { tradingViewAutomation, TradingViewCredentials, NavigationOptions } from './tradingview-automation'
+import fs from 'fs/promises'
+import path from 'path'
+
+export interface ScreenshotConfig {
+ symbol: string
+ timeframe: string
+ layouts?: string[] // Multiple chart layouts if needed
+ credentials?: TradingViewCredentials // Optional if using .env
+}
+
+export class EnhancedScreenshotService {
+ async captureWithLogin(config: ScreenshotConfig): Promise {
+ const screenshotFiles: string[] = []
+
+ try {
+ // Ensure screenshots directory exists
+ const screenshotsDir = path.join(process.cwd(), 'screenshots')
+ await fs.mkdir(screenshotsDir, { recursive: true })
+
+ console.log('Initializing TradingView automation for Docker container...')
+
+ // Initialize automation with Docker-optimized settings
+ await tradingViewAutomation.init()
+
+ // Check if already logged in
+ const alreadyLoggedIn = await tradingViewAutomation.isLoggedIn()
+
+ if (!alreadyLoggedIn) {
+ console.log('Attempting TradingView login...')
+ const loginSuccess = await tradingViewAutomation.login(config.credentials)
+
+ if (!loginSuccess) {
+ throw new Error('Login failed')
+ }
+ } else {
+ console.log('Already logged in to TradingView')
+ }
+
+ // Navigate to chart
+ const navOptions: NavigationOptions = {
+ symbol: config.symbol,
+ timeframe: config.timeframe,
+ waitForChart: true
+ }
+
+ console.log(`Navigating to ${config.symbol} chart...`)
+ const navSuccess = await tradingViewAutomation.navigateToChart(navOptions)
+
+ if (!navSuccess) {
+ throw new Error('Chart navigation failed')
+ }
+
+ // Wait for chart data to fully load
+ const chartLoaded = await tradingViewAutomation.waitForChartData()
+
+ if (!chartLoaded) {
+ console.warn('Chart data may not be fully loaded, proceeding with screenshot anyway')
+ }
+
+ // Take screenshot
+ const timestamp = Date.now()
+ const filename = `${config.symbol}_${config.timeframe}_${timestamp}_ai.png`
+
+ console.log(`Taking screenshot: ${filename}`)
+ const screenshotFile = await tradingViewAutomation.takeScreenshot(filename)
+ screenshotFiles.push(screenshotFile)
+
+ // If multiple layouts are needed, handle them here
+ if (config.layouts && config.layouts.length > 0) {
+ for (const layout of config.layouts) {
+ // Logic to switch to different layouts would go here
+ // This depends on your specific TradingView setup
+ const layoutFilename = `${config.symbol}_${config.timeframe}_${layout}_${timestamp}_ai.png`
+ const layoutScreenshot = await tradingViewAutomation.takeScreenshot(layoutFilename)
+ screenshotFiles.push(layoutScreenshot)
+ }
+ }
+
+ console.log(`Successfully captured ${screenshotFiles.length} screenshot(s)`)
+ return screenshotFiles
+
+ } catch (error) {
+ console.error('Enhanced screenshot capture failed:', error)
+ throw error
+ } finally {
+ // Always cleanup
+ await tradingViewAutomation.close()
+ }
+ }
+
+ async captureQuick(symbol: string, timeframe: string, credentials: TradingViewCredentials): Promise {
+ try {
+ const config: ScreenshotConfig = {
+ symbol,
+ timeframe,
+ credentials
+ }
+
+ const screenshots = await this.captureWithLogin(config)
+ return screenshots.length > 0 ? screenshots[0] : null
+ } catch (error) {
+ console.error('Quick screenshot capture failed:', error)
+ return null
+ }
+ }
+
+ async captureMultipleTimeframes(
+ symbol: string,
+ timeframes: string[],
+ credentials: TradingViewCredentials
+ ): Promise {
+ const allScreenshots: string[] = []
+
+ for (const timeframe of timeframes) {
+ try {
+ console.log(`Capturing ${symbol} ${timeframe} chart...`)
+ const screenshot = await this.captureQuick(symbol, timeframe, credentials)
+ if (screenshot) {
+ allScreenshots.push(screenshot)
+ }
+ } catch (error) {
+ console.error(`Failed to capture ${symbol} ${timeframe}:`, error)
+ }
+ }
+
+ return allScreenshots
+ }
+
+ // Method to check if we can access TradingView in Docker environment
+ async healthCheck(): Promise<{ status: 'ok' | 'error'; message: string }> {
+ try {
+ console.log('Performing TradingView health check in Docker...')
+ await tradingViewAutomation.init()
+
+ // Navigate to TradingView homepage to check accessibility
+ const page = (tradingViewAutomation as any).page
+ if (!page) {
+ return { status: 'error', message: 'Failed to initialize browser page in Docker' }
+ }
+
+ await page.goto('https://www.tradingview.com/', {
+ waitUntil: 'networkidle',
+ timeout: 30000
+ })
+
+ const currentUrl = await tradingViewAutomation.getCurrentUrl()
+
+ if (currentUrl.includes('tradingview.com')) {
+ return { status: 'ok', message: 'TradingView is accessible from Docker container' }
+ } else {
+ return { status: 'error', message: 'TradingView is not accessible from Docker container' }
+ }
+ } catch (error) {
+ return { status: 'error', message: `TradingView health check failed: ${error}` }
+ } finally {
+ await tradingViewAutomation.close()
+ }
+ }
+
+ // Method to verify credentials in Docker environment
+ async verifyCredentials(credentials?: TradingViewCredentials): Promise {
+ try {
+ console.log('Verifying TradingView credentials in Docker...')
+ await tradingViewAutomation.init()
+
+ const loginSuccess = await tradingViewAutomation.login(credentials)
+ return loginSuccess
+ } catch (error) {
+ console.error('Credential verification error in Docker:', error)
+ return false
+ } finally {
+ await tradingViewAutomation.close()
+ }
+ }
+
+ // Backward compatibility method - matches old tradingViewCapture.capture() API
+ async capture(symbol: string, filename: string, layouts?: string[], timeframe?: string): Promise {
+ try {
+ console.log(`Starting Playwright-based capture for ${symbol} in Docker container`)
+
+ const config: ScreenshotConfig = {
+ symbol: symbol,
+ timeframe: timeframe || '5', // Default to 5-minute timeframe
+ layouts: layouts || []
+ }
+
+ const screenshots = await this.captureWithLogin(config)
+
+ // Return full paths to screenshots for backward compatibility
+ const screenshotsDir = path.join(process.cwd(), 'screenshots')
+ return screenshots.map(filename => path.join(screenshotsDir, filename))
+
+ } catch (error) {
+ console.error('Backward compatible capture failed:', error)
+ throw error
+ }
+ }
+}
+
+export const enhancedScreenshotService = new EnhancedScreenshotService()
diff --git a/lib/tradingview-automation.ts b/lib/tradingview-automation.ts
new file mode 100644
index 0000000..3bb61ca
--- /dev/null
+++ b/lib/tradingview-automation.ts
@@ -0,0 +1,1077 @@
+import { chromium, Browser, Page } 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
+}
+
+export class TradingViewAutomation {
+ private browser: Browser | null = null
+ private page: Page | null = null
+
+ async init(): Promise {
+ this.browser = await chromium.launch({
+ headless: true, // Must be true for Docker containers
+ 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',
+ '--no-first-run',
+ '--safebrowsing-disable-auto-update',
+ '--disable-component-extensions-with-background-pages',
+ '--disable-background-networking',
+ '--disable-software-rasterizer',
+ '--remote-debugging-port=9222'
+ ]
+ })
+
+ if (!this.browser) {
+ throw new Error('Failed to launch browser')
+ }
+
+ this.page = await this.browser.newPage()
+
+ if (!this.page) {
+ throw new Error('Failed to create new page')
+ }
+
+ // Set viewport and user agent
+ await this.page.setViewportSize({ width: 1920, height: 1080 })
+
+ // Use setExtraHTTPHeaders instead of setUserAgent for better compatibility
+ await this.page.setExtraHTTPHeaders({
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
+ })
+ }
+
+ async login(credentials?: TradingViewCredentials): Promise {
+ if (!this.page) throw new Error('Page not initialized')
+
+ // Use provided credentials or fall back to environment variables
+ const email = credentials?.email || TRADINGVIEW_EMAIL
+ const password = credentials?.password || TRADINGVIEW_PASSWORD
+
+ if (!email || !password) {
+ throw new Error('TradingView credentials not provided. Either pass credentials parameter or set TRADINGVIEW_EMAIL and TRADINGVIEW_PASSWORD in .env file')
+ }
+
+ try {
+ console.log('Navigating to TradingView login page...')
+
+ // Try different login URLs that TradingView might use
+ const loginUrls = [
+ 'https://www.tradingview.com/accounts/signin/',
+ 'https://www.tradingview.com/sign-in/',
+ 'https://www.tradingview.com/'
+ ]
+
+ let loginPageLoaded = false
+ for (const url of loginUrls) {
+ try {
+ console.log(`Trying login URL: ${url}`)
+ await this.page.goto(url, {
+ waitUntil: 'networkidle',
+ timeout: 30000
+ })
+
+ // Check if we're on the login page or need to navigate to it
+ 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/') {
+ // If we're on the main page, try to find and click the Sign In button
+ console.log('On main page, looking for Sign In button...')
+ const signInSelectors = [
+ 'a[href*="signin"]',
+ 'a:has-text("Sign in")',
+ 'button:has-text("Sign in")',
+ '.tv-header__user-menu-button--anonymous',
+ '[data-name="header-user-menu-sign-in"]',
+ '.js-signin-button'
+ ]
+
+ for (const selector of signInSelectors) {
+ try {
+ console.log(`Trying sign in selector: ${selector}`)
+ await this.page.waitForSelector(selector, { timeout: 3000 })
+ await this.page.click(selector)
+ await this.page.waitForLoadState('networkidle', { timeout: 10000 })
+
+ const newUrl = await this.page.url()
+ if (newUrl.includes('signin') || newUrl.includes('login')) {
+ console.log('Successfully navigated to login page via sign in button')
+ loginPageLoaded = true
+ break
+ }
+ } catch (e) {
+ console.log(`Sign in selector ${selector} not found or failed`)
+ }
+ }
+
+ if (loginPageLoaded) break
+ }
+
+ } catch (e) {
+ console.log(`Failed to load ${url}:`, e)
+ }
+ }
+
+ if (!loginPageLoaded) {
+ console.log('Could not reach login page, trying to proceed anyway...')
+ }
+
+ // Take a screenshot to debug the current page
+ await this.takeDebugScreenshot('page_loaded')
+
+ // Wait for page to settle and dynamic content to load
+ await this.page.waitForTimeout(5000)
+
+ // Log current URL and page title for debugging
+ const currentUrl = await this.page.url()
+ const pageTitle = await this.page.title()
+ console.log('Current URL:', currentUrl)
+ console.log('Page title:', pageTitle)
+
+ // Check if we got redirected or are on an unexpected page
+ if (!currentUrl.includes('tradingview.com')) {
+ console.log('WARNING: Not on TradingView domain!')
+ await this.takeDebugScreenshot('wrong_domain')
+ }
+
+ // Log page content length and check for common elements
+ const bodyContent = await this.page.textContent('body')
+ console.log('Page content length:', bodyContent?.length || 0)
+ console.log('Page content preview:', bodyContent?.substring(0, 500) || 'No content')
+
+ // Check for iframes that might contain the login form
+ const iframes = await this.page.$$('iframe')
+ console.log('Number of iframes found:', iframes.length)
+ if (iframes.length > 0) {
+ for (let i = 0; i < iframes.length; i++) {
+ const src = await iframes[i].getAttribute('src')
+ console.log(`Iframe ${i} src:`, src)
+ }
+ }
+
+ // Wait for any dynamic content to load
+ try {
+ // Wait for form or login-related elements to appear
+ await Promise.race([
+ this.page.waitForSelector('form', { timeout: 10000 }),
+ this.page.waitForSelector('input[type="email"]', { timeout: 10000 }),
+ this.page.waitForSelector('input[type="text"]', { timeout: 10000 }),
+ this.page.waitForSelector('input[name*="email"]', { timeout: 10000 }),
+ this.page.waitForSelector('input[name*="username"]', { timeout: 10000 })
+ ])
+ console.log('Form elements detected, proceeding...')
+ } catch (e) {
+ console.log('No form elements detected within timeout, continuing anyway...')
+ }
+
+ // Check for common login-related elements
+ const loginElements = await this.page.$$eval('*', (elements: Element[]) => {
+ const found = []
+ for (const el of elements) {
+ const text = el.textContent?.toLowerCase() || ''
+ if (text.includes('login') || text.includes('sign in') || text.includes('email') || text.includes('username')) {
+ found.push({
+ tagName: el.tagName,
+ text: text.substring(0, 100),
+ className: el.className,
+ id: el.id
+ })
+ }
+ }
+ return found.slice(0, 10) // Limit to first 10 matches
+ })
+ console.log('Login-related elements found:', JSON.stringify(loginElements, null, 2))
+
+ // CRITICAL FIX: TradingView requires clicking "Email" button to show login form
+ console.log('🔍 Looking for Email login trigger button...')
+
+ try {
+ // Wait for the "Email" button to appear and click it
+ const emailButton = this.page.locator('text="Email"').first()
+ await emailButton.waitFor({ state: 'visible', timeout: 10000 })
+ console.log('✅ Found Email button, clicking...')
+
+ await emailButton.click()
+ console.log('🖱️ Clicked Email button successfully')
+
+ // Wait for login form to appear after clicking
+ await this.page.waitForTimeout(3000)
+ console.log('⏳ Waiting for login form to appear...')
+
+ } catch (error) {
+ console.log(`❌ Could not find or click Email button: ${error}`)
+
+ // Fallback: try other possible email triggers
+ const emailTriggers = [
+ 'button:has-text("Email")',
+ 'button:has-text("email")',
+ '[data-name="email"]',
+ 'text="Sign in with email"',
+ 'text="Continue with email"'
+ ]
+
+ let triggerFound = false
+ for (const trigger of emailTriggers) {
+ try {
+ const element = this.page.locator(trigger).first()
+ if (await element.isVisible({ timeout: 2000 })) {
+ console.log(`🔄 Trying fallback trigger: ${trigger}`)
+ await element.click()
+ await this.page.waitForTimeout(2000)
+ triggerFound = true
+ break
+ }
+ } catch (e) {
+ continue
+ }
+ }
+
+ if (!triggerFound) {
+ await this.takeDebugScreenshot('no_email_trigger')
+ throw new Error('Could not find Email button or trigger to show login form')
+ }
+ }
+
+ // Try to find email input with various selectors
+ console.log('Looking for email input field...')
+ const emailSelectors = [
+ // TradingView specific selectors (discovered through debugging) - PRIORITY
+ 'input[name="id_username"]',
+
+ // Standard selectors
+ 'input[name="username"]',
+ 'input[type="email"]',
+ 'input[data-name="email"]',
+ 'input[placeholder*="email" i]',
+ 'input[placeholder*="username" i]',
+ 'input[id*="email" i]',
+ 'input[id*="username" i]',
+ 'input[class*="email" i]',
+ 'input[class*="username" i]',
+ 'input[data-testid*="email" i]',
+ 'input[data-testid*="username" i]',
+ 'input[name*="email" i]',
+ 'input[name*="user" i]',
+ 'form input[type="text"]',
+ 'form input:not([type="password"]):not([type="hidden"])',
+ '.signin-form input[type="text"]',
+ '.login-form input[type="text"]',
+ '[data-role="email"] input',
+ '[data-role="username"] input',
+
+ // More TradingView specific selectors
+ 'input[autocomplete="username"]',
+ 'input[autocomplete="email"]',
+ '.tv-signin-dialog input[type="text"]',
+ '.tv-signin-dialog input[type="email"]',
+ '#id_username',
+ '#email',
+ '#username',
+ 'input[data-test="username"]',
+ 'input[data-test="email"]'
+ ]
+
+ let emailInput = null
+ // First pass: Try selectors with timeout
+ for (const selector of emailSelectors) {
+ try {
+ console.log(`Trying email selector: ${selector}`)
+ await this.page.waitForSelector(selector, { timeout: 2000 })
+ const isVisible = await this.page.isVisible(selector)
+ if (isVisible) {
+ emailInput = selector
+ console.log(`Found email input with selector: ${selector}`)
+ break
+ }
+ } catch (e) {
+ console.log(`Email selector ${selector} not found or not visible`)
+ }
+ }
+
+ // Second pass: If no input found, check all visible inputs
+ if (!emailInput) {
+ console.log('No email input found with standard selectors. Checking all visible inputs...')
+
+ try {
+ const visibleInputs = await this.page.$$eval('input', (inputs: HTMLInputElement[]) =>
+ inputs
+ .filter((input: HTMLInputElement) => {
+ const style = window.getComputedStyle(input)
+ return style.display !== 'none' && style.visibility !== 'hidden' && input.offsetParent !== null
+ })
+ .map((input: HTMLInputElement, index: number) => ({
+ index,
+ type: input.type,
+ name: input.name,
+ id: input.id,
+ className: input.className,
+ placeholder: input.placeholder,
+ 'data-name': input.getAttribute('data-name'),
+ 'data-testid': input.getAttribute('data-testid'),
+ 'autocomplete': input.getAttribute('autocomplete'),
+ outerHTML: input.outerHTML.substring(0, 300)
+ }))
+ )
+
+ console.log('Visible inputs found:', JSON.stringify(visibleInputs, null, 2))
+
+ // Try to find the first visible text or email input
+ if (visibleInputs.length > 0) {
+ const usernameInput = visibleInputs.find((input: any) =>
+ input.type === 'email' ||
+ input.type === 'text' ||
+ input.name?.toLowerCase().includes('user') ||
+ input.name?.toLowerCase().includes('email') ||
+ input.placeholder?.toLowerCase().includes('email') ||
+ input.placeholder?.toLowerCase().includes('user')
+ )
+
+ if (usernameInput) {
+ // Create selector for this input
+ if (usernameInput.id) {
+ emailInput = `#${usernameInput.id}`
+ } else if (usernameInput.name) {
+ emailInput = `input[name="${usernameInput.name}"]`
+ } else {
+ emailInput = `input:nth-of-type(${usernameInput.index + 1})`
+ }
+ console.log(`Using detected email input: ${emailInput}`)
+ }
+ }
+ } catch (e) {
+ console.log('Error analyzing visible inputs:', e)
+ }
+ }
+
+ if (!emailInput) {
+ console.log('No email input found. Logging all input elements on page...')
+ const allInputs = await this.page.$$eval('input', (inputs: HTMLInputElement[]) =>
+ inputs.map((input: HTMLInputElement) => ({
+ type: input.type,
+ name: input.name,
+ id: input.id,
+ className: input.className,
+ placeholder: input.placeholder,
+ 'data-name': input.getAttribute('data-name'),
+ 'data-testid': input.getAttribute('data-testid'),
+ outerHTML: input.outerHTML.substring(0, 200)
+ }))
+ )
+ console.log('All inputs found:', JSON.stringify(allInputs, null, 2))
+ 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)
+
+ // Try to find password input with various selectors
+ console.log('Looking for password input field...')
+ const passwordSelectors = [
+ // TradingView specific selectors (discovered through debugging) - PRIORITY
+ 'input[name="id_password"]',
+
+ // Standard selectors
+ 'input[name="password"]',
+ 'input[type="password"]',
+ 'input[data-name="password"]',
+ 'input[placeholder*="password" i]',
+ 'input[id*="password" i]',
+ 'input[class*="password" i]',
+ 'input[data-testid*="password" i]',
+ 'form input[type="password"]',
+ '.signin-form input[type="password"]',
+ '.login-form input[type="password"]',
+ '[data-role="password"] input',
+
+ // More TradingView specific selectors
+ 'input[autocomplete="current-password"]',
+ '.tv-signin-dialog input[type="password"]',
+ '#id_password',
+ '#password',
+ 'input[data-test="password"]'
+ ]
+
+ let passwordInput = null
+ // First pass: Try selectors with timeout
+ for (const selector of passwordSelectors) {
+ try {
+ console.log(`Trying password selector: ${selector}`)
+ await this.page.waitForSelector(selector, { timeout: 2000 })
+ const isVisible = await this.page.isVisible(selector)
+ if (isVisible) {
+ passwordInput = selector
+ console.log(`Found password input with selector: ${selector}`)
+ break
+ }
+ } catch (e) {
+ console.log(`Password selector ${selector} not found or not visible`)
+ }
+ }
+
+ // Second pass: If no password input found, look for any visible password field
+ if (!passwordInput) {
+ console.log('No password input found with standard selectors. Checking all password inputs...')
+
+ try {
+ const passwordInputs = await this.page.$$eval('input[type="password"]', (inputs: HTMLInputElement[]) =>
+ inputs
+ .filter((input: HTMLInputElement) => {
+ const style = window.getComputedStyle(input)
+ return style.display !== 'none' && style.visibility !== 'hidden' && input.offsetParent !== null
+ })
+ .map((input: HTMLInputElement, index: number) => ({
+ index,
+ name: input.name,
+ id: input.id,
+ className: input.className,
+ placeholder: input.placeholder,
+ outerHTML: input.outerHTML.substring(0, 300)
+ }))
+ )
+
+ console.log('Password inputs found:', JSON.stringify(passwordInputs, null, 2))
+
+ if (passwordInputs.length > 0) {
+ const firstPassword = passwordInputs[0]
+ if (firstPassword.id) {
+ passwordInput = `#${firstPassword.id}`
+ } else if (firstPassword.name) {
+ passwordInput = `input[name="${firstPassword.name}"]`
+ } else {
+ passwordInput = `input[type="password"]:nth-of-type(${firstPassword.index + 1})`
+ }
+ console.log(`Using detected password input: ${passwordInput}`)
+ }
+ } catch (e) {
+ console.log('Error analyzing password inputs:', e)
+ }
+ }
+
+ if (!passwordInput) {
+ console.log('No password input found. Taking debug screenshot...')
+ await this.takeDebugScreenshot('no_password_field')
+ throw new Error('Could not find password input field')
+ }
+
+ // Fill password
+ console.log('Filling password field...')
+ await this.page.fill(passwordInput, password)
+
+ // Handle potential captcha
+ console.log('Checking for captcha...')
+ try {
+ const captchaFrame = this.page.frameLocator('iframe[src*="recaptcha"]').first()
+ const captchaCheckbox = captchaFrame.locator('div.recaptcha-checkbox-border')
+
+ if (await captchaCheckbox.isVisible({ timeout: 3000 })) {
+ console.log('Captcha detected, clicking checkbox...')
+ await captchaCheckbox.click()
+
+ // Wait a bit for captcha to process
+ await this.page.waitForTimeout(5000)
+
+ // Check if captcha is solved
+ const isSolved = await captchaFrame.locator('.recaptcha-checkbox-checked').isVisible({ timeout: 10000 })
+ if (!isSolved) {
+ console.log('Captcha may require manual solving. Waiting 15 seconds...')
+ await this.page.waitForTimeout(15000)
+ }
+ }
+ } catch (captchaError: any) {
+ console.log('No captcha found or captcha handling failed:', captchaError?.message || 'Unknown error')
+ }
+
+ // Find and click sign in button
+ console.log('Looking for sign in button...')
+ const submitSelectors = [
+ 'button[type="submit"]',
+ 'button:has-text("Sign in")',
+ 'button:has-text("Sign In")',
+ 'button:has-text("Log in")',
+ 'button:has-text("Log In")',
+ 'button:has-text("Login")',
+ '.tv-button--primary',
+ 'input[type="submit"]',
+ '[data-testid="signin-button"]',
+ '[data-testid="login-button"]',
+ '.signin-button',
+ '.login-button',
+ 'form button',
+ 'button[class*="submit"]',
+ 'button[class*="signin"]',
+ 'button[class*="login"]'
+ ]
+
+ let submitButton = null
+ for (const selector of submitSelectors) {
+ try {
+ console.log(`Trying submit selector: ${selector}`)
+ const element = this.page.locator(selector)
+ if (await element.isVisible({ timeout: 2000 })) {
+ submitButton = selector
+ console.log(`Found submit button with selector: ${selector}`)
+ break
+ }
+ } catch (e) {
+ console.log(`Submit selector ${selector} not found`)
+ }
+ }
+
+ if (!submitButton) {
+ console.log('No submit button found. Taking debug screenshot...')
+ await this.takeDebugScreenshot('no_submit_button')
+
+ // Log all buttons on the page
+ const allButtons = await this.page.$$eval('button', (buttons: HTMLButtonElement[]) =>
+ buttons.map((button: HTMLButtonElement) => ({
+ type: button.type,
+ textContent: button.textContent?.trim(),
+ className: button.className,
+ id: button.id,
+ outerHTML: button.outerHTML.substring(0, 200)
+ }))
+ )
+ console.log('All buttons found:', JSON.stringify(allButtons, null, 2))
+
+ throw new Error('Could not find submit button')
+ }
+
+ console.log('Clicking sign in button...')
+ await this.page.click(submitButton)
+
+ // Wait for successful login - look for the "M" watchlist indicator
+ console.log('Waiting for login success indicators...')
+ try {
+ // Wait for any of these success indicators
+ await Promise.race([
+ this.page.waitForSelector('[data-name="watchlist-button"], .tv-header__watchlist-button, button:has-text("M")', {
+ timeout: 20000
+ }),
+ this.page.waitForSelector('.tv-header__user-menu-button, .js-header-user-menu-button', {
+ timeout: 20000
+ }),
+ this.page.waitForSelector('.tv-header__logo', {
+ timeout: 20000
+ })
+ ])
+
+ // Additional check - make sure we're not still on login page
+ await this.page.waitForFunction(
+ () => !window.location.href.includes('/accounts/signin/'),
+ { timeout: 10000 }
+ )
+
+ console.log('Login successful!')
+ return true
+ } catch (error) {
+ console.error('Login verification failed:', error)
+
+ // Take a debug screenshot
+ await this.takeDebugScreenshot('login_failed')
+ return false
+ }
+
+ } catch (error) {
+ console.error('Login failed:', error)
+ await this.takeDebugScreenshot('login_error')
+ return false
+ }
+ }
+
+ async navigateToChart(options: NavigationOptions = {}): Promise {
+ if (!this.page) throw new Error('Page not initialized')
+
+ try {
+ const { symbol = 'SOLUSD', timeframe = '5', waitForChart = true } = options
+
+ console.log('Navigating to chart page...')
+
+ // Wait a bit after login before navigating
+ await this.page.waitForTimeout(2000)
+
+ // 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
+ })
+ }
+
+ // 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
+ await this.page.waitForTimeout(5000)
+ }
+
+ // Change symbol if not BTC
+ if (symbol !== 'BTCUSD') {
+ console.log(`Changing symbol to ${symbol}...`)
+ await this.changeSymbol(symbol)
+ }
+
+ // Change timeframe if specified
+ if (timeframe) {
+ console.log(`Setting timeframe to ${timeframe}...`)
+ await this.changeTimeframe(timeframe)
+ }
+
+ console.log(`Successfully navigated to ${symbol} chart with ${timeframe} timeframe`)
+ return true
+
+ } catch (error) {
+ console.error('Navigation to chart failed:', error)
+ await this.takeDebugScreenshot('navigation_failed')
+ return false
+ }
+ }
+
+ private async changeSymbol(symbol: string): Promise {
+ if (!this.page) return
+
+ try {
+ // Try multiple selectors for the symbol searcher
+ const symbolSelectors = [
+ '.tv-symbol-header__short-title',
+ '.js-symbol-title',
+ '[data-name="legend-source-title"]',
+ '.tv-symbol-header',
+ '.tv-chart-header__symbol'
+ ]
+
+ let symbolElement = null
+ for (const selector of symbolSelectors) {
+ try {
+ await this.page.waitForSelector(selector, { timeout: 3000 })
+ symbolElement = selector
+ break
+ } catch (e) {
+ console.log(`Symbol selector ${selector} not found, trying next...`)
+ }
+ }
+
+ if (!symbolElement) {
+ throw new Error('Could not find symbol selector')
+ }
+
+ await this.page.click(symbolElement)
+
+ // Wait for search input
+ const searchSelectors = [
+ 'input[data-role="search"]',
+ '.tv-dialog__body input',
+ '.tv-symbol-search-dialog__input input',
+ 'input[placeholder*="Search"]'
+ ]
+
+ let searchInput = null
+ for (const selector of searchSelectors) {
+ try {
+ await this.page.waitForSelector(selector, { timeout: 3000 })
+ searchInput = selector
+ break
+ } catch (e) {
+ console.log(`Search input selector ${selector} not found, trying next...`)
+ }
+ }
+
+ if (!searchInput) {
+ throw new Error('Could not find search input')
+ }
+
+ // Clear and type new symbol
+ await this.page.fill(searchInput, symbol)
+
+ // Wait a bit for search results
+ await this.page.waitForTimeout(2000)
+
+ // Try to click first result or press Enter
+ const resultSelectors = [
+ '.tv-screener-table__row',
+ '.js-searchbar-suggestion',
+ '.tv-symbol-search-dialog__item',
+ '.tv-symbol-search-dialog__symbol'
+ ]
+
+ let clicked = false
+ for (const selector of resultSelectors) {
+ try {
+ const firstResult = this.page.locator(selector).first()
+ if (await firstResult.isVisible({ timeout: 2000 })) {
+ await firstResult.click()
+ clicked = true
+ break
+ }
+ } catch (e) {
+ console.log(`Result selector ${selector} not found, trying next...`)
+ }
+ }
+
+ if (!clicked) {
+ console.log('No result found, pressing Enter...')
+ await this.page.press(searchInput, 'Enter')
+ }
+
+ // Wait for symbol to change
+ await this.page.waitForTimeout(3000)
+
+ } catch (error) {
+ console.error('Failed to change symbol:', error)
+ await this.takeDebugScreenshot('symbol_change_failed')
+ }
+ }
+
+ private async changeTimeframe(timeframe: string): Promise {
+ if (!this.page) return
+
+ try {
+ console.log(`Attempting to change timeframe to: ${timeframe}`)
+
+ // Wait for chart to be ready
+ await this.page.waitForTimeout(3000)
+
+ // Map common timeframe values to TradingView format
+ const timeframeMap: { [key: string]: string[] } = {
+ '1': ['1', '1m', '1min'],
+ '5': ['5', '5m', '5min'],
+ '15': ['15', '15m', '15min'],
+ '30': ['30', '30m', '30min'],
+ '60': ['1h', '1H', '60', '60m', '60min'], // Prioritize 1h format
+ '240': ['4h', '4H', '240', '240m'],
+ '1D': ['1D', 'D', 'daily'],
+ '1W': ['1W', 'W', 'weekly']
+ }
+
+ // Get possible timeframe values to try
+ const timeframesToTry = timeframeMap[timeframe] || [timeframe]
+ console.log(`Will try these timeframe values: ${timeframesToTry.join(', ')}`)
+
+ let found = false
+
+ // Take a screenshot to see current timeframe bar
+ await this.takeDebugScreenshot('before_timeframe_change')
+
+ // CRITICAL: Click the interval legend to open timeframe selector
+ console.log('🎯 Looking for interval legend to open timeframe selector...')
+ const intervalLegendSelectors = [
+ '[data-name="legend-source-interval"]',
+ '.intervalTitle-l31H9iuA',
+ '[title="Change interval"]',
+ '.intervalTitle-l31H9iuA button',
+ '[data-name="legend-source-interval"] button'
+ ]
+
+ let intervalLegendClicked = false
+ for (const selector of intervalLegendSelectors) {
+ try {
+ console.log(`Trying interval legend selector: ${selector}`)
+ const element = this.page.locator(selector).first()
+ if (await element.isVisible({ timeout: 3000 })) {
+ console.log(`✅ Found interval legend: ${selector}`)
+ await element.click()
+ await this.page.waitForTimeout(2000)
+ console.log('🖱️ Clicked interval legend - timeframe selector should be open')
+ intervalLegendClicked = true
+ break
+ }
+ } catch (e) {
+ console.log(`Interval legend selector ${selector} not found`)
+ }
+ }
+
+ if (!intervalLegendClicked) {
+ console.log('❌ Could not find interval legend to click')
+ await this.takeDebugScreenshot('no_interval_legend')
+ return
+ }
+
+ // Now look for timeframe options in the opened selector
+ console.log('🔍 Looking for timeframe options in selector...')
+
+ for (const tf of timeframesToTry) {
+ const timeframeSelectors = [
+ // After clicking interval legend, look for options
+ `[data-value="${tf}"]`,
+ `button:has-text("${tf}")`,
+ `.tv-dropdown__item:has-text("${tf}")`,
+ `.tv-interval-item:has-text("${tf}")`,
+ `[title="${tf}"]`,
+ `[aria-label*="${tf}"]`,
+
+ // Look in the opened dropdown/menu
+ `.tv-dropdown-behavior__body [data-value="${tf}"]`,
+ `.tv-dropdown-behavior__body button:has-text("${tf}")`,
+
+ // Look for list items or menu items
+ `li:has-text("${tf}")`,
+ `div[role="option"]:has-text("${tf}")`,
+ `[role="menuitem"]:has-text("${tf}")`,
+
+ // TradingView specific interval selectors
+ `.tv-screener-table__row:has-text("${tf}")`,
+ `.tv-interval-tabs button:has-text("${tf}")`,
+ `.intervals-GwQQdU8S [data-value="${tf}"]`,
+
+ // Generic selectors in visible containers
+ `.tv-dialog [data-value="${tf}"]`,
+ `.tv-dialog button:has-text("${tf}")`
+ ]
+
+ for (const selector of timeframeSelectors) {
+ try {
+ console.log(`Trying timeframe option selector: ${selector}`)
+ const element = this.page.locator(selector).first()
+
+ // Check if element exists and is visible
+ const isVisible = await element.isVisible({ timeout: 2000 })
+ if (isVisible) {
+ console.log(`✅ Found timeframe option: ${selector}`)
+ await element.click()
+ await this.page.waitForTimeout(2000)
+ console.log(`🎉 Successfully clicked timeframe option for ${tf}`)
+ found = true
+ break
+ }
+ } catch (e) {
+ console.log(`Timeframe option selector ${selector} not found or not clickable`)
+ }
+ }
+ if (found) break
+ }
+
+ // Fallback: Try keyboard navigation
+ if (!found) {
+ console.log('🔄 Timeframe options not found, trying keyboard navigation...')
+
+ // Try pressing specific keys for common timeframes
+ const keyMap: { [key: string]: string } = {
+ '60': '1', // Often 1h is mapped to '1' key
+ '1': '1',
+ '5': '5',
+ '15': '1',
+ '30': '3',
+ '240': '4',
+ '1D': 'D'
+ }
+
+ if (keyMap[timeframe]) {
+ console.log(`🎹 Trying keyboard shortcut: ${keyMap[timeframe]}`)
+ await this.page.keyboard.press(keyMap[timeframe])
+ await this.page.waitForTimeout(1000)
+ found = true
+ }
+ }
+
+ if (found) {
+ console.log(`✅ Successfully changed timeframe to ${timeframe}`)
+ await this.takeDebugScreenshot('after_timeframe_change')
+ } else {
+ console.log(`❌ Could not change timeframe to ${timeframe} - timeframe options not found`)
+ // Take a debug screenshot to see current state
+ await this.takeDebugScreenshot('timeframe_change_failed')
+
+ // Log all visible elements that might be timeframe related
+ try {
+ const visibleElements = await this.page.$$eval('[data-value], button, [role="option"], [role="menuitem"], li', (elements: Element[]) =>
+ elements
+ .filter((el: Element) => {
+ const style = window.getComputedStyle(el)
+ return style.display !== 'none' && style.visibility !== 'hidden'
+ })
+ .slice(0, 20)
+ .map((el: Element) => ({
+ tagName: el.tagName,
+ text: el.textContent?.trim().substring(0, 20),
+ className: el.className.substring(0, 50),
+ dataValue: el.getAttribute('data-value'),
+ role: el.getAttribute('role'),
+ outerHTML: el.outerHTML.substring(0, 150)
+ }))
+ )
+ console.log('Visible interactive elements:', JSON.stringify(visibleElements, null, 2))
+ } catch (e) {
+ console.log('Could not analyze visible elements')
+ }
+ }
+
+ } catch (error) {
+ console.error('Failed to change timeframe:', error)
+ await this.takeDebugScreenshot('timeframe_change_error')
+ }
+ }
+
+ async takeScreenshot(filename: string): Promise {
+ if (!this.page) throw new Error('Page not initialized')
+
+ const screenshotsDir = path.join(process.cwd(), 'screenshots')
+ await fs.mkdir(screenshotsDir, { recursive: true })
+
+ const fullPath = path.join(screenshotsDir, filename)
+
+ await this.page.screenshot({
+ path: fullPath,
+ fullPage: false, // Only visible area
+ type: 'png'
+ })
+
+ console.log(`Screenshot saved: ${filename}`)
+ return filename
+ }
+
+ private async takeDebugScreenshot(prefix: string): Promise {
+ try {
+ const timestamp = Date.now()
+ const filename = `debug_${prefix}_${timestamp}.png`
+ await this.takeScreenshot(filename)
+ } catch (error) {
+ console.error('Failed to take debug screenshot:', error)
+ }
+ }
+
+ async close(): Promise {
+ if (this.page) {
+ await this.page.close()
+ this.page = null
+ }
+ if (this.browser) {
+ await this.browser.close()
+ this.browser = null
+ }
+ }
+
+ // Utility method to wait for chart data to load
+ async waitForChartData(timeout: number = 15000): Promise {
+ if (!this.page) return false
+
+ try {
+ console.log('Waiting for chart data to load...')
+
+ // Wait for chart canvas or chart elements to be present
+ await Promise.race([
+ this.page.waitForSelector('canvas', { timeout }),
+ this.page.waitForSelector('.tv-lightweight-charts', { timeout }),
+ this.page.waitForSelector('.tv-chart-view', { timeout })
+ ])
+
+ // Additional wait for data to load
+ await this.page.waitForTimeout(3000)
+
+ console.log('Chart data loaded successfully')
+ return true
+ } catch (error) {
+ console.error('Chart data loading timeout:', error)
+ await this.takeDebugScreenshot('chart_data_timeout')
+ return false
+ }
+ }
+
+ // Get current page URL for debugging
+ async getCurrentUrl(): Promise {
+ if (!this.page) return ''
+ return await this.page.url()
+ }
+
+ // Check if we're logged in
+ async isLoggedIn(): Promise {
+ if (!this.page) return false
+
+ try {
+ const indicators = [
+ '[data-name="watchlist-button"]',
+ '.tv-header__watchlist-button',
+ '.tv-header__user-menu-button',
+ 'button:has-text("M")'
+ ]
+
+ for (const selector of indicators) {
+ try {
+ if (await this.page.locator(selector).isVisible({ timeout: 2000 })) {
+ return true
+ }
+ } catch (e) {
+ continue
+ }
+ }
+
+ return false
+ } catch (error) {
+ return false
+ }
+ }
+}
+
+export const tradingViewAutomation = new TradingViewAutomation()
diff --git a/lib/tradingview.ts b/lib/tradingview.ts
index 96c84e8..26d6006 100644
--- a/lib/tradingview.ts
+++ b/lib/tradingview.ts
@@ -3,6 +3,16 @@ import path from 'path'
import fs from 'fs/promises'
import { settingsManager } from './settings'
+// Video recording support - Simple implementation without external dependencies
+let isRecordingSupported = true
+try {
+ // Test if we can use basic screenshot recording
+ require('fs/promises')
+} catch (e) {
+ console.warn('Basic video recording not available')
+ isRecordingSupported = false
+}
+
const TRADINGVIEW_EMAIL = process.env.TRADINGVIEW_EMAIL
const TRADINGVIEW_PASSWORD = process.env.TRADINGVIEW_PASSWORD
const TRADINGVIEW_LAYOUTS = (process.env.TRADINGVIEW_LAYOUTS || '').split(',').map(l => l.trim())
@@ -15,27 +25,80 @@ const LAYOUT_URLS: { [key: string]: string } = {
// Add more layout mappings as needed
}
+// Construct layout URL with hash parameters
+const getLayoutUrl = (layoutId: string, symbol: string, timeframe?: string): string => {
+ const baseParams = `symbol=${symbol}${timeframe ? `&interval=${encodeURIComponent(timeframe)}` : ''}`
+ return `https://www.tradingview.com/chart/${layoutId}/#${baseParams}`
+}
+
export class TradingViewCapture {
private browser: Browser | null = null
private page: Page | null = null
private loggedIn = false
+ private recorder: any = null
+
+ private async debugScreenshot(step: string, page: Page): Promise {
+ try {
+ const timestamp = Date.now()
+ const filename = `debug_${step.replace(/[^a-zA-Z0-9]/g, '_')}_${timestamp}.png`
+ const screenshotsDir = path.join(process.cwd(), 'screenshots')
+ await fs.mkdir(screenshotsDir, { recursive: true })
+ const filePath = path.join(screenshotsDir, filename)
+
+ await page.screenshot({ path: filePath as `${string}.png`, type: 'png', fullPage: true })
+
+ // Also get page info for debugging
+ const pageInfo = await page.evaluate(() => ({
+ url: window.location.href,
+ title: document.title,
+ hasChart: document.querySelector('.chart-container, [data-name="chart"], canvas') !== null,
+ bodyText: document.body.textContent?.substring(0, 500) || ''
+ }))
+
+ console.log(`🔍 DEBUG Screenshot [${step}]: ${filePath}`)
+ console.log(`📄 Page Info:`, pageInfo)
+ } catch (e) {
+ console.error(`Failed to take debug screenshot for step ${step}:`, e)
+ }
+ }
async init() {
if (!this.browser) {
+ // Check for debug mode from environment variable
+ const isDebugMode = process.env.TRADINGVIEW_DEBUG === 'true'
+ const isDocker = process.env.DOCKER_ENV === 'true' || process.env.NODE_ENV === 'production'
+
+ // Docker-optimized browser args
+ const dockerArgs = [
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-accelerated-2d-canvas',
+ '--no-first-run',
+ '--no-zygote',
+ '--disable-gpu',
+ '--disable-background-timer-throttling',
+ '--disable-backgrounding-occluded-windows',
+ '--disable-renderer-backgrounding',
+ '--disable-features=TranslateUI',
+ '--disable-ipc-flooding-protection',
+ '--memory-pressure-off',
+ '--max_old_space_size=4096'
+ ]
+
+ // Additional args for non-Docker debug mode
+ const debugArgs = isDebugMode && !isDocker ? ['--start-maximized'] : []
+
this.browser = await puppeteer.launch({
- headless: true,
- args: [
- '--no-sandbox',
- '--disable-setuid-sandbox',
- '--disable-dev-shm-usage',
- '--disable-accelerated-2d-canvas',
- '--no-first-run',
- '--no-zygote',
- '--disable-gpu'
- ],
+ headless: isDocker ? true : !isDebugMode, // Always headless in Docker
+ devtools: isDebugMode && !isDocker, // DevTools only in local debug mode
+ slowMo: isDebugMode ? 250 : 0, // Slow down actions in debug mode
+ args: [...dockerArgs, ...debugArgs],
executablePath: PUPPETEER_EXECUTABLE_PATH
})
- console.log('Puppeteer browser launched')
+
+ const mode = isDocker ? 'Docker headless mode' : (isDebugMode ? 'visible debug mode' : 'headless mode')
+ console.log(`Puppeteer browser launched (${mode})`)
}
if (!this.page) {
this.page = await this.browser.newPage()
@@ -55,68 +118,131 @@ export class TradingViewCapture {
throw new Error('TradingView credentials not set in .env')
}
const page = this.page || (await this.browser!.newPage())
+
+ // Start video recording for login process
+ await this.startVideoRecording('login_process')
+
console.log('Navigating to TradingView login page...')
await page.goto('https://www.tradingview.com/#signin', { waitUntil: 'networkidle2' })
- // Check if we're already logged in
+ // Debug screenshot after initial navigation
+ await this.debugScreenshot('login_01_initial_page', page)
+
+ // Check if we're already properly logged in with our account
try {
const loggedInIndicator = await page.waitForSelector('.tv-header__user-menu-button, [data-name="header-user-menu"]', { timeout: 3000 })
if (loggedInIndicator) {
- console.log('Already logged in to TradingView')
- // Reset the loggedIn flag to true to ensure we don't re-login unnecessarily
- this.loggedIn = true
- return
+ // Check if we're logged in with our specific account by looking for account-specific elements
+ const isProperlyLoggedIn = await page.evaluate(() => {
+ // Look for specific logged-in indicators that show we have an actual account
+ const hasUserMenu = document.querySelector('.tv-header__user-menu-button, [data-name="header-user-menu"]') !== null
+ const notGuestSession = !document.body.textContent?.includes('Guest') &&
+ !document.body.textContent?.includes('Sign up') &&
+ !document.body.textContent?.includes('Get started for free')
+ return hasUserMenu && notGuestSession
+ })
+
+ if (isProperlyLoggedIn) {
+ console.log('Already properly logged in to TradingView with account')
+ await this.debugScreenshot('login_02_already_logged_in', page)
+ this.loggedIn = true
+ return
+ } else {
+ console.log('Detected guest session, forcing proper login...')
+ await this.debugScreenshot('login_02b_guest_session_detected', page)
+ // Force logout first, then login
+ try {
+ await page.goto('https://www.tradingview.com/accounts/logout/', { waitUntil: 'networkidle2', timeout: 10000 })
+ await new Promise(res => setTimeout(res, 2000))
+ } catch (e) {
+ console.log('Logout attempt completed, proceeding with login...')
+ }
+ }
}
} catch (e) {
console.log('Not logged in yet, proceeding with login...')
+ await this.debugScreenshot('login_03_not_logged_in', page)
}
// Reset login flag since we need to login
this.loggedIn = false
+ // Navigate to fresh login page
+ console.log('Navigating to fresh login page...')
+ await page.goto('https://www.tradingview.com/accounts/signin/', { waitUntil: 'networkidle2', timeout: 30000 })
+ await this.debugScreenshot('login_04_fresh_login_page', page)
+
try {
- // Wait for the login modal to appear and look for email input directly
- console.log('Looking for email input field...')
+ // Wait for the page to load and look for login form
+ console.log('Looking for login form...')
+ await page.waitForSelector('form, input[type="email"], input[name="username"]', { timeout: 10000 })
- // Try to find the email input field directly (new TradingView layout)
- const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 })
+ // Look for email input field with multiple selectors
+ let emailInput = await page.$('input[name="username"]') ||
+ await page.$('input[name="email"]') ||
+ await page.$('input[type="email"]') ||
+ await page.$('input[placeholder*="email" i]')
- if (emailInput) {
- console.log('Found email input field directly')
- await emailInput.click() // Click to focus
- await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
+ if (!emailInput) {
+ // Try to find and click "Email" button if login options are presented
+ console.log('Looking for email login option...')
+ const emailButton = await page.evaluateHandle(() => {
+ const buttons = Array.from(document.querySelectorAll('button, a, div[role="button"]'))
+ return buttons.find(btn => {
+ const text = btn.textContent?.toLowerCase() || ''
+ return text.includes('email') || text.includes('continue with email') || text.includes('sign in with email')
+ })
+ })
- // Find password field
- const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"], input[placeholder*="password" i]', { timeout: 5000 })
- if (!passwordInput) {
- throw new Error('Could not find password input field')
+ if (emailButton.asElement()) {
+ console.log('Found email login button, clicking...')
+ await emailButton.asElement()?.click()
+ await new Promise(res => setTimeout(res, 2000))
+ await this.debugScreenshot('login_04b_after_email_button', page)
+
+ // Now look for email input again
+ emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"]', { timeout: 10000 })
}
- await passwordInput.click() // Click to focus
- await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
-
- // Find and click the sign in button
- const signInButton = await page.waitForSelector('button[type="submit"]', { timeout: 5000 })
- if (!signInButton) {
- // Try to find button with sign in text
- const buttons = await page.$$('button')
- let foundButton = null
- for (const btn of buttons) {
- const text = await page.evaluate(el => el.innerText || el.textContent, btn)
- if (text && (text.toLowerCase().includes('sign in') || text.toLowerCase().includes('login'))) {
- foundButton = btn
- break
- }
- }
- if (!foundButton) {
- throw new Error('Could not find sign in button')
- }
- await foundButton.click()
- } else {
- await signInButton.click()
- }
- } else {
+ }
+
+ if (!emailInput) {
throw new Error('Could not find email input field')
}
+
+ console.log('Found email input, filling credentials...')
+ await emailInput.click() // Click to focus
+ await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
+
+ // Find password field
+ const passwordInput = await page.waitForSelector('input[name="password"], input[type="password"]', { timeout: 5000 })
+ if (!passwordInput) {
+ throw new Error('Could not find password input field')
+ }
+ await passwordInput.click() // Click to focus
+ await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
+
+ await this.debugScreenshot('login_05_credentials_filled', page)
+
+ // Find and click the sign in button
+ let signInButton = await page.$('button[type="submit"]')
+ if (!signInButton) {
+ // Look for button with sign in text
+ signInButton = await page.evaluateHandle(() => {
+ const buttons = Array.from(document.querySelectorAll('button'))
+ return buttons.find(btn => {
+ const text = btn.textContent?.toLowerCase() || ''
+ return text.includes('sign in') || text.includes('login') || text.includes('submit')
+ })
+ }).then(handle => handle.asElement())
+ }
+
+ if (!signInButton) {
+ throw new Error('Could not find sign in button')
+ }
+
+ console.log('Clicking sign in button...')
+ await signInButton.click()
+ await this.debugScreenshot('login_06_after_signin_click', page)
} catch (e) {
// Fallback: try to find email button first
console.log('Fallback: looking for email button...')
@@ -191,11 +317,72 @@ export class TradingViewCapture {
try {
console.log('Waiting for login to complete...')
await page.waitForSelector('.tv-header__user-menu-button, .chart-container, [data-name="header-user-menu"]', { timeout: 30000 })
+
+ // Check if we're on the Supercharts selection page
+ const isSuperchartsPage = await page.evaluate(() => {
+ const text = document.body.textContent || ''
+ return text.includes('Supercharts') && text.includes('The one terminal to rule them all')
+ })
+
+ if (isSuperchartsPage) {
+ console.log('🎯 On Supercharts selection page, clicking Supercharts...')
+
+ // Look for Supercharts button/link
+ let superchartsClicked = false
+
+ // Try different approaches to find and click Supercharts
+ try {
+ // Approach 1: Look for direct link to chart
+ const chartLink = await page.$('a[href*="/chart"]')
+ if (chartLink) {
+ console.log('Found direct chart link, clicking...')
+ await chartLink.click()
+ superchartsClicked = true
+ }
+ } catch (e) {
+ console.log('Direct chart link not found, trying text-based search...')
+ }
+
+ if (!superchartsClicked) {
+ // Approach 2: Find by text content
+ const clicked = await page.evaluate(() => {
+ const elements = Array.from(document.querySelectorAll('a, button, div[role="button"]'))
+ for (const el of elements) {
+ if (el.textContent?.includes('Supercharts')) {
+ (el as HTMLElement).click()
+ return true
+ }
+ }
+ return false
+ })
+ superchartsClicked = clicked
+ }
+
+ if (superchartsClicked) {
+ console.log('✅ Clicked Supercharts, waiting for charts interface...')
+ // Wait for navigation to charts interface
+ await new Promise(res => setTimeout(res, 3000))
+ await page.waitForSelector('.chart-container, [data-name="chart"]', { timeout: 15000 })
+ console.log('✅ Successfully navigated to Supercharts interface')
+ } else {
+ console.log('⚠️ Supercharts button not found, trying direct navigation...')
+ // Fallback: navigate directly to charts
+ await page.goto('https://www.tradingview.com/chart/', { waitUntil: 'networkidle2', timeout: 30000 })
+ }
+ }
+
this.loggedIn = true
console.log('TradingView login complete')
+ await this.debugScreenshot('login_04_complete', page)
+
+ // Stop video recording
+ await this.stopVideoRecording()
} catch (e) {
console.error('Login navigation did not complete.')
this.loggedIn = false
+
+ // Stop video recording on error
+ await this.stopVideoRecording()
throw new Error('Login navigation did not complete.')
}
}
@@ -224,6 +411,9 @@ export class TradingViewCapture {
const page = await this.init()
+ // Start video recording for capture process
+ await this.startVideoRecording(`capture_${finalSymbol}_${finalTimeframe || 'default'}`)
+
// Capture screenshots for each layout
const screenshots: string[] = []
@@ -234,70 +424,102 @@ export class TradingViewCapture {
// Check if we have a direct URL for this layout
const layoutUrlPath = LAYOUT_URLS[layout]
if (layoutUrlPath) {
- // Use direct layout URL
- let url = `https://www.tradingview.com/chart/${layoutUrlPath}/?symbol=${finalSymbol}`
+ // Navigate to layout URL with hash parameters, then to base chart interface
+ let layoutUrl = `https://www.tradingview.com/chart/${layoutUrlPath}/#symbol=${finalSymbol}`
if (finalTimeframe) {
- url += `&interval=${encodeURIComponent(finalTimeframe)}`
+ layoutUrl += `&interval=${encodeURIComponent(finalTimeframe)}`
}
try {
- console.log('Navigating to layout URL:', url)
- await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 })
+ console.log('🎯 Navigating to layout URL:', layoutUrl)
- // Check if we landed on the login restriction page
- const restrictionCheck = await page.evaluate(() => {
- const text = document.body.textContent || ''
- return text.includes("We can't open this chart layout for you") ||
- text.includes("log in to see it") ||
- text.includes("chart layout sharing")
- })
+ // Navigate to the specific layout URL with hash parameters and stay there
+ await page.goto(layoutUrl, { waitUntil: 'networkidle2', timeout: 60000 })
+ await this.debugScreenshot(`layout_${layout}_01_after_navigation`, page)
- if (restrictionCheck) {
- console.log(`Layout "${layout}" requires login verification, checking login status...`)
+ // Check if we get a "Chart Not Found" or "can't open this chart layout" error
+ const pageContent = await page.content()
+ const currentUrl = page.url()
+ const pageTitle = await page.title()
+
+ const isPrivateLayout = pageContent.includes("can't open this chart layout") ||
+ pageContent.includes("Chart Not Found") ||
+ pageTitle.includes("Chart Not Found") ||
+ await page.$('.tv-dialog__error, .tv-dialog__warning') !== null
+
+ if (isPrivateLayout) {
+ console.log(`⚠️ Layout "${layout}" appears to be private or not found. This might be due to:`)
+ console.log(' 1. Layout is private and requires proper account access')
+ console.log(' 2. Layout ID is incorrect or outdated')
+ console.log(' 3. Account doesn\'t have access to this layout')
+ console.log(` Current URL: ${currentUrl}`)
+ console.log(` Page title: ${pageTitle}`)
+ await this.debugScreenshot(`layout_${layout}_01b_private_layout_detected`, page)
- // Verify we're actually logged in by checking for user menu
- const loggedInCheck = await page.waitForSelector('.tv-header__user-menu-button, [data-name="header-user-menu"]', { timeout: 5000 }).catch(() => null)
+ // Check if we're properly logged in with account that should have access
+ const loginStatus = await page.evaluate(() => {
+ const hasUserMenu = document.querySelector('.tv-header__user-menu-button, [data-name="header-user-menu"]') !== null
+ const hasGuestIndicators = document.body.textContent?.includes('Guest') ||
+ document.body.textContent?.includes('Sign up') ||
+ document.body.textContent?.includes('Get started for free')
+ return { hasUserMenu, hasGuestIndicators, bodyText: document.body.textContent?.substring(0, 200) }
+ })
- if (!loggedInCheck) {
- console.log('Not properly logged in, re-authenticating...')
- // Reset login state and force re-authentication
+ if (loginStatus.hasGuestIndicators || !loginStatus.hasUserMenu) {
+ console.log('🔄 Detected we might not be properly logged in. Forcing re-login...')
this.loggedIn = false
await this.login()
- // Try navigating to the layout URL again
- console.log('Retrying navigation to layout URL after login:', url)
- await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 })
+ // Try the layout URL again after proper login
+ console.log('🔄 Retrying layout URL after proper login...')
+ await page.goto(layoutUrl, { waitUntil: 'networkidle2', timeout: 60000 })
+ await this.debugScreenshot(`layout_${layout}_01d_retry_after_login`, page)
- // Check again if we still get the restriction
- const secondCheck = await page.evaluate(() => {
- const text = document.body.textContent || ''
- return text.includes("We can't open this chart layout for you") ||
- text.includes("log in to see it") ||
- text.includes("chart layout sharing")
- })
+ // Check again if layout is accessible
+ const retryPageContent = await page.content()
+ const retryIsPrivate = retryPageContent.includes("can't open this chart layout") ||
+ retryPageContent.includes("Chart Not Found") ||
+ await page.title().then(t => t.includes("Chart Not Found"))
- if (secondCheck) {
- console.log(`Layout "${layout}" is private or not accessible, falling back to base chart`)
- // Navigate to base chart instead
- let baseUrl = `https://www.tradingview.com/chart/?symbol=${finalSymbol}`
- if (finalTimeframe) {
- baseUrl += `&interval=${encodeURIComponent(finalTimeframe)}`
- }
- await page.goto(baseUrl, { waitUntil: 'networkidle2', timeout: 60000 })
+ if (retryIsPrivate) {
+ console.log(`❌ Layout "${layout}" is still not accessible after proper login. Falling back to default chart.`)
+ } else {
+ console.log(`✅ Layout "${layout}" is now accessible after proper login!`)
+ // Continue with normal flow
+ return
}
- } else {
- console.log('User menu found but still getting restriction - layout may be private')
- // Even though we're logged in, the layout is restricted, use base chart
- console.log(`Layout "${layout}" is private or not accessible, falling back to base chart`)
- let baseUrl = `https://www.tradingview.com/chart/?symbol=${finalSymbol}`
- if (finalTimeframe) {
- baseUrl += `&interval=${encodeURIComponent(finalTimeframe)}`
- }
- await page.goto(baseUrl, { waitUntil: 'networkidle2', timeout: 60000 })
}
+
+ // Navigate to default chart with the symbol and timeframe
+ const fallbackUrl = `https://www.tradingview.com/chart/?symbol=${finalSymbol}&interval=${encodeURIComponent(finalTimeframe || '5')}`
+ console.log('🔄 Falling back to default chart:', fallbackUrl)
+ await page.goto(fallbackUrl, { waitUntil: 'networkidle2', timeout: 60000 })
+ await this.debugScreenshot(`layout_${layout}_01c_fallback_navigation`, page)
}
- console.log('Successfully navigated to layout:', layout)
+ // Wait for the layout to load properly
+ await new Promise(res => setTimeout(res, 5000))
+ await this.debugScreenshot(`layout_${layout}_02_after_wait`, page)
+
+ // Verify we're on the correct layout by checking if we can see chart content
+ const layoutLoaded = await page.evaluate(() => {
+ const hasChart = document.querySelector('.chart-container, [data-name="chart"], canvas') !== null
+ const title = document.title
+ const url = window.location.href
+ return { hasChart, title, url }
+ })
+
+ console.log('📊 Layout verification:', layoutLoaded)
+ await this.debugScreenshot(`layout_${layout}_03_after_verification`, page)
+
+ if (!layoutLoaded.hasChart) {
+ console.log('⚠️ Chart not detected, waiting longer for layout to load...')
+ await new Promise(res => setTimeout(res, 5000))
+ await this.debugScreenshot(`layout_${layout}_04_after_extra_wait`, page)
+ }
+
+ console.log(`✅ Successfully loaded layout "${layout}" and staying on it`)
+ await this.debugScreenshot(`layout_${layout}_05_final_success`, page)
} catch (e: any) {
console.error(`Failed to load layout "${layout}":`, e)
throw new Error(`Failed to load layout "${layout}": ` + (e.message || e))
@@ -335,6 +557,7 @@ export class TradingViewCapture {
const filePath = path.join(screenshotsDir, layoutFilename)
try {
+ await this.debugScreenshot(`final_screenshot_${layout}_before_capture`, page)
await page.screenshot({ path: filePath as `${string}.png`, type: 'png' })
console.log(`Screenshot saved for layout ${layout}:`, filePath)
screenshots.push(filePath)
@@ -347,6 +570,9 @@ export class TradingViewCapture {
}
}
+ // Stop video recording
+ await this.stopVideoRecording()
+
return screenshots
}
@@ -362,34 +588,189 @@ export class TradingViewCapture {
return
}
- // Construct the full URL for the layout
- const layoutUrl = `https://www.tradingview.com/chart/${layoutUrlPath}/`
- console.log('Navigating to layout URL:', layoutUrl)
-
- // Navigate directly to the layout URL
- await page.goto(layoutUrl, { waitUntil: 'networkidle2', timeout: 60000 })
- console.log('Successfully navigated to layout:', layout)
-
- // Wait for the layout to fully load
- await new Promise(res => setTimeout(res, 3000))
-
- // Take a screenshot after layout loads for debugging
- const debugAfterPath = path.resolve(`debug_after_layout_${layout.replace(/\s+/g, '_')}.png`) as `${string}.png`
- await page.screenshot({ path: debugAfterPath })
- console.log('After layout load screenshot saved:', debugAfterPath)
+ // This method is deprecated - the layout loading logic is now in captureScreenshots
+ console.log('Note: This method is deprecated. Layout loading handled in captureScreenshots.')
} catch (e: any) {
console.error(`Failed to load layout "${layout}":`, e)
-
- // Take debug screenshot on error
- const debugErrorPath = path.resolve(`debug_layout_error_${layout.replace(/\s+/g, '_')}.png`) as `${string}.png`
- await page.screenshot({ path: debugErrorPath })
- console.log('Layout error screenshot saved:', debugErrorPath)
-
- // Don't throw error, just continue with default chart
console.log('Continuing with default chart layout...')
}
}
+
+ private async startVideoRecording(filename: string): Promise {
+ if (!isRecordingSupported || !this.page) {
+ console.log('Video recording not available or page not initialized')
+ return
+ }
+
+ try {
+ const isRecordingEnabled = process.env.TRADINGVIEW_RECORD_VIDEO === 'true'
+ const isDebugMode = process.env.TRADINGVIEW_DEBUG === 'true'
+
+ if (!isRecordingEnabled && !isDebugMode) {
+ console.log('Video recording disabled (set TRADINGVIEW_RECORD_VIDEO=true to enable)')
+ return
+ }
+
+ const videosDir = path.join(process.cwd(), 'videos')
+ await fs.mkdir(videosDir, { recursive: true })
+
+ const timestamp = Date.now()
+ const videoFilename = `${filename.replace('.png', '')}_${timestamp}`
+
+ // Simple screenshot-based recording
+ this.recorder = {
+ isRecording: true,
+ filename: videoFilename,
+ videosDir,
+ screenshotCount: 0,
+ interval: null as NodeJS.Timeout | null
+ }
+
+ // Take screenshots every 2 seconds for a basic "video"
+ this.recorder.interval = setInterval(async () => {
+ if (this.recorder && this.recorder.isRecording && this.page) {
+ try {
+ const screenshotPath = path.join(this.recorder.videosDir, `${this.recorder.filename}_frame_${String(this.recorder.screenshotCount).padStart(4, '0')}.png`)
+ await this.page.screenshot({ path: screenshotPath as `${string}.png`, type: 'png' })
+ this.recorder.screenshotCount++
+ } catch (e) {
+ console.error('Failed to capture video frame:', e)
+ }
+ }
+ }, 2000)
+
+ console.log(`🎥 Video recording started (screenshot mode): ${videosDir}/${videoFilename}_frame_*.png`)
+ } catch (e) {
+ console.error('Failed to start video recording:', e)
+ this.recorder = null
+ }
+ }
+
+ private async stopVideoRecording(): Promise {
+ if (!this.recorder) {
+ return null
+ }
+
+ try {
+ this.recorder.isRecording = false
+ if (this.recorder.interval) {
+ clearInterval(this.recorder.interval)
+ }
+
+ const videoPath = `${this.recorder.videosDir}/${this.recorder.filename}_frames`
+ console.log(`🎥 Video recording stopped: ${this.recorder.screenshotCount} frames saved to ${videoPath}_*.png`)
+
+ // Optionally create a simple HTML viewer for the frames
+ const htmlPath = path.join(this.recorder.videosDir, `${this.recorder.filename}_viewer.html`)
+ const framesList = Array.from({length: this.recorder.screenshotCount}, (_, i) =>
+ `${this.recorder!.filename}_frame_${String(i).padStart(4, '0')}.png`
+ )
+
+ const htmlContent = `
+
+
+
+ Video Recording: ${this.recorder.filename}
+
+
+
+ Video Recording: ${this.recorder.filename}
+
+
+
+
+
+ Frame: / ${this.recorder.screenshotCount}
+
+
+
Total frames: ${this.recorder.screenshotCount} | Captured every 2 seconds
+
+
+
+
+
+`
+
+ await fs.writeFile(htmlPath, htmlContent, 'utf8')
+ console.log(`📄 Video viewer created: ${htmlPath}`)
+
+ this.recorder = null
+ return videoPath
+ } catch (e) {
+ console.error('Failed to stop video recording:', e)
+ this.recorder = null
+ return null
+ }
+ }
+
+ async cleanup() {
+ try {
+ // Stop any ongoing video recording
+ if (this.recorder) {
+ await this.stopVideoRecording()
+ }
+
+ // Close the page
+ if (this.page) {
+ await this.page.close()
+ this.page = null
+ }
+
+ // Close the browser
+ if (this.browser) {
+ await this.browser.close()
+ this.browser = null
+ }
+
+ this.loggedIn = false
+ console.log('TradingViewCapture cleaned up successfully')
+ } catch (e) {
+ console.error('Error during cleanup:', e)
+ }
+ }
}
export const tradingViewCapture = new TradingViewCapture()
diff --git a/package-lock.json b/package-lock.json
index 6327ccd..ac01a04 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"bs58": "^6.0.0",
"next": "15.3.5",
"openai": "^5.8.3",
+ "playwright": "^1.54.1",
"prisma": "^6.11.1",
"puppeteer": "^24.12.0"
},
@@ -5408,6 +5409,19 @@
"node": ">= 6"
}
},
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -7433,6 +7447,34 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/playwright": {
+ "version": "1.54.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz",
+ "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==",
+ "dependencies": {
+ "playwright-core": "1.54.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.54.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz",
+ "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
diff --git a/package.json b/package.json
index ab13ee8..c4321f6 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,29 @@
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
- "start": "next start"
+ "start": "next start",
+ "docker:build": "docker compose build",
+ "docker:up": "docker compose up",
+ "docker:up:build": "docker compose up --build",
+ "docker:up:detached": "docker compose up -d",
+ "docker:down": "docker compose down",
+ "docker:down:volumes": "docker compose down -v",
+ "docker:logs": "docker compose logs -f app",
+ "docker:exec": "docker compose exec app bash",
+ "docker:restart": "docker compose restart app",
+ "docker:ps": "docker compose ps",
+ "docker:pull": "docker compose pull",
+ "docker:dev": "docker compose up --build",
+ "docker:dev:detached": "docker compose up -d --build",
+ "docker:prod:build": "docker compose -f docker-compose.yml -f docker-compose.prod.yml build",
+ "docker:prod:up": "docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d",
+ "docker:prod:down": "docker compose -f docker-compose.yml -f docker-compose.prod.yml down",
+ "docker:prod:logs": "docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f app",
+ "docker:prod:restart": "docker compose -f docker-compose.yml -f docker-compose.prod.yml restart app",
+ "docker:health": "docker compose exec app curl -f http://localhost:3000/ || echo 'Health check failed'",
+ "docker:clean": "docker compose down -v && docker system prune -f",
+ "docker:reset": "docker compose down -v && docker compose build --no-cache && docker compose up -d",
+ "test:docker": "./test-docker-automation.sh"
},
"dependencies": {
"@drift-labs/sdk": "^2.126.0-beta.14",
@@ -14,6 +36,7 @@
"bs58": "^6.0.0",
"next": "15.3.5",
"openai": "^5.8.3",
+ "playwright": "^1.54.1",
"prisma": "^6.11.1",
"puppeteer": "^24.12.0"
},
diff --git a/screenshots/BTCUSD_ai_5_1752062666039.png b/screenshots/BTCUSD_ai_5_1752062666039.png
deleted file mode 100644
index 73545ae..0000000
Binary files a/screenshots/BTCUSD_ai_5_1752062666039.png and /dev/null differ
diff --git a/screenshots/SOLUSD_5_1752064560978_Diy module.png b/screenshots/SOLUSD_5_1752064560978_Diy module.png
deleted file mode 100644
index 0a01dcd..0000000
Binary files a/screenshots/SOLUSD_5_1752064560978_Diy module.png and /dev/null differ
diff --git a/screenshots/SOLUSD_5_1752064560978_ai.png b/screenshots/SOLUSD_5_1752064560978_ai.png
deleted file mode 100644
index f2b0a4a..0000000
Binary files a/screenshots/SOLUSD_5_1752064560978_ai.png and /dev/null differ
diff --git a/screenshots/SOLUSD_5_1752065182442_Diy module.png b/screenshots/SOLUSD_5_1752065182442_Diy module.png
deleted file mode 100644
index e05e6cc..0000000
Binary files a/screenshots/SOLUSD_5_1752065182442_Diy module.png and /dev/null differ
diff --git a/screenshots/SOLUSD_5_1752065182442_ai.png b/screenshots/SOLUSD_5_1752065182442_ai.png
deleted file mode 100644
index b45c075..0000000
Binary files a/screenshots/SOLUSD_5_1752065182442_ai.png and /dev/null differ
diff --git a/screenshots/SOLUSD_5_1752065763122_Diy module.png b/screenshots/SOLUSD_5_1752065763122_Diy module.png
deleted file mode 100644
index fdfdf31..0000000
Binary files a/screenshots/SOLUSD_5_1752065763122_Diy module.png and /dev/null differ
diff --git a/screenshots/SOLUSD_5_1752065763122_ai.png b/screenshots/SOLUSD_5_1752065763122_ai.png
deleted file mode 100644
index fdfdf31..0000000
Binary files a/screenshots/SOLUSD_5_1752065763122_ai.png and /dev/null differ
diff --git a/screenshots/SOLUSD_5_1752066176889_Diy module.png b/screenshots/SOLUSD_5_1752066176889_Diy module.png
deleted file mode 100644
index fdfdf31..0000000
Binary files a/screenshots/SOLUSD_5_1752066176889_Diy module.png and /dev/null differ
diff --git a/screenshots/SOLUSD_5_1752066176889_ai.png b/screenshots/SOLUSD_5_1752066176889_ai.png
deleted file mode 100644
index fdfdf31..0000000
Binary files a/screenshots/SOLUSD_5_1752066176889_ai.png and /dev/null differ
diff --git a/screenshots/SOLUSD_ai_5_1752062839851.png b/screenshots/SOLUSD_ai_5_1752062839851.png
deleted file mode 100644
index 6ca612a..0000000
Binary files a/screenshots/SOLUSD_ai_5_1752062839851.png and /dev/null differ
diff --git a/test-docker-automation.sh b/test-docker-automation.sh
new file mode 100755
index 0000000..2349afb
--- /dev/null
+++ b/test-docker-automation.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+# Test script for TradingView automation in Docker using Docker Compose v2
+echo "Testing TradingView automation in Docker container..."
+
+# Check if container is running
+if ! docker compose ps app | grep -q "Up"; then
+ echo "Starting Docker container with Docker Compose v2..."
+ docker compose up -d --build
+ sleep 10 # Wait for container to be fully ready
+fi
+
+# Test 1: Health check
+echo "1. Testing health check..."
+curl -X GET http://localhost:3000/api/trading/automated-analysis
+
+echo -e "\n\n2. Testing automated analysis (you'll need to provide credentials)..."
+echo "Example curl command:"
+echo 'curl -X POST http://localhost:3000/api/trading/automated-analysis \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"symbol\": \"SOLUSD\",
+ \"timeframe\": \"5\",
+ \"credentials\": {
+ \"email\": \"your-email@example.com\",
+ \"password\": \"your-password\"
+ }
+ }"'
+
+echo -e "\n\nNote: Replace the credentials with your actual TradingView login details."
+echo "The automation will:"
+echo "- Login to TradingView"
+echo "- Navigate to the specified chart"
+echo "- Take a screenshot"
+echo "- Analyze it with AI"
+echo "- Return the analysis results"
+
+echo -e "\n\nTo view logs: docker compose logs -f app"
+echo "To stop container: docker compose down"
diff --git a/test-video-recording.js b/test-video-recording.js
new file mode 100644
index 0000000..e6cefe3
--- /dev/null
+++ b/test-video-recording.js
@@ -0,0 +1,101 @@
+#!/usr/bin/env node
+
+/**
+ * Test script for video recording functionality
+ *
+ * Usage:
+ * - Local development with video: TRADINGVIEW_DEBUG=true TRADINGVIEW_RECORD_VIDEO=true node test-video-recording.js
+ * - Local development with GUI: TRADINGVIEW_DEBUG=true node test-video-recording.js
+ * - Docker mode: DOCKER_ENV=true node test-video-recording.js
+ */
+
+// Test via API endpoint instead of direct import
+const https = require('https');
+const http = require('http');
+
+async function testVideoRecording() {
+ try {
+ console.log('🎬 Testing TradingView video recording via API...');
+
+ // Check environment
+ const isDebugMode = process.env.TRADINGVIEW_DEBUG === 'true';
+ const isRecordingEnabled = process.env.TRADINGVIEW_RECORD_VIDEO === 'true';
+ const isDocker = process.env.DOCKER_ENV === 'true';
+
+ console.log('Environment:');
+ console.log(`- Debug mode: ${isDebugMode}`);
+ console.log(`- Video recording: ${isRecordingEnabled}`);
+ console.log(`- Docker mode: ${isDocker}`);
+
+ // Make a POST request to the analyze endpoint
+ const postData = JSON.stringify({
+ symbol: 'BTCUSD',
+ layouts: ['ai'],
+ timeframe: '5'
+ });
+
+ const options = {
+ hostname: 'localhost',
+ port: 3000,
+ path: '/api/analyze',
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Content-Length': Buffer.byteLength(postData)
+ }
+ };
+
+ console.log('\n🚀 Making request to /api/analyze endpoint...');
+ console.log('This will trigger video recording automatically if enabled.');
+
+ const req = http.request(options, (res) => {
+ console.log(`Response status: ${res.statusCode}`);
+
+ let responseData = '';
+ res.on('data', (chunk) => {
+ responseData += chunk;
+ });
+
+ res.on('end', () => {
+ try {
+ const result = JSON.parse(responseData);
+ console.log('✅ Video recording test completed!');
+ console.log('Response:', result);
+ console.log('\n📁 Check these directories:');
+ console.log('- screenshots/ directory for debug screenshots');
+ console.log('- videos/ directory for recorded videos');
+ } catch (e) {
+ console.log('Response:', responseData);
+ }
+ process.exit(0);
+ });
+ });
+
+ req.on('error', (e) => {
+ console.error('❌ Request failed:', e.message);
+ console.log('\n💡 Make sure your Next.js server is running:');
+ console.log(' npm run dev');
+ process.exit(1);
+ });
+
+ req.write(postData);
+ req.end();
+
+ } catch (error) {
+ console.error('❌ Video recording test failed:', error);
+ process.exit(1);
+ }
+}
+
+// Handle process termination
+process.on('SIGINT', () => {
+ console.log('\n🛑 Stopping video recording test...');
+ process.exit(0);
+});
+
+process.on('SIGTERM', () => {
+ console.log('\n🛑 Stopping video recording test...');
+ process.exit(0);
+});
+
+testVideoRecording();
diff --git a/test_api.sh b/test_api.sh
new file mode 100644
index 0000000..6a76db6
--- /dev/null
+++ b/test_api.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+echo "Testing TradingView analysis endpoint..."
+curl -X POST http://localhost:3000/api/analyze \
+ -H "Content-Type: application/json" \
+ -d '{"symbol":"BTCUSD","timeframe":"60","layouts":["ai"]}' \
+ -v
diff --git a/trading-settings.json b/trading-settings.json
index 365a54b..8e21eb5 100644
--- a/trading-settings.json
+++ b/trading-settings.json
@@ -1,9 +1,8 @@
{
"symbol": "SOLUSD",
- "timeframe": "5",
+ "timeframe": "60",
"layouts": [
- "ai",
- "Diy module"
+ "ai"
],
- "lastUpdated": 1752064560981
+ "lastUpdated": 1752225457945
}
\ No newline at end of file