feat: Add persistent settings and multiple layouts support
- Add settings manager to persist symbol, timeframe, and layouts - Support multiple layouts for comprehensive chart analysis - Remove debug screenshots for cleaner logs - Update AI analysis with professional trading prompt - Add multi-screenshot analysis for better trading insights - Update analyze API to use saved settings and multiple layouts
This commit is contained in:
208
lib/ai-analysis.ts
Normal file
208
lib/ai-analysis.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import OpenAI from 'openai'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
})
|
||||
|
||||
export interface AnalysisResult {
|
||||
summary: string
|
||||
marketSentiment: 'BULLISH' | 'BEARISH' | 'NEUTRAL'
|
||||
keyLevels: {
|
||||
support: number[]
|
||||
resistance: number[]
|
||||
}
|
||||
recommendation: 'BUY' | 'SELL' | 'HOLD'
|
||||
confidence: number // 0-100
|
||||
reasoning: string
|
||||
}
|
||||
|
||||
export class AIAnalysisService {
|
||||
async analyzeScreenshot(filename: string): Promise<AnalysisResult | null> {
|
||||
try {
|
||||
const screenshotsDir = path.join(process.cwd(), 'screenshots')
|
||||
const imagePath = path.join(screenshotsDir, filename)
|
||||
// Read image file
|
||||
const imageBuffer = await fs.readFile(imagePath)
|
||||
const base64Image = imageBuffer.toString('base64')
|
||||
|
||||
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.
|
||||
|
||||
Analyze the attached TradingView chart screenshot and provide a detailed trading analysis.
|
||||
|
||||
### WHEN GIVING A TRADE SETUP:
|
||||
Be 100% SPECIFIC. Provide:
|
||||
|
||||
1. **ENTRY**
|
||||
- Exact price level (with a ± entry buffer if needed)
|
||||
- Rationale: e.g., "Rejection from 15 EMA + VWAP confluence near intraday supply"
|
||||
|
||||
2. **STOP-LOSS (SL)**
|
||||
- Exact level (not arbitrary)
|
||||
- Explain *why* it's there: "Above VWAP + failed breakout zone"
|
||||
|
||||
3. **TAKE PROFITS**
|
||||
- TP1: Immediate structure (ex: previous low at $149.20)
|
||||
- TP2: Extended target if momentum continues (e.g., $148.00)
|
||||
- Mention **expected RSI/OBV behavior** at each TP zone
|
||||
|
||||
4. **RISK-TO-REWARD**
|
||||
- Show R:R. Ex: "1:2.5 — Risking $X to potentially gain $Y"
|
||||
|
||||
5. **CONFIRMATION TRIGGER**
|
||||
- Exact signal to wait for: e.g., "Bearish engulfing candle on rejection from VWAP zone"
|
||||
- OBV: "Must be making lower highs + dropping below 30min average"
|
||||
- RSI: "Should remain under 50 on rejection. Overbought ≥70 = wait"
|
||||
|
||||
6. **INDICATOR ANALYSIS**
|
||||
- **RSI**: If RSI crosses above 70 while price is under resistance → *wait*
|
||||
- **VWAP**: If price retakes VWAP with bullish momentum → *consider invalidation*
|
||||
- **OBV**: If OBV starts climbing while price stays flat → *early exit or reconsider bias*
|
||||
|
||||
Return your answer as a JSON object with the following structure:
|
||||
{
|
||||
"summary": "Brief market summary",
|
||||
"marketSentiment": "BULLISH" | "BEARISH" | "NEUTRAL",
|
||||
"keyLevels": {
|
||||
"support": [number array],
|
||||
"resistance": [number array]
|
||||
},
|
||||
"recommendation": "BUY" | "SELL" | "HOLD",
|
||||
"confidence": number (0-100),
|
||||
"reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers"
|
||||
}
|
||||
|
||||
Be concise but thorough. Only return valid JSON.`
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o", // Updated to current vision model
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: prompt },
|
||||
{ type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } }
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens: 1024
|
||||
})
|
||||
const content = response.choices[0]?.message?.content
|
||||
if (!content) return null
|
||||
// Extract JSON from response
|
||||
const match = content.match(/\{[\s\S]*\}/)
|
||||
if (!match) return null
|
||||
const json = match[0]
|
||||
const result = JSON.parse(json)
|
||||
// Optionally: validate result structure here
|
||||
return result as AnalysisResult
|
||||
} catch (e) {
|
||||
console.error('AI analysis error:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async analyzeMultipleScreenshots(filenames: string[]): Promise<AnalysisResult | null> {
|
||||
try {
|
||||
const screenshotsDir = path.join(process.cwd(), 'screenshots')
|
||||
const images: any[] = []
|
||||
|
||||
for (const filename of filenames) {
|
||||
const imagePath = path.join(screenshotsDir, filename)
|
||||
const imageBuffer = await fs.readFile(imagePath)
|
||||
const base64Image = imageBuffer.toString('base64')
|
||||
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.
|
||||
|
||||
Analyze the attached TradingView chart screenshots (multiple layouts of the same symbol) and provide a comprehensive trading analysis by combining insights from all charts.
|
||||
|
||||
### WHEN GIVING A TRADE SETUP:
|
||||
Be 100% SPECIFIC. Provide:
|
||||
|
||||
1. **ENTRY**
|
||||
- Exact price level (with a ± entry buffer if needed)
|
||||
- Rationale: e.g., "Rejection from 15 EMA + VWAP confluence near intraday supply"
|
||||
|
||||
2. **STOP-LOSS (SL)**
|
||||
- Exact level (not arbitrary)
|
||||
- Explain *why* it's there: "Above VWAP + failed breakout zone"
|
||||
|
||||
3. **TAKE PROFITS**
|
||||
- TP1: Immediate structure (ex: previous low at $149.20)
|
||||
- TP2: Extended target if momentum continues (e.g., $148.00)
|
||||
- Mention **expected RSI/OBV behavior** at each TP zone
|
||||
|
||||
4. **RISK-TO-REWARD**
|
||||
- Show R:R. Ex: "1:2.5 — Risking $X to potentially gain $Y"
|
||||
|
||||
5. **CONFIRMATION TRIGGER**
|
||||
- Exact signal to wait for: e.g., "Bearish engulfing candle on rejection from VWAP zone"
|
||||
- OBV: "Must be making lower highs + dropping below 30min average"
|
||||
- RSI: "Should remain under 50 on rejection. Overbought ≥70 = wait"
|
||||
|
||||
6. **INDICATOR ANALYSIS**
|
||||
- **RSI**: If RSI crosses above 70 while price is under resistance → *wait*
|
||||
- **VWAP**: If price retakes VWAP with bullish momentum → *consider invalidation*
|
||||
- **OBV**: If OBV starts climbing while price stays flat → *early exit or reconsider bias*
|
||||
|
||||
Cross-reference all layouts to provide the most accurate analysis. If layouts show conflicting signals, explain which one takes priority and why.
|
||||
|
||||
Return your answer as a JSON object with the following structure:
|
||||
{
|
||||
"summary": "Brief market summary combining all layouts",
|
||||
"marketSentiment": "BULLISH" | "BEARISH" | "NEUTRAL",
|
||||
"keyLevels": {
|
||||
"support": [number array],
|
||||
"resistance": [number array]
|
||||
},
|
||||
"recommendation": "BUY" | "SELL" | "HOLD",
|
||||
"confidence": number (0-100),
|
||||
"reasoning": "Detailed reasoning with specific levels, indicators, and confirmation triggers from all layouts"
|
||||
}
|
||||
|
||||
Be concise but thorough. Only return valid JSON.`
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: prompt },
|
||||
...images
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens: 1500,
|
||||
temperature: 0.1
|
||||
})
|
||||
|
||||
const content = response.choices[0]?.message?.content
|
||||
if (!content) {
|
||||
throw new Error('No content received from OpenAI')
|
||||
}
|
||||
|
||||
// Parse the JSON response
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/)
|
||||
if (!jsonMatch) {
|
||||
throw new Error('No JSON found in response')
|
||||
}
|
||||
|
||||
const analysis = JSON.parse(jsonMatch[0])
|
||||
|
||||
// Validate the structure
|
||||
if (!analysis.summary || !analysis.marketSentiment || !analysis.recommendation || !analysis.confidence) {
|
||||
throw new Error('Invalid analysis structure')
|
||||
}
|
||||
|
||||
return analysis
|
||||
} catch (error) {
|
||||
console.error('AI multi-analysis error:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const aiAnalysisService = new AIAnalysisService()
|
||||
82
lib/auto-trading.ts
Normal file
82
lib/auto-trading.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { tradingViewCapture } from './tradingview'
|
||||
import { aiAnalysisService } from './ai-analysis'
|
||||
import prisma from './prisma'
|
||||
|
||||
export interface AutoTradingConfig {
|
||||
enabled: boolean
|
||||
symbols: string[]
|
||||
intervalMinutes: number
|
||||
maxDailyTrades: number
|
||||
tradingAmount: number
|
||||
confidenceThreshold: number
|
||||
}
|
||||
|
||||
export class AutoTradingService {
|
||||
private config: AutoTradingConfig
|
||||
private intervalId: NodeJS.Timeout | null = null
|
||||
private dailyTradeCount: Record<string, number> = {}
|
||||
|
||||
constructor(config: AutoTradingConfig) {
|
||||
this.config = config
|
||||
this.dailyTradeCount = {}
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.intervalId || !this.config.enabled) return
|
||||
this.intervalId = setInterval(() => this.run(), this.config.intervalMinutes * 60 * 1000)
|
||||
this.run() // Run immediately on start
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId)
|
||||
this.intervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
async run() {
|
||||
if (!this.config.enabled) return
|
||||
for (const symbol of this.config.symbols) {
|
||||
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)
|
||||
// 2. Analyze screenshot
|
||||
const analysis = await aiAnalysisService.analyzeScreenshot(filename)
|
||||
if (!analysis || analysis.confidence < this.config.confidenceThreshold) continue
|
||||
// 3. Execute trade (stub: integrate with driftTradingService)
|
||||
// const tradeResult = await driftTradingService.executeTrade({ ... })
|
||||
// 4. Save trade to DB
|
||||
await prisma.trade.create({
|
||||
data: {
|
||||
symbol,
|
||||
side: analysis.recommendation === 'BUY' ? 'LONG' : analysis.recommendation === 'SELL' ? 'SHORT' : 'NONE',
|
||||
amount: this.config.tradingAmount,
|
||||
price: 0, // To be filled with actual execution price
|
||||
status: 'PENDING',
|
||||
screenshotUrl: screenshotPath,
|
||||
aiAnalysis: JSON.stringify(analysis),
|
||||
executedAt: new Date(),
|
||||
userId: 'system', // Or actual user if available
|
||||
}
|
||||
})
|
||||
this.dailyTradeCount[symbol] = (this.dailyTradeCount[symbol] || 0) + 1
|
||||
}
|
||||
}
|
||||
|
||||
setConfig(config: Partial<AutoTradingConfig>) {
|
||||
this.config = { ...this.config, ...config }
|
||||
}
|
||||
}
|
||||
|
||||
export function getAutoTradingService() {
|
||||
// Singleton pattern or similar
|
||||
return new AutoTradingService({
|
||||
enabled: false,
|
||||
symbols: ['BTCUSD'],
|
||||
intervalMinutes: 15,
|
||||
maxDailyTrades: 10,
|
||||
tradingAmount: 100,
|
||||
confidenceThreshold: 80
|
||||
})
|
||||
}
|
||||
13
lib/prisma.ts
Normal file
13
lib/prisma.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
declare global {
|
||||
var prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
const prisma = globalThis.prisma || new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalThis.prisma = prisma
|
||||
}
|
||||
|
||||
export default prisma
|
||||
88
lib/settings.ts
Normal file
88
lib/settings.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
export interface TradingSettings {
|
||||
symbol: string
|
||||
timeframe: string
|
||||
layouts: string[]
|
||||
lastUpdated: number
|
||||
}
|
||||
|
||||
const SETTINGS_FILE = path.join(process.cwd(), 'trading-settings.json')
|
||||
|
||||
export class SettingsManager {
|
||||
private static instance: SettingsManager
|
||||
private settings: TradingSettings = {
|
||||
symbol: 'BTCUSD',
|
||||
timeframe: '5',
|
||||
layouts: ['ai'],
|
||||
lastUpdated: Date.now()
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): SettingsManager {
|
||||
if (!SettingsManager.instance) {
|
||||
SettingsManager.instance = new SettingsManager()
|
||||
}
|
||||
return SettingsManager.instance
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<TradingSettings> {
|
||||
try {
|
||||
const data = await fs.readFile(SETTINGS_FILE, 'utf-8')
|
||||
this.settings = JSON.parse(data)
|
||||
console.log('Loaded settings:', this.settings)
|
||||
} catch (error) {
|
||||
console.log('No existing settings found, using defaults')
|
||||
await this.saveSettings()
|
||||
}
|
||||
return this.settings
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
try {
|
||||
this.settings.lastUpdated = Date.now()
|
||||
await fs.writeFile(SETTINGS_FILE, JSON.stringify(this.settings, null, 2))
|
||||
console.log('Settings saved:', this.settings)
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async updateSettings(updates: Partial<TradingSettings>): Promise<TradingSettings> {
|
||||
this.settings = { ...this.settings, ...updates }
|
||||
await this.saveSettings()
|
||||
return this.settings
|
||||
}
|
||||
|
||||
getSettings(): TradingSettings {
|
||||
return this.settings
|
||||
}
|
||||
|
||||
async setSymbol(symbol: string): Promise<void> {
|
||||
await this.updateSettings({ symbol })
|
||||
}
|
||||
|
||||
async setTimeframe(timeframe: string): Promise<void> {
|
||||
await this.updateSettings({ timeframe })
|
||||
}
|
||||
|
||||
async setLayouts(layouts: string[]): Promise<void> {
|
||||
await this.updateSettings({ layouts })
|
||||
}
|
||||
|
||||
async addLayout(layout: string): Promise<void> {
|
||||
if (!this.settings.layouts.includes(layout)) {
|
||||
const layouts = [...this.settings.layouts, layout]
|
||||
await this.updateSettings({ layouts })
|
||||
}
|
||||
}
|
||||
|
||||
async removeLayout(layout: string): Promise<void> {
|
||||
const layouts = this.settings.layouts.filter(l => l !== layout)
|
||||
await this.updateSettings({ layouts })
|
||||
}
|
||||
}
|
||||
|
||||
export const settingsManager = SettingsManager.getInstance()
|
||||
396
lib/tradingview.ts
Normal file
396
lib/tradingview.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import puppeteer, { Browser, Page, Frame } from 'puppeteer'
|
||||
import path from 'path'
|
||||
import fs from 'fs/promises'
|
||||
import { settingsManager } from './settings'
|
||||
|
||||
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())
|
||||
const PUPPETEER_EXECUTABLE_PATH = process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium'
|
||||
|
||||
export class TradingViewCapture {
|
||||
private browser: Browser | null = null
|
||||
private page: Page | null = null
|
||||
private loggedIn = false
|
||||
|
||||
async init() {
|
||||
if (!this.browser) {
|
||||
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'
|
||||
],
|
||||
executablePath: PUPPETEER_EXECUTABLE_PATH
|
||||
})
|
||||
console.log('Puppeteer browser launched')
|
||||
}
|
||||
if (!this.page) {
|
||||
this.page = await this.browser.newPage()
|
||||
await this.page.setViewport({ width: 1920, height: 1080 })
|
||||
console.log('Puppeteer page created')
|
||||
}
|
||||
if (!this.loggedIn) {
|
||||
console.log('Logging in to TradingView...')
|
||||
await this.login()
|
||||
this.loggedIn = true
|
||||
console.log('Logged in to TradingView')
|
||||
}
|
||||
return this.page
|
||||
}
|
||||
|
||||
async login() {
|
||||
if (!TRADINGVIEW_EMAIL || !TRADINGVIEW_PASSWORD) {
|
||||
throw new Error('TradingView credentials not set in .env')
|
||||
}
|
||||
const page = this.page || (await this.browser!.newPage())
|
||||
console.log('Navigating to TradingView login page...')
|
||||
await page.goto('https://www.tradingview.com/#signin', { waitUntil: 'networkidle2' })
|
||||
|
||||
// Check if we're already logged in
|
||||
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')
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Not logged in yet, proceeding with login...')
|
||||
}
|
||||
|
||||
try {
|
||||
// Wait for the login modal to appear and look for email input directly
|
||||
console.log('Looking for email input field...')
|
||||
|
||||
// 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 })
|
||||
|
||||
if (emailInput) {
|
||||
console.log('Found email input field directly')
|
||||
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"], input[placeholder*="password" i]', { 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 })
|
||||
|
||||
// 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 {
|
||||
throw new Error('Could not find email input field')
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback: try to find email button first
|
||||
console.log('Fallback: looking for email button...')
|
||||
try {
|
||||
await page.waitForSelector('button', { timeout: 15000 })
|
||||
const buttons = await page.$$('button')
|
||||
let emailBtn = null
|
||||
|
||||
// Look for email button with various text patterns
|
||||
for (const btn of buttons) {
|
||||
const text = await page.evaluate(el => el.innerText || el.textContent, btn)
|
||||
if (text && (
|
||||
text.trim().toLowerCase().includes('email') ||
|
||||
text.trim().toLowerCase().includes('sign in with email') ||
|
||||
text.trim().toLowerCase().includes('continue with email')
|
||||
)) {
|
||||
emailBtn = btn
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (emailBtn) {
|
||||
console.log('Found email button, clicking...')
|
||||
await emailBtn.click()
|
||||
await new Promise(res => setTimeout(res, 1000))
|
||||
|
||||
// Now fill in the form
|
||||
const emailInput = await page.waitForSelector('input[name="username"], input[name="email"], input[type="email"], input[placeholder*="email" i]', { timeout: 10000 })
|
||||
if (!emailInput) {
|
||||
throw new Error('Could not find email input field after clicking email button')
|
||||
}
|
||||
await emailInput.click() // Click to focus
|
||||
await emailInput.type(TRADINGVIEW_EMAIL, { delay: 50 })
|
||||
|
||||
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 after clicking email button')
|
||||
}
|
||||
await passwordInput.click() // Click to focus
|
||||
await passwordInput.type(TRADINGVIEW_PASSWORD, { delay: 50 })
|
||||
|
||||
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 after clicking email button')
|
||||
}
|
||||
await foundButton.click()
|
||||
} else {
|
||||
await signInButton.click()
|
||||
}
|
||||
} else {
|
||||
throw new Error('Could not find email button')
|
||||
}
|
||||
} catch (e2) {
|
||||
console.error('Could not find or click email button:', e2)
|
||||
const errorMessage = e2 instanceof Error ? e2.message : String(e2)
|
||||
throw new Error('Could not find or click email button on TradingView login page. ' + errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for navigation or dashboard (main page)
|
||||
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 })
|
||||
} catch (e) {
|
||||
console.error('Login navigation did not complete.')
|
||||
throw new Error('Login navigation did not complete.')
|
||||
}
|
||||
|
||||
console.log('TradingView login complete')
|
||||
}
|
||||
|
||||
async capture(symbol: string, filename: string, layouts?: string[], timeframe?: string) {
|
||||
console.log('Working directory:', process.cwd())
|
||||
|
||||
// Load settings and update if provided
|
||||
const settings = await settingsManager.loadSettings()
|
||||
if (symbol && symbol !== settings.symbol) {
|
||||
await settingsManager.setSymbol(symbol)
|
||||
}
|
||||
if (timeframe && timeframe !== settings.timeframe) {
|
||||
await settingsManager.setTimeframe(timeframe)
|
||||
}
|
||||
if (layouts && JSON.stringify(layouts) !== JSON.stringify(settings.layouts)) {
|
||||
await settingsManager.setLayouts(layouts)
|
||||
}
|
||||
|
||||
// Use saved settings if not provided
|
||||
const finalSymbol = symbol || settings.symbol
|
||||
const finalTimeframe = timeframe || settings.timeframe
|
||||
const finalLayouts = layouts || settings.layouts
|
||||
|
||||
console.log('Using settings:', { symbol: finalSymbol, timeframe: finalTimeframe, layouts: finalLayouts })
|
||||
|
||||
const page = await this.init()
|
||||
// Add timeframe to TradingView URL if provided
|
||||
let url = `https://www.tradingview.com/chart/?symbol=${finalSymbol}`
|
||||
if (finalTimeframe) {
|
||||
url += `&interval=${encodeURIComponent(finalTimeframe)}`
|
||||
}
|
||||
try {
|
||||
console.log('Navigating to TradingView chart:', url)
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 })
|
||||
console.log('Successfully navigated to chart')
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load TradingView chart page:', e)
|
||||
throw new Error('Failed to load TradingView chart page: ' + (e.message || e))
|
||||
}
|
||||
|
||||
// Capture screenshots for each layout
|
||||
const screenshots: string[] = []
|
||||
|
||||
for (let i = 0; i < finalLayouts.length; i++) {
|
||||
const layout = finalLayouts[i]
|
||||
console.log(`Processing layout ${i + 1}/${finalLayouts.length}: ${layout}`)
|
||||
|
||||
// Load the layout
|
||||
await this.loadLayout(page, layout)
|
||||
|
||||
// Wait for layout to load
|
||||
await new Promise(res => setTimeout(res, 3000))
|
||||
|
||||
// Generate filename for this layout
|
||||
const layoutFilename = filename.replace('.png', `_${layout}.png`)
|
||||
const screenshotsDir = path.join(process.cwd(), 'screenshots')
|
||||
await fs.mkdir(screenshotsDir, { recursive: true })
|
||||
const filePath = path.join(screenshotsDir, layoutFilename)
|
||||
|
||||
try {
|
||||
await page.screenshot({ path: filePath as `${string}.png`, type: 'png' })
|
||||
console.log(`Screenshot saved for layout ${layout}:`, filePath)
|
||||
screenshots.push(filePath)
|
||||
} catch (e: any) {
|
||||
const debugScreenshotErrorPath = path.resolve(`debug_screenshot_error_${layout}.png`) as `${string}.png`
|
||||
await page.screenshot({ path: debugScreenshotErrorPath })
|
||||
console.error(`Failed to capture screenshot for layout ${layout}:`, e)
|
||||
console.error('Screenshot on screenshot error:', debugScreenshotErrorPath)
|
||||
throw new Error(`Failed to capture screenshot for layout ${layout}: ` + (e.message || e))
|
||||
}
|
||||
}
|
||||
|
||||
return screenshots
|
||||
}
|
||||
|
||||
private async loadLayout(page: Page, layout: string): Promise<void> {
|
||||
try {
|
||||
console.log('Trying to load layout:', layout)
|
||||
|
||||
// Try multiple selectors for the layout button
|
||||
const layoutSelectors = [
|
||||
'[data-name="load-chart-layout-dialog"]',
|
||||
'[data-name="layouts-menu"]',
|
||||
'[data-name="chart-layout-button"]',
|
||||
'button[title*="Layout" i]',
|
||||
'button[aria-label*="Layout" i]',
|
||||
'[data-testid*="layout"]'
|
||||
]
|
||||
|
||||
let layoutButton = null
|
||||
for (const selector of layoutSelectors) {
|
||||
try {
|
||||
layoutButton = await page.waitForSelector(selector, { timeout: 3000 })
|
||||
if (layoutButton) {
|
||||
console.log('Found layout button with selector:', selector)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue to next selector
|
||||
}
|
||||
}
|
||||
|
||||
if (!layoutButton) {
|
||||
// Try to find layout button by text content
|
||||
const buttons = await page.$$('button, [role="button"]')
|
||||
for (const btn of buttons) {
|
||||
const text = await page.evaluate(el => {
|
||||
const element = el as HTMLElement
|
||||
return element.innerText || element.textContent || element.title || element.getAttribute('aria-label')
|
||||
}, btn)
|
||||
if (text && text.toLowerCase().includes('layout')) {
|
||||
layoutButton = btn
|
||||
console.log('Found layout button by text:', text)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutButton) {
|
||||
await layoutButton.click()
|
||||
console.log('Clicked layout button')
|
||||
await new Promise(res => setTimeout(res, 1000))
|
||||
|
||||
// Look for search input or layout items
|
||||
const searchSelectors = [
|
||||
'input[name="search"]',
|
||||
'input[placeholder*="search" i]',
|
||||
'input[type="search"]',
|
||||
'input[data-name="search"]'
|
||||
]
|
||||
|
||||
let searchInput = null
|
||||
for (const selector of searchSelectors) {
|
||||
try {
|
||||
searchInput = await page.waitForSelector(selector, { timeout: 3000 })
|
||||
if (searchInput) break
|
||||
} catch (e) {
|
||||
// Continue to next selector
|
||||
}
|
||||
}
|
||||
|
||||
if (searchInput) {
|
||||
console.log('Found search input, typing layout name...')
|
||||
await searchInput.type(layout, { delay: 50 })
|
||||
await new Promise(res => setTimeout(res, 2000))
|
||||
|
||||
// Try to find and click the layout item
|
||||
const layoutItemSelectors = [
|
||||
'[data-name="chart-layout-list-item"]',
|
||||
'[data-testid*="layout-item"]',
|
||||
'.layout-item',
|
||||
'[role="option"]'
|
||||
]
|
||||
|
||||
let layoutItem = null
|
||||
for (const selector of layoutItemSelectors) {
|
||||
try {
|
||||
const items = await page.$$(selector)
|
||||
for (const item of items) {
|
||||
const text = await page.evaluate(el => {
|
||||
const element = el as HTMLElement
|
||||
return element.innerText || element.textContent
|
||||
}, item)
|
||||
if (text && text.toLowerCase().includes(layout.toLowerCase())) {
|
||||
layoutItem = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if (layoutItem) break
|
||||
} catch (e) {
|
||||
// Continue to next selector
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutItem) {
|
||||
await layoutItem.click()
|
||||
console.log('Clicked layout item:', layout)
|
||||
} else {
|
||||
console.log('Layout item not found, trying generic approach...')
|
||||
await page.evaluate((layout) => {
|
||||
const items = Array.from(document.querySelectorAll('*'))
|
||||
const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase()))
|
||||
if (item) (item as HTMLElement).click()
|
||||
}, layout)
|
||||
}
|
||||
} else {
|
||||
console.log('Search input not found, trying to find layout directly...')
|
||||
await page.evaluate((layout) => {
|
||||
const items = Array.from(document.querySelectorAll('*'))
|
||||
const item = items.find(el => el.textContent?.toLowerCase().includes(layout.toLowerCase()))
|
||||
if (item) (item as HTMLElement).click()
|
||||
}, layout)
|
||||
}
|
||||
await new Promise(res => setTimeout(res, 4000))
|
||||
console.log('Layout loaded:', layout)
|
||||
} else {
|
||||
console.log('Layout button not found, skipping layout loading')
|
||||
}
|
||||
} catch (e: any) {
|
||||
const debugLayoutErrorPath = path.resolve('debug_layout_error.png') as `${string}.png`
|
||||
await page.screenshot({ path: debugLayoutErrorPath })
|
||||
console.error('TradingView layout not found or could not be loaded:', e)
|
||||
console.log('Continuing without layout...')
|
||||
// Don't throw error, just continue without layout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const tradingViewCapture = new TradingViewCapture()
|
||||
Reference in New Issue
Block a user