diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b9d8a64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +node_modules +.next +.git +.gitignore +README.md +Dockerfile +.dockerignore +docker-compose.yml +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +lerna-debug.log* +dist +dist-ssr +*.local +.env*.local +.vscode +.DS_Store +*.tsbuildinfo diff --git a/.gitignore b/.gitignore index 26b002a..425b5a1 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# videos and screenshots +/videos/ +/screenshots/ diff --git a/CREDENTIAL_USAGE.md b/CREDENTIAL_USAGE.md new file mode 100644 index 0000000..ff7e12c --- /dev/null +++ b/CREDENTIAL_USAGE.md @@ -0,0 +1,113 @@ +# TradingView Automation - Credential Usage + +## Overview + +You now have **two ways** to provide TradingView credentials: + +### Method 1: Environment Variables (.env file) - Like the original +```bash +# In your .env file +TRADINGVIEW_EMAIL=your-email@example.com +TRADINGVIEW_PASSWORD=your-password +``` + +### Method 2: API Parameters - More flexible +```json +{ + "symbol": "SOLUSD", + "timeframe": "5", + "credentials": { + "email": "your-email@example.com", + "password": "your-password" + } +} +``` + +## Usage Examples + +### 1. Using .env credentials (like original tradingview.ts) + +```bash +# Set in .env file first +echo "TRADINGVIEW_EMAIL=your-email@example.com" >> .env +echo "TRADINGVIEW_PASSWORD=your-password" >> .env + +# Then call API without credentials +curl -X POST http://localhost:3000/api/trading/automated-analysis \ + -H "Content-Type: application/json" \ + -d '{ + "symbol": "SOLUSD", + "timeframe": "5" + }' +``` + +### 2. Using API parameters (more secure for multi-user) + +```bash +curl -X POST http://localhost:3000/api/trading/automated-analysis \ + -H "Content-Type: application/json" \ + -d '{ + "symbol": "SOLUSD", + "timeframe": "5", + "credentials": { + "email": "user1@example.com", + "password": "password1" + } + }' +``` + +### 3. Programmatic usage + +```typescript +import { enhancedScreenshotService } from './lib/enhanced-screenshot' + +// Method 1: Using .env (no credentials needed) +const screenshots1 = await enhancedScreenshotService.captureWithLogin({ + symbol: 'SOLUSD', + timeframe: '5' + // credentials will be read from .env automatically +}) + +// Method 2: Explicit credentials +const screenshots2 = await enhancedScreenshotService.captureWithLogin({ + symbol: 'SOLUSD', + timeframe: '5', + credentials: { + email: 'user@example.com', + password: 'password' + } +}) +``` + +## Key Differences from Original + +| Feature | tradingview.ts (Original) | tradingview-automation.ts (New) | +|---------|---------------------------|----------------------------------| +| Framework | Puppeteer | Playwright | +| Credentials | Only .env | .env OR parameters | +| Usage | Complex layouts | Simple AI analysis | +| Video Recording | ✅ Yes | ❌ No | +| Code Size | 777 lines | ~400 lines | +| Docker Optimized | ✅ Yes | ✅ Yes | +| Debug Screenshots | ✅ Extensive | ✅ Basic | + +## Priority Order for Credentials + +1. **API parameters** (if provided) +2. **Environment variables** (.env file) +3. **Error** if neither is available + +## Security Considerations + +- **Environment variables**: Good for single-user setups +- **API parameters**: Better for multi-user applications +- **Never commit credentials** to git repositories +- Use `.env.local` for local development + +## Migration from Original + +If you're currently using the original `tradingview.ts`: + +1. **Keep using .env**: No changes needed, just call the new API +2. **Switch to parameters**: More flexible, supports multiple users +3. **Hybrid approach**: Use .env as fallback, parameters for specific users diff --git a/DOCKER_AUTOMATION.md b/DOCKER_AUTOMATION.md new file mode 100644 index 0000000..8bf9498 --- /dev/null +++ b/DOCKER_AUTOMATION.md @@ -0,0 +1,366 @@ +# Docker Compose v2 Commands + +This project uses Docker Compose v2. Here are the most common commands: + +## Available npm Scripts + +### Development Scripts +```bash +# Build and start for development (with override) +npm run docker:dev # docker compose up --build +npm run docker:dev:detached # docker compose up -d --build + +# Basic operations +npm run docker:build # docker compose build +npm run docker:up # docker compose up +npm run docker:up:build # docker compose up --build +npm run docker:up:detached # docker compose up -d +npm run docker:down # docker compose down +npm run docker:down:volumes # docker compose down -v +``` + +### Production Scripts +```bash +# Production environment +npm run docker:prod:build # Build for production +npm run docker:prod:up # Start production stack +npm run docker:prod:down # Stop production stack +npm run docker:prod:logs # View production logs +npm run docker:prod:restart # Restart production app +``` + +### Maintenance Scripts +```bash +# Monitoring and debugging +npm run docker:logs # View app logs +npm run docker:ps # Show running containers +npm run docker:exec # Access container shell +npm run docker:health # Health check +npm run docker:restart # Restart app service + +# Cleanup and reset +npm run docker:clean # Stop and clean volumes + system prune +npm run docker:reset # Complete reset with no-cache rebuild +npm run docker:pull # Pull latest images + +# Testing +npm run test:docker # Run automation tests +``` + +## Basic Commands + +```bash +# Build and start containers +npm run docker:up:build +# or directly: +docker compose up --build + +# Start containers (without building) +npm run docker:up +# or directly: +docker compose up -d + +# Stop containers +npm run docker:down +# or directly: +docker compose down + +# View logs +npm run docker:logs +# or directly: +docker compose logs -f app + +# Execute commands in container +npm run docker:exec +# or directly: +docker compose exec app bash + +# Restart just the app service +npm run docker:restart +# or directly: +docker compose restart app +``` + +## Environment-Specific Workflows + +### Development Workflow +```bash +# 1. Start development environment (uses docker-compose.override.yml automatically) +npm run docker:dev + +# 2. View logs in real-time +npm run docker:logs + +# 3. Access container for debugging +npm run docker:exec + +# 4. Stop when done +npm run docker:down +``` + +### Production Workflow +```bash +# 1. Build production images +npm run docker:prod:build + +# 2. Start production stack +npm run docker:prod:up + +# 3. Monitor production logs +npm run docker:prod:logs + +# 4. Health check +npm run docker:health + +# 5. Stop production stack +npm run docker:prod:down +``` + +## Docker Compose v2 vs v1 + +This project uses Docker Compose v2 (`docker compose`) instead of v1 (`docker-compose`): + +- **v2**: `docker compose up` +- **v1**: `docker-compose up` (deprecated) + +Docker Compose v2 is integrated into Docker Desktop and provides better performance and features. + +# TradingView Automation in Docker + +This document explains how to use the automated TradingView analysis system in a Docker container environment. + +## Overview + +The automation system: +1. **Logs into TradingView** using your credentials +2. **Navigates to the specified chart** (e.g., SOL/USD) +3. **Sets the timeframe** (5min, 15min, etc.) +4. **Takes a screenshot** of the chart +5. **Analyzes it with AI** (OpenAI GPT-4 Vision) +6. **Returns trading recommendations** + +## Docker Setup + +### 1. Environment Variables + +Copy `.env.docker` to `.env.local` and configure: + +```bash +# Required +OPENAI_API_KEY=your_openai_api_key + +# Optional (can be passed via API) +TRADINGVIEW_EMAIL=your_email@example.com +TRADINGVIEW_PASSWORD=your_password +``` + +### 2. Start the Docker Container + +```bash +docker compose up --build +``` + +This will: +- Install all dependencies including Playwright +- Set up Chromium browser for automation +- Start the Next.js application on port 3000 + +## Usage + +### Method 1: API Endpoint + +#### Health Check +```bash +curl -X GET http://localhost:3000/api/trading/automated-analysis +``` + +#### Run Analysis +```bash +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" + } + }' +``` + +### Method 2: Web Interface + +1. Navigate to `http://localhost:3000` +2. Use the "Automated Trading Panel" component +3. Enter your TradingView credentials +4. Select symbol and timeframe +5. Click "Start Automated Analysis" + +### Method 3: Test Script + +Run the included test script: +```bash +./test-docker-automation.sh +``` + +## Supported Symbols + +- `SOLUSD` (Solana) +- `BTCUSD` (Bitcoin) +- `ETHUSD` (Ethereum) +- Any symbol available on TradingView + +## Supported Timeframes + +- `1` (1 minute) +- `5` (5 minutes) +- `15` (15 minutes) +- `30` (30 minutes) +- `60` (1 hour) + +## Response Format + +```json +{ + "success": true, + "data": { + "screenshots": ["SOLUSD_5_1234567890_ai.png"], + "analysis": { + "summary": "Market showing bearish sentiment...", + "marketSentiment": "BEARISH", + "recommendation": "SELL", + "confidence": 85, + "reasoning": "Price rejected from VWAP resistance...", + "entry": { + "price": 149.50, + "buffer": "±0.20", + "rationale": "Entry on rejection from VWAP" + }, + "stopLoss": { + "price": 151.00, + "rationale": "Above VWAP resistance" + }, + "takeProfits": { + "tp1": { "price": 148.00, "description": "Previous support" }, + "tp2": { "price": 146.50, "description": "Extended target" } + }, + "riskToReward": "1:2.5", + "confirmationTrigger": "Bearish engulfing candle on VWAP rejection", + "indicatorAnalysis": { + "rsi": "RSI at 68, approaching overbought", + "vwap": "Price rejected from VWAP resistance", + "obv": "OBV showing divergence" + } + }, + "symbol": "SOLUSD", + "timeframe": "5", + "timestamp": "2025-07-11T13:10:00.000Z" + } +} +``` + +## Docker Configuration Details + +### Browser Settings +- **Headless Mode**: Enabled for Docker +- **Chromium Path**: `/usr/bin/chromium` +- **No Sandbox**: Required for container security +- **Disabled GPU**: For stability in containers + +### Security Considerations +- Credentials are only used for the session +- Screenshots are stored locally in the container +- Browser data is not persisted between runs + +### Performance Notes +- Initial startup may take 30-60 seconds +- Each analysis takes approximately 45-90 seconds +- Screenshots are automatically cleaned up + +## Troubleshooting + +### Common Issues + +1. **Login Failed** + - Check TradingView credentials + - Verify captcha handling (may need manual intervention) + - Check if account has 2FA enabled + +2. **Screenshot Failed** + - Ensure chart loaded completely + - Check network connectivity + - Verify symbol exists on TradingView + +3. **AI Analysis Failed** + - Check OpenAI API key + - Verify screenshot was captured + - Check OpenAI quota/billing + +### Debug Mode + +To enable debug screenshots: +```bash +# Add to docker-compose.yml environment +- DEBUG_SCREENSHOTS=true +``` + +This will save additional debug screenshots during the process. + +### Logs + +View container logs: +```bash +docker compose logs -f app +``` + +## Integration Examples + +### With Trading Bot +```typescript +import { enhancedScreenshotService } from './lib/enhanced-screenshot' +import { aiAnalysisService } from './lib/ai-analysis' + +const analysis = await enhancedScreenshotService.captureWithLogin({ + symbol: 'SOLUSD', + timeframe: '5', + credentials: { email: 'user@example.com', password: 'password' } +}) + +if (analysis.length > 0) { + const aiResult = await aiAnalysisService.analyzeScreenshot(analysis[0]) + + if (aiResult?.recommendation === 'BUY') { + // Execute buy order + } +} +``` + +### Batch Analysis +```typescript +const configs = [ + { symbol: 'SOLUSD', timeframe: '5' }, + { symbol: 'BTCUSD', timeframe: '15' }, + { symbol: 'ETHUSD', timeframe: '5' } +] + +const results = await enhancedScreenshotService.captureBatch( + configs, + credentials +) +``` + +## Limitations + +- Requires TradingView account +- Rate limited by TradingView's anti-bot measures +- Captcha may require manual intervention +- Screenshot quality depends on chart layout +- AI analysis accuracy varies with market conditions + +## Support + +For issues or questions: +1. Check the logs first +2. Verify all environment variables are set +3. Test with the health check endpoint +4. Review the debug screenshots if available diff --git a/Dockerfile b/Dockerfile index fcc0e43..18d7a4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -# Dockerfile for Next.js 15 + Puppeteer/Chromium + Prisma + Tailwind + OpenAI +# Dockerfile for Next.js 15 + Playwright + Puppeteer/Chromium + Prisma + Tailwind + OpenAI FROM node:20-slim -# Install system dependencies for Chromium +# Install system dependencies for Chromium and Playwright RUN apt-get update && apt-get install -y \ wget \ ca-certificates \ @@ -21,6 +21,15 @@ RUN apt-get update && apt-get install -y \ libxdamage1 \ libxrandr2 \ xdg-utils \ + libxss1 \ + libgconf-2-4 \ + libxtst6 \ + libxrandr2 \ + libasound2 \ + libpangocairo-1.0-0 \ + libgdk-pixbuf2.0-0 \ + libgtk-3-0 \ + libxshmfence1 \ --no-install-recommends && \ rm -rf /var/lib/apt/lists/* @@ -37,9 +46,19 @@ WORKDIR /app COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* .npmrc* ./ RUN npm install +# Install Playwright browsers and dependencies +RUN npx playwright install --with-deps chromium +RUN npx playwright install-deps + # Copy the rest of the app COPY . . +# Generate Prisma client +RUN npx prisma generate + +# Fix permissions for node_modules binaries +RUN chmod +x node_modules/.bin/* + # Expose port EXPOSE 3000 diff --git a/VIDEO_RECORDING.md b/VIDEO_RECORDING.md new file mode 100644 index 0000000..735f348 --- /dev/null +++ b/VIDEO_RECORDING.md @@ -0,0 +1,82 @@ +# Video Recording Feature for TradingView Automation + +This feature allows you to record videos of the browser automation process for debugging and monitoring purposes. + +## Environment Variables + +- `TRADINGVIEW_DEBUG=true` - Enable debug mode (shows browser window locally, enables video recording) +- `TRADINGVIEW_RECORD_VIDEO=true` - Explicitly enable video recording +- `DOCKER_ENV=true` - Automatically detected in Docker containers + +## Usage Examples + +### Local Development with Live Browser Window +```bash +TRADINGVIEW_DEBUG=true node test-video-recording.js +``` +This will: +- Show the browser window in real-time +- Slow down actions (250ms delay) +- Open DevTools +- Take debug screenshots +- Record video if TRADINGVIEW_RECORD_VIDEO=true + +### Local Development with Video Recording +```bash +TRADINGVIEW_DEBUG=true TRADINGVIEW_RECORD_VIDEO=true node test-video-recording.js +``` +This will do everything above plus save MP4 videos to the `videos/` directory. + +### Docker Container Mode +```bash +DOCKER_ENV=true node test-video-recording.js +``` +This will: +- Run in headless mode (required for Docker) +- Use Docker-optimized browser arguments +- Take debug screenshots +- Record video if TRADINGVIEW_RECORD_VIDEO=true + +### Production Mode with Video Recording +```bash +TRADINGVIEW_RECORD_VIDEO=true node your-script.js +``` + +## File Locations + +- **Screenshots**: `screenshots/` directory +- **Videos**: `videos/` directory +- **Debug Screenshots**: Automatically named with timestamps and step descriptions + +## Video Settings + +- **FPS**: 10 (optimized for Docker) +- **Quality**: 50% (smaller file sizes) +- **Aspect Ratio**: 16:9 +- **Format**: MP4 + +## Docker Considerations + +When running in Docker containers: +1. The browser runs in headless mode only +2. Video recording works but files are saved inside the container +3. Mount volumes to access videos: `-v $(pwd)/videos:/app/videos` +4. Ensure sufficient memory for video encoding + +## Troubleshooting + +### Video Recording Not Working +1. Check if `puppeteer-screen-recorder` is installed +2. Ensure you have sufficient disk space +3. In Docker, check memory limits + +### Browser Not Showing Locally +1. Ensure `TRADINGVIEW_DEBUG=true` is set +2. Check if you're running in Docker mode +3. Verify X11 forwarding if using SSH + +### Performance Issues +1. Reduce video quality in the code +2. Lower FPS settings +3. Increase Docker memory limits +4. Use SSD storage for video files diff --git a/app/api/analyze/route.ts b/app/api/analyze/route.ts index b8f1dd9..bfd66a2 100644 --- a/app/api/analyze/route.ts +++ b/app/api/analyze/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { aiAnalysisService } from '../../../lib/ai-analysis' -import { tradingViewCapture } from '../../../lib/tradingview' +import { enhancedScreenshotService } from '../../../lib/enhanced-screenshot' import { settingsManager } from '../../../lib/settings' import path from 'path' @@ -21,7 +21,7 @@ export async function POST(req: NextRequest) { } const baseFilename = `${finalSymbol}_${finalTimeframe}_${Date.now()}` - const screenshots = await tradingViewCapture.capture(finalSymbol, `${baseFilename}.png`, finalLayouts, finalTimeframe) + const screenshots = await enhancedScreenshotService.capture(finalSymbol, `${baseFilename}.png`, finalLayouts, finalTimeframe) let result if (screenshots.length === 1) { @@ -30,7 +30,7 @@ export async function POST(req: NextRequest) { result = await aiAnalysisService.analyzeScreenshot(filename) } else { // Multiple screenshots analysis - const filenames = screenshots.map(screenshot => path.basename(screenshot)) + const filenames = screenshots.map((screenshot: string) => path.basename(screenshot)) result = await aiAnalysisService.analyzeMultipleScreenshots(filenames) } @@ -46,7 +46,7 @@ export async function POST(req: NextRequest) { timeframe: finalTimeframe, layouts: finalLayouts }, - screenshots: screenshots.map(s => path.basename(s)) + screenshots: screenshots.map((s: string) => path.basename(s)) }) } catch (e: any) { return NextResponse.json({ error: e.message }, { status: 500 }) diff --git a/app/api/automated-analysis/route.ts b/app/api/automated-analysis/route.ts new file mode 100644 index 0000000..116c1f2 --- /dev/null +++ b/app/api/automated-analysis/route.ts @@ -0,0 +1,131 @@ +import { NextRequest, NextResponse } from 'next/server' +import { aiAnalysisService } from '../../../lib/ai-analysis' +import { TradingViewCredentials } from '../../../lib/tradingview-automation' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { symbol, timeframe, credentials, action } = body + + // Validate input + if (!symbol || !timeframe || !credentials?.email || !credentials?.password) { + return NextResponse.json({ + success: false, + error: 'Missing required fields: symbol, timeframe, and credentials' + }, { status: 400 }) + } + + const tradingViewCredentials: TradingViewCredentials = { + email: credentials.email, + password: credentials.password + } + + switch (action) { + case 'capture_and_analyze': + // Single symbol and timeframe + const analysis = await aiAnalysisService.captureAndAnalyze( + symbol, + timeframe, + tradingViewCredentials + ) + + if (!analysis) { + return NextResponse.json({ + success: false, + error: 'Failed to capture screenshot or analyze chart' + }, { status: 500 }) + } + + return NextResponse.json({ + success: true, + data: { + symbol, + timeframe, + analysis, + timestamp: new Date().toISOString() + } + }) + + case 'capture_multiple': + // Multiple symbols or timeframes + const { symbols = [symbol], timeframes = [timeframe] } = body + + const results = await aiAnalysisService.captureAndAnalyzeMultiple( + symbols, + timeframes, + tradingViewCredentials + ) + + return NextResponse.json({ + success: true, + data: { + results, + timestamp: new Date().toISOString() + } + }) + + case 'capture_with_config': + // Advanced configuration + const { layouts } = body + + const configResult = await aiAnalysisService.captureAndAnalyzeWithConfig({ + symbol, + timeframe, + layouts, + credentials: tradingViewCredentials + }) + + return NextResponse.json({ + success: true, + data: { + symbol, + timeframe, + screenshots: configResult.screenshots, + analysis: configResult.analysis, + timestamp: new Date().toISOString() + } + }) + + default: + return NextResponse.json({ + success: false, + error: 'Invalid action. Use: capture_and_analyze, capture_multiple, or capture_with_config' + }, { status: 400 }) + } + + } catch (error) { + console.error('Automated analysis API error:', error) + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred' + }, { status: 500 }) + } +} + +export async function GET() { + return NextResponse.json({ + success: true, + message: 'TradingView Automated Analysis API', + endpoints: { + POST: { + description: 'Automated screenshot capture and AI analysis', + actions: [ + 'capture_and_analyze - Single symbol/timeframe analysis', + 'capture_multiple - Multiple symbols/timeframes', + 'capture_with_config - Advanced configuration with layouts' + ], + required_fields: ['symbol', 'timeframe', 'credentials', 'action'], + example: { + symbol: 'SOLUSD', + timeframe: '5', + credentials: { + email: 'your_email@example.com', + password: 'your_password' + }, + action: 'capture_and_analyze' + } + } + } + }) +} diff --git a/app/api/screenshot/route.ts b/app/api/screenshot/route.ts index a91facf..e935cac 100644 --- a/app/api/screenshot/route.ts +++ b/app/api/screenshot/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { tradingViewCapture } from '../../../lib/tradingview' +import { enhancedScreenshotService } from '../../../lib/enhanced-screenshot' export async function POST(req: NextRequest) { try { @@ -7,7 +7,8 @@ export async function POST(req: NextRequest) { if (!symbol || !filename) { return NextResponse.json({ error: 'Missing symbol or filename' }, { status: 400 }) } - const filePath = await tradingViewCapture.capture(symbol, filename) + const screenshots = await enhancedScreenshotService.capture(symbol, filename) + const filePath = screenshots.length > 0 ? screenshots[0] : null return NextResponse.json({ filePath }) } catch (e: any) { return NextResponse.json({ error: e.message }, { status: 500 }) diff --git a/app/api/trading/automated-analysis/route.ts b/app/api/trading/automated-analysis/route.ts new file mode 100644 index 0000000..2f1dabc --- /dev/null +++ b/app/api/trading/automated-analysis/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server' +import { enhancedScreenshotService } from '../../../../lib/enhanced-screenshot' +import { aiAnalysisService } from '../../../../lib/ai-analysis' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { symbol, timeframe, credentials } = body + + // Validate required fields (credentials optional if using .env) + if (!symbol || !timeframe) { + return NextResponse.json( + { error: 'Missing required fields: symbol, timeframe' }, + { status: 400 } + ) + } + + console.log(`Starting automated analysis for ${symbol} ${timeframe}`) + + // Take screenshot with automated login and navigation + const screenshots = await enhancedScreenshotService.captureWithLogin({ + symbol, + timeframe, + credentials // Will use .env if not provided + }) + + if (screenshots.length === 0) { + throw new Error('Failed to capture screenshots') + } + + // Analyze the first screenshot + const analysis = await aiAnalysisService.analyzeScreenshot(screenshots[0]) + + if (!analysis) { + throw new Error('Failed to analyze screenshot') + } + + return NextResponse.json({ + success: true, + data: { + screenshots, + analysis, + symbol, + timeframe, + timestamp: new Date().toISOString() + } + }) + + } catch (error: any) { + console.error('Automated analysis error:', error) + + return NextResponse.json( + { + error: 'Failed to perform automated analysis', + details: error?.message || 'Unknown error' + }, + { status: 500 } + ) + } +} + +export async function GET() { + try { + // Health check for the automation system + const healthCheck = await enhancedScreenshotService.healthCheck() + + return NextResponse.json({ + status: healthCheck.status, + message: healthCheck.message, + timestamp: new Date().toISOString(), + dockerEnvironment: true + }) + + } catch (error: any) { + return NextResponse.json( + { + status: 'error', + message: `Health check failed: ${error?.message || 'Unknown error'}`, + dockerEnvironment: true + }, + { status: 500 } + ) + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 375ce91..c850dc0 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,8 +8,8 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - + +
{children}
diff --git a/components/AIAnalysisPanel.tsx b/components/AIAnalysisPanel.tsx index 5ef7929..e7c6ac1 100644 --- a/components/AIAnalysisPanel.tsx +++ b/components/AIAnalysisPanel.tsx @@ -13,6 +13,17 @@ const timeframes = [ { label: '1M', value: 'M' }, ] +const popularCoins = [ + { name: 'Bitcoin', symbol: 'BTCUSD', icon: '₿' }, + { name: 'Ethereum', symbol: 'ETHUSD', icon: 'Ξ' }, + { name: 'Solana', symbol: 'SOLUSD', icon: '◎' }, + { name: 'Sui', symbol: 'SUIUSD', icon: '🔷' }, + { name: 'Avalanche', symbol: 'AVAXUSD', icon: '🔺' }, + { name: 'Cardano', symbol: 'ADAUSD', icon: '♠' }, + { name: 'Polygon', symbol: 'MATICUSD', icon: '🔷' }, + { name: 'Chainlink', symbol: 'LINKUSD', icon: '🔗' }, +] + export default function AIAnalysisPanel() { const [symbol, setSymbol] = useState('BTCUSD') const [selectedLayouts, setSelectedLayouts] = useState([layouts[0]]) @@ -21,6 +32,17 @@ export default function AIAnalysisPanel() { const [result, setResult] = useState(null) const [error, setError] = useState(null) + // Helper function to safely render any value + const safeRender = (value: any): string => { + if (typeof value === 'string') return value + if (typeof value === 'number') return value.toString() + if (Array.isArray(value)) return value.join(', ') + if (typeof value === 'object' && value !== null) { + return JSON.stringify(value) + } + return String(value) + } + const toggleLayout = (layout: string) => { setSelectedLayouts(prev => prev.includes(layout) @@ -29,6 +51,32 @@ export default function AIAnalysisPanel() { ) } + const selectCoin = (coinSymbol: string) => { + setSymbol(coinSymbol) + } + + const quickAnalyze = async (coinSymbol: string) => { + setSymbol(coinSymbol) + setSelectedLayouts([layouts[0]]) // Use first layout + setLoading(true) + setError(null) + setResult(null) + + try { + const res = await fetch('/api/analyze', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ symbol: coinSymbol, layouts: [layouts[0]], timeframe }) + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error || 'Unknown error') + setResult(data) + } catch (e: any) { + setError(e.message) + } + setLoading(false) + } + async function handleAnalyze() { setLoading(true) setError(null) @@ -58,23 +106,55 @@ export default function AIAnalysisPanel() { return (

AI Chart Analysis

-
- setSymbol(e.target.value)} - placeholder="Symbol (e.g. BTCUSD)" - /> - - + + {/* Quick Coin Selection */} +
+

Quick Analysis - Popular Coins

+
+ {popularCoins.map(coin => ( + + ))} +
+
+ + {/* Manual Input Section */} +
+

Manual Analysis

+
+ setSymbol(e.target.value)} + placeholder="Symbol (e.g. BTCUSD)" + /> + + +
{/* Layout selection */} @@ -84,60 +164,188 @@ export default function AIAnalysisPanel() {
{layouts.map(layout => ( -
{selectedLayouts.length > 0 && ( -
+
Selected: {selectedLayouts.join(', ')}
)}
{error && ( -
- {error.includes('frame was detached') ? ( - <> - TradingView chart could not be loaded. Please check your symbol and layout, or try again.
- (Technical: {error}) - - ) : error.includes('layout not found') ? ( - <> - TradingView layout not found. Please select a valid layout.
- (Technical: {error}) - - ) : ( - error - )} +
+
+ {error.includes('frame was detached') ? ( + <> + TradingView Error: Chart could not be loaded. Please check your symbol and layout, or try again.
+ Technical: {error} + + ) : error.includes('layout not found') ? ( + <> + Layout Error: TradingView layout not found. Please select a valid layout.
+ Technical: {error} + + ) : error.includes('Private layout access denied') ? ( + <> + Access Error: The selected layout is private or requires authentication. Try a different layout or check your TradingView login.
+ Technical: {error} + + ) : ( + <> + Analysis Error: {error} + + )} +
)} {loading && ( -
- - Analyzing chart... +
+
+ + + + + Analyzing {symbol} chart... +
)} {result && ( -
+
+

Analysis Results

{result.layoutsAnalyzed && ( -
- Layouts analyzed: {result.layoutsAnalyzed.join(', ')} +
+ Layouts analyzed: + {result.layoutsAnalyzed.join(', ')}
)} -
-
Summary: {result.summary}
-
Sentiment: {result.marketSentiment}
-
Recommendation: {result.recommendation} ({result.confidence}%)
-
Support: {result.keyLevels?.support?.join(', ')}
-
Resistance: {result.keyLevels?.resistance?.join(', ')}
-
Reasoning: {result.reasoning}
+
+
+ Summary: +

{safeRender(result.summary)}

+
+
+
+ Sentiment: +

{safeRender(result.marketSentiment)}

+
+
+ Recommendation: +

{safeRender(result.recommendation)}

+ ({safeRender(result.confidence)}% confidence) +
+
+ {result.keyLevels && ( +
+
+ Support Levels: +

{result.keyLevels.support?.join(', ') || 'None identified'}

+
+
+ Resistance Levels: +

{result.keyLevels.resistance?.join(', ') || 'None identified'}

+
+
+ )} + + {/* Enhanced Trading Analysis */} + {result.entry && ( +
+ Entry: +

${safeRender(result.entry.price || result.entry)}

+ {result.entry.buffer &&

{safeRender(result.entry.buffer)}

} + {result.entry.rationale &&

{safeRender(result.entry.rationale)}

} +
+ )} + + {result.stopLoss && ( +
+ Stop Loss: +

${safeRender(result.stopLoss.price || result.stopLoss)}

+ {result.stopLoss.rationale &&

{safeRender(result.stopLoss.rationale)}

} +
+ )} + + {result.takeProfits && ( +
+ Take Profits: + {typeof result.takeProfits === 'object' ? ( + <> + {result.takeProfits.tp1 && ( +
+

TP1: ${safeRender(result.takeProfits.tp1.price || result.takeProfits.tp1)}

+ {result.takeProfits.tp1.description &&

{safeRender(result.takeProfits.tp1.description)}

} +
+ )} + {result.takeProfits.tp2 && ( +
+

TP2: ${safeRender(result.takeProfits.tp2.price || result.takeProfits.tp2)}

+ {result.takeProfits.tp2.description &&

{safeRender(result.takeProfits.tp2.description)}

} +
+ )} + + ) : ( +

{safeRender(result.takeProfits)}

+ )} +
+ )} + + {result.riskToReward && ( +
+ Risk to Reward: +

{safeRender(result.riskToReward)}

+
+ )} + + {result.confirmationTrigger && ( +
+ Confirmation Trigger: +

{safeRender(result.confirmationTrigger)}

+
+ )} + + {result.indicatorAnalysis && ( +
+ Indicator Analysis: + {typeof result.indicatorAnalysis === 'object' ? ( +
+ {result.indicatorAnalysis.rsi && ( +
+ RSI: + {safeRender(result.indicatorAnalysis.rsi)} +
+ )} + {result.indicatorAnalysis.vwap && ( +
+ VWAP: + {safeRender(result.indicatorAnalysis.vwap)} +
+ )} + {result.indicatorAnalysis.obv && ( +
+ OBV: + {safeRender(result.indicatorAnalysis.obv)} +
+ )} +
+ ) : ( +

{safeRender(result.indicatorAnalysis)}

+ )} +
+ )} + +
+ Reasoning: +

{safeRender(result.reasoning)}

+
)} diff --git a/components/AutomatedAnalysisPanel.tsx b/components/AutomatedAnalysisPanel.tsx new file mode 100644 index 0000000..59d2afa --- /dev/null +++ b/components/AutomatedAnalysisPanel.tsx @@ -0,0 +1,303 @@ +'use client' + +import { useState } from 'react' +import { AnalysisResult } from '../lib/ai-analysis' + +interface AutomatedAnalysisProps { + onAnalysisComplete?: (analysis: AnalysisResult) => void +} + +export default function AutomatedAnalysisPanel({ onAnalysisComplete }: AutomatedAnalysisProps) { + const [isAnalyzing, setIsAnalyzing] = useState(false) + const [symbol, setSymbol] = useState('SOLUSD') + const [timeframe, setTimeframe] = useState('5') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [analysis, setAnalysis] = useState(null) + const [error, setError] = useState('') + + const handleAnalyze = async () => { + if (!email || !password) { + setError('Please provide TradingView email and password') + return + } + + setIsAnalyzing(true) + setError('') + setAnalysis(null) + + try { + const response = await fetch('/api/automated-analysis', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + symbol, + timeframe, + credentials: { + email, + password, + }, + action: 'capture_and_analyze' + }) + }) + + const result = await response.json() + + if (!result.success) { + throw new Error(result.error || 'Analysis failed') + } + + const analysisResult = result.data.analysis + setAnalysis(analysisResult) + + if (onAnalysisComplete) { + onAnalysisComplete(analysisResult) + } + + } catch (err) { + console.error('Analysis error:', err) + setError(err instanceof Error ? err.message : 'Analysis failed') + } finally { + setIsAnalyzing(false) + } + } + + const handleMultipleAnalysis = async () => { + if (!email || !password) { + setError('Please provide TradingView email and password') + return + } + + setIsAnalyzing(true) + setError('') + setAnalysis(null) + + try { + const response = await fetch('/api/automated-analysis', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + symbols: [symbol], + timeframes: ['5', '15', '60'], + credentials: { + email, + password, + }, + action: 'capture_multiple' + }) + }) + + const result = await response.json() + + if (!result.success) { + throw new Error(result.error || 'Multiple analysis failed') + } + + // Show results from all timeframes + console.log('Multiple analysis results:', result.data.results) + + // Use the first successful analysis for display + const firstSuccessful = result.data.results.find((r: any) => r.analysis !== null) + if (firstSuccessful) { + setAnalysis(firstSuccessful.analysis) + if (onAnalysisComplete) { + onAnalysisComplete(firstSuccessful.analysis) + } + } + + } catch (err) { + console.error('Multiple analysis error:', err) + setError(err instanceof Error ? err.message : 'Multiple analysis failed') + } finally { + setIsAnalyzing(false) + } + } + + return ( +
+

Automated TradingView Analysis

+ + {/* Configuration */} +
+
+ + +
+ +
+ + +
+ +
+ + setEmail(e.target.value)} + className="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-md text-white" + placeholder="your.email@example.com" + disabled={isAnalyzing} + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-md text-white" + placeholder="••••••••" + disabled={isAnalyzing} + /> +
+
+ + {/* Action Buttons */} +
+ + + +
+ + {/* Status */} + {isAnalyzing && ( +
+
+
+ + Logging into TradingView and capturing chart... + +
+
+ )} + + {/* Error Display */} + {error && ( +
+

{error}

+
+ )} + + {/* Analysis Results */} + {analysis && ( +
+

Analysis Results

+ +
+
+ Sentiment: +

+ {analysis.marketSentiment} +

+
+ +
+ Recommendation: +

+ {analysis.recommendation} +

+
+ +
+ Confidence: +

{analysis.confidence}%

+
+
+ +
+ Summary: +

{analysis.summary}

+
+ + {analysis.entry && ( +
+ Entry: +

${analysis.entry.price}

+

{analysis.entry.rationale}

+
+ )} + + {analysis.stopLoss && ( +
+ Stop Loss: +

${analysis.stopLoss.price}

+

{analysis.stopLoss.rationale}

+
+ )} + + {analysis.takeProfits && ( +
+ Take Profits: + {analysis.takeProfits.tp1 && ( +

TP1: ${analysis.takeProfits.tp1.price} - {analysis.takeProfits.tp1.description}

+ )} + {analysis.takeProfits.tp2 && ( +

TP2: ${analysis.takeProfits.tp2.price} - {analysis.takeProfits.tp2.description}

+ )} +
+ )} + +
+ Reasoning: +

{analysis.reasoning}

+
+
+ )} +
+ ) +} diff --git a/components/AutomatedTradingPanel.tsx b/components/AutomatedTradingPanel.tsx new file mode 100644 index 0000000..6cde813 --- /dev/null +++ b/components/AutomatedTradingPanel.tsx @@ -0,0 +1,249 @@ +import React, { useState } from 'react' + +interface TradingViewCredentials { + email: string + password: string +} + +interface AutomatedAnalysisResult { + screenshots: string[] + analysis: any + symbol: string + timeframe: string + timestamp: string +} + +export function AutomatedTradingPanel() { + const [credentials, setCredentials] = useState({ + email: '', + password: '' + }) + const [symbol, setSymbol] = useState('SOLUSD') + const [timeframe, setTimeframe] = useState('5') + const [isLoading, setIsLoading] = useState(false) + const [result, setResult] = useState(null) + const [error, setError] = useState('') + const [healthStatus, setHealthStatus] = useState<'unknown' | 'ok' | 'error'>('unknown') + + const checkHealth = async () => { + try { + const response = await fetch('/api/trading/automated-analysis') + const data = await response.json() + + if (data.status === 'ok') { + setHealthStatus('ok') + } else { + setHealthStatus('error') + setError(data.message || 'Health check failed') + } + } catch (err: any) { + setHealthStatus('error') + setError(err.message || 'Failed to check health') + } + } + + const runAutomatedAnalysis = async () => { + if (!credentials.email || !credentials.password) { + setError('Please enter your TradingView credentials') + return + } + + setIsLoading(true) + setError('') + setResult(null) + + try { + const response = await fetch('/api/trading/automated-analysis', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + symbol, + timeframe, + credentials + }) + }) + + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || data.details || 'Analysis failed') + } + + setResult(data.data) + } catch (err: any) { + setError(err.message || 'Failed to run automated analysis') + } finally { + setIsLoading(false) + } + } + + return ( +
+

Automated TradingView Analysis (Docker)

+ + {/* Health Check */} +
+
+

System Health

+ +
+ + {healthStatus !== 'unknown' && ( +
+ Status: {healthStatus === 'ok' ? '✅ System Ready' : '❌ System Error'} +
+ )} +
+ + {/* Configuration */} +
+
+
+ + setSymbol(e.target.value)} + className="w-full p-2 border rounded" + placeholder="e.g., SOLUSD, BTCUSD" + /> +
+
+ + +
+
+ +
+
+ + setCredentials(prev => ({ ...prev, email: e.target.value }))} + className="w-full p-2 border rounded" + placeholder="your-email@example.com" + /> +
+
+ + setCredentials(prev => ({ ...prev, password: e.target.value }))} + className="w-full p-2 border rounded" + placeholder="your-password" + /> +
+
+
+ + {/* Action Button */} + + + {/* Error Display */} + {error && ( +
+ Error: {error} +
+ )} + + {/* Results */} + {result && ( +
+

Analysis Results

+ + {/* Screenshots */} +
+

Screenshots Captured:

+
+ {result.screenshots.map((screenshot, index) => ( +
+ 📸 {screenshot} +
+ ))} +
+
+ + {/* Analysis Summary */} +
+

AI Analysis Summary

+
+
Symbol: {result.symbol}
+
Timeframe: {result.timeframe}
+
Sentiment: + + {result.analysis.marketSentiment} + +
+
Recommendation: + + {result.analysis.recommendation} + +
+
Confidence: {result.analysis.confidence}%
+
Summary: {result.analysis.summary}
+
Reasoning: {result.analysis.reasoning}
+
+
+ + {/* Trading Details */} + {result.analysis.entry && ( +
+

Trading Setup

+
+
Entry: ${result.analysis.entry.price}
+ {result.analysis.stopLoss && ( +
Stop Loss: ${result.analysis.stopLoss.price}
+ )} + {result.analysis.takeProfits?.tp1 && ( +
Take Profit 1: ${result.analysis.takeProfits.tp1.price}
+ )} + {result.analysis.riskToReward && ( +
Risk/Reward: {result.analysis.riskToReward}
+ )} +
+
+ )} +
+ )} +
+ ) +} + +export default AutomatedTradingPanel diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx index f66ce69..92f0865 100644 --- a/components/Dashboard.tsx +++ b/components/Dashboard.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react' import AutoTradingPanel from './AutoTradingPanel' import TradingHistory from './TradingHistory' import DeveloperSettings from './DeveloperSettings' +import AIAnalysisPanel from './AIAnalysisPanel' export default function Dashboard() { const [positions, setPositions] = useState([]) @@ -28,8 +29,9 @@ export default function Dashboard() { }, []) return ( -
+
+
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: 1 / ${this.recorder.screenshotCount} +
+
+

Total frames: ${this.recorder.screenshotCount} | Captured every 2 seconds

+
+ Video frame + + + +` + + 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