Spaces:
Running
Running
| /** | |
| * main.js | |
| * Main entry point of the Trade Analyzer application. | |
| * Manages states, coordinates UI components, coordinates WebSocket streams, | |
| * and executes analysis runs. | |
| */ | |
| import DataManager from './data/dataManager.js'; | |
| import ChartManager from './chart/chartManager.js'; | |
| import Header from './components/header.js'; | |
| import SignalPanel from './components/signalPanel.js'; | |
| import AnalysisPanel from './components/analysisPanel.js'; | |
| import TradeManager from './components/tradeManager.js'; | |
| // Import technical analysis tools | |
| import { detectOrderBlocks, updateMitigation } from './analysis/orderBlocks.js'; | |
| import { detectZones, updateZoneStatus } from './analysis/supplyDemand.js'; | |
| import { detectStructure } from './analysis/marketStructure.js'; | |
| import { analyzeTrend } from './analysis/trendDetector.js'; | |
| import { rsi, ema, atr, macd, bollingerBands } from './analysis/indicators.js'; | |
| import { generateSignals, LIVE_STRATEGY_OPTS } from './analysis/signalGenerator.js'; | |
| import { analyzeExit } from './analysis/exitManager.js'; | |
| // --- Application States --- | |
| let currentSymbol = 'BTCUSDT'; | |
| let currentTimeframe = '15m'; | |
| /** @type {Array<{time:number, open:number, high:number, low:number, close:number, volume:number}>} */ | |
| let history = []; | |
| let lastTickPrice = null; | |
| const livePrices = { | |
| BTCUSDT: 0, | |
| XAUUSD: 0, | |
| GBPUSD: 0, | |
| USDCAD: 1.38, // approximate fallback | |
| }; | |
| // --- Instantiate Core Classes --- | |
| const dataManager = new DataManager(); | |
| const chartManager = new ChartManager('chart-container'); | |
| // Header | |
| const header = new Header({ | |
| onSymbolChange: (symbol) => { | |
| if (symbol !== currentSymbol) { | |
| initSymbolTimeframe(symbol, currentTimeframe); | |
| } | |
| }, | |
| onTimeframeChange: (tf) => { | |
| if (tf !== currentTimeframe) { | |
| initSymbolTimeframe(currentSymbol, tf); | |
| } | |
| }, | |
| }); | |
| // Trade Manager | |
| const tradeManager = new TradeManager({ | |
| onDailyLossUpdate: (count) => { | |
| header.updateDailyLosses(count); | |
| // Halt checking disabled - trade anytime setup meets | |
| }, | |
| }); | |
| // Signal Panel | |
| const signalPanel = new SignalPanel({ | |
| onTakeTrade: (signal) => { | |
| if (tradeManager.shouldStopTrading()) { | |
| alert('Trading is halted today. You have reached 3 daily losses.'); | |
| return; | |
| } | |
| const trade = tradeManager.takeTrade(signal); | |
| console.log('[Main] Trade taken:', trade); | |
| // Draw the active trade levels on the chart | |
| chartManager.drawSignal(signal); | |
| }, | |
| }); | |
| // Analysis Panel | |
| const analysisPanel = new AnalysisPanel(); | |
| // Set initial daily losses state in header | |
| const initialLosses = tradeManager.getDailyLosses(); | |
| header.updateDailyLosses(initialLosses); | |
| // Set initial account summary in header | |
| const initSummary = tradeManager.getAccountSummary(); | |
| header.updateAccountSummary(initSummary.balance, initSummary.equity); | |
| // Ensure daily stop overlay remains hidden | |
| const overlay = document.getElementById('daily-stop-overlay'); | |
| if (overlay) { | |
| overlay.classList.add('hidden'); | |
| } | |
| // Acknowledge stop button listener | |
| const ackBtn = document.getElementById('acknowledge-stop'); | |
| if (ackBtn) { | |
| ackBtn.addEventListener('click', () => { | |
| const stopOverlay = document.getElementById('daily-stop-overlay'); | |
| if (stopOverlay) stopOverlay.classList.add('hidden'); | |
| }); | |
| } | |
| // Reset Account button listener | |
| const resetAccountBtn = document.getElementById('btn-reset-account'); | |
| if (resetAccountBtn) { | |
| resetAccountBtn.addEventListener('click', async () => { | |
| if (confirm('Are you sure you want to reset your account? This will clear all trade history, active trades, and restore your balance to $5,000.')) { | |
| resetAccountBtn.disabled = true; | |
| const originalText = resetAccountBtn.innerHTML; | |
| resetAccountBtn.innerHTML = '🔄 Resetting...'; | |
| try { | |
| await tradeManager.resetAccount(); | |
| console.log('[Main] Account reset successful.'); | |
| } catch (err) { | |
| console.error('[Main] Account reset failed:', err); | |
| alert('Failed to reset account. Please try again.'); | |
| } finally { | |
| resetAccountBtn.disabled = false; | |
| resetAccountBtn.innerHTML = originalText; | |
| } | |
| } | |
| }); | |
| } | |
| // --- Subscription / Initialization Flow --- | |
| /** | |
| * Switch the active symbol and timeframe, load history, and subscribe to feeds. | |
| * | |
| * @param {string} symbol | |
| * @param {string} timeframe | |
| */ | |
| async function initSymbolTimeframe(symbol, timeframe) { | |
| try { | |
| currentSymbol = symbol; | |
| currentTimeframe = timeframe; | |
| lastTickPrice = null; | |
| console.log(`[Main] Initializing ${symbol} on ${timeframe}`); | |
| // Unsubscribe existing WebSockets | |
| dataManager.unsubscribeAll(); | |
| // Clear chart overlays | |
| chartManager.clearAll(); | |
| // Show connecting status | |
| header.setConnectionStatus('connecting'); | |
| // Fetch historical candles | |
| const candles = await dataManager.loadHistory(symbol, timeframe); | |
| if (!candles || candles.length === 0) { | |
| throw new Error('No historical data returned'); | |
| } | |
| history = candles; | |
| console.log(`[Main] Loaded ${history.length} historical candles.`); | |
| // Live-only engine: trades are opened/closed server-side on live ticks only. | |
| // Set chart base data | |
| chartManager.setData(history, symbol); | |
| // Sync header open price for percentage change | |
| if (history.length > 0) { | |
| const lastCandle = history[history.length - 1]; | |
| // Find the candle closest to exactly 24 hours ago (86400 seconds ago) for a true 24h change | |
| const targetTime = Math.floor(Date.now() / 1000) - 86400; | |
| let openCandle = history[0]; | |
| let minDiff = Math.abs(openCandle.time - targetTime); | |
| for (const c of history) { | |
| const diff = Math.abs(c.time - targetTime); | |
| if (diff < minDiff) { | |
| minDiff = diff; | |
| openCandle = c; | |
| } | |
| } | |
| header.setOpenPrice(openCandle.close); | |
| header.updatePrice(lastCandle.close, null); | |
| lastTickPrice = lastCandle.close; | |
| livePrices[symbol] = lastCandle.close; | |
| // Immediately calculate active trades' P&L and R:R with the loaded price | |
| tradeManager.updatePrices(lastCandle.close, symbol); | |
| analysisPanel.setLiveRates(livePrices); | |
| } | |
| // Run structural analytics | |
| runAnalysis(); | |
| // Update connection status | |
| header.setConnectionStatus('connected'); | |
| // Subscribe to live feed | |
| dataManager.subscribeRealtime(symbol, timeframe, { | |
| onTick: (price) => { | |
| if (symbol !== currentSymbol) return; // Stale stream protection | |
| handleTick(price); | |
| }, | |
| onCandleUpdate: (candle) => { | |
| if (symbol !== currentSymbol) return; // Stale stream protection | |
| handleCandleUpdate(candle); | |
| }, | |
| }); | |
| } catch (error) { | |
| console.error(`[Main] Initialization failed for ${symbol} (${timeframe}):`, error); | |
| header.setConnectionStatus('disconnected'); | |
| } | |
| } | |
| /** | |
| * Handle real-time price updates. | |
| * | |
| * @param {number} price | |
| */ | |
| function handleTick(price) { | |
| if (price === lastTickPrice) return; | |
| // Update header ticker | |
| header.updatePrice(price, lastTickPrice); | |
| // Feed to trade manager to update active positions | |
| tradeManager.updatePrices(price, currentSymbol); | |
| // If we have active positions, update exit suggestions in real-time | |
| if (tradeManager.activeTrades.length > 0) { | |
| for (const trade of tradeManager.activeTrades) { | |
| if (trade.symbol === currentSymbol) { | |
| // Clone history and replace latest close with live price | |
| const candlesCopy = [...history]; | |
| if (candlesCopy.length > 0) { | |
| const idx = candlesCopy.length - 1; | |
| candlesCopy[idx] = { | |
| ...candlesCopy[idx], | |
| close: price, | |
| high: Math.max(candlesCopy[idx].high, price), | |
| low: Math.min(candlesCopy[idx].low, price), | |
| }; | |
| } | |
| const exitAnalysis = analyzeExit(candlesCopy, trade); | |
| tradeManager.updateExitAnalysis(trade.id, exitAnalysis); | |
| } | |
| } | |
| } | |
| // Update live price caches | |
| livePrices[currentSymbol] = price; | |
| analysisPanel.setLiveRates(livePrices); | |
| // Update account summary display in header in real-time! | |
| const summary = tradeManager.getAccountSummary(); | |
| header.updateAccountSummary(summary.balance, summary.equity); | |
| lastTickPrice = price; | |
| } | |
| /** | |
| * Handle streaming kline updates. | |
| * | |
| * @param {object} candle | |
| */ | |
| function handleCandleUpdate(candle) { | |
| const lastIndex = history.findIndex(c => c.time === candle.time); | |
| let isNewCandle = false; | |
| if (lastIndex !== -1) { | |
| // Update current in-progress candle | |
| history[lastIndex] = { | |
| time: candle.time, | |
| open: candle.open, | |
| high: candle.high, | |
| low: candle.low, | |
| close: candle.close, | |
| volume: candle.volume, | |
| }; | |
| } else { | |
| // Transition detected! A new candle has started. | |
| // The previous candle in history is now closed and finalized. | |
| history.push({ | |
| time: candle.time, | |
| open: candle.open, | |
| high: candle.high, | |
| low: candle.low, | |
| close: candle.close, | |
| volume: candle.volume, | |
| }); | |
| if (history.length > 1000) { | |
| history.shift(); | |
| } | |
| isNewCandle = true; | |
| } | |
| // Rerender the chart candle | |
| chartManager.updateCandle(candle); | |
| // Recalculate setups if the candle has officially closed, or if a new candle started (transition) | |
| if (candle.isClosed || isNewCandle) { | |
| console.log(`[Main] Candle closed at ${new Date(candle.time * 1000).toLocaleTimeString()}. Recalculating setups.`); | |
| runAnalysis(); | |
| } | |
| } | |
| /** | |
| * Run technical indicator formulas and smart money scanning algorithms. | |
| */ | |
| function runAnalysis() { | |
| if (history.length < 50) { | |
| console.warn('[Main] Insufficient historical data to run scanners.'); | |
| return; | |
| } | |
| const closes = history.map(c => c.close); | |
| // 1. Indicators | |
| const rsiValues = rsi(closes); | |
| const ema9 = ema(closes, 9); | |
| const ema21 = ema(closes, 21); | |
| const ema50 = ema(closes, 50); | |
| const ema200 = ema(closes, 200); | |
| const atrValues = atr(history); | |
| const macdData = macd(closes); | |
| const bb = bollingerBands(closes); | |
| const indicators = { | |
| rsi: rsiValues, | |
| emas: { 9: ema9, 21: ema21, 50: ema50, 200: ema200 }, | |
| atr: atrValues, | |
| macd: macdData, | |
| bollingerBands: bb, | |
| }; | |
| // 2. Structural Analysis | |
| const structure = detectStructure(history); | |
| // 3. Smart Money Concepts (Order Blocks) | |
| let orderBlocks = detectOrderBlocks(history); | |
| orderBlocks = updateMitigation(orderBlocks, history); | |
| // 4. Supply & Demand Zones | |
| let zones = detectZones(history); | |
| zones = updateZoneStatus(zones, history); | |
| // 5. Overall Trend | |
| const trend = analyzeTrend(history); | |
| // 6. Signal Scanner | |
| const dailyLossCount = tradeManager.getDailyLosses(); | |
| let signals = generateSignals(history, currentSymbol, dailyLossCount, LIVE_STRATEGY_OPTS); | |
| // Expose/Suggest Grade A and B setups (Score >= 4) | |
| signals = signals.filter(sig => ['A', 'B'].includes(sig.quality)); | |
| // Do not suggest or trade if there's already an active trade running on this symbol | |
| signals = signals.filter(sig => !tradeManager.activeTrades.some(t => t.symbol === sig.symbol && t.status === 'active')); | |
| // Do not suggest or trade if this symbol is in a 10-minute cooldown after close | |
| signals = signals.filter(sig => !tradeManager.isSymbolCoolingDown(sig.symbol)); | |
| // Client-side auto-execution is disabled; server runs scans and autopilot auto-execution 24/7. | |
| // --- Render Chart Overlays --- | |
| const emaDataForChart = { | |
| 9: history.map((c, i) => ({ time: c.time, value: ema9[i] })), | |
| 21: history.map((c, i) => ({ time: c.time, value: ema21[i] })), | |
| 50: history.map((c, i) => ({ time: c.time, value: ema50[i] })), | |
| 200: history.map((c, i) => ({ time: c.time, value: ema200[i] })), | |
| }; | |
| chartManager.drawEMAs(emaDataForChart); | |
| chartManager.drawOrderBlocks(orderBlocks, history); | |
| chartManager.drawZones(zones); | |
| chartManager.setMarkers(structure, history); | |
| // Draw active trade levels or first high-probability scanned signal overlays on the chart | |
| const activeTrade = tradeManager.activeTrades.find(t => t.symbol === currentSymbol); | |
| if (activeTrade) { | |
| chartManager.drawSignal({ | |
| entry: activeTrade.entry, | |
| sl: activeTrade.sl, | |
| tp1: activeTrade.tp1, | |
| tp2: activeTrade.tp2, | |
| symbol: activeTrade.symbol, | |
| }); | |
| } else if (signals.length > 0) { | |
| chartManager.drawSignal(signals[0]); | |
| } else { | |
| chartManager._clearDrawnObjects('signalLines'); | |
| } | |
| // --- Update UI Panels --- | |
| signalPanel.updateSignals(signals, currentSymbol); | |
| const symbolStats = tradeManager.getSymbolStats(); | |
| analysisPanel.updateOverview(trend, structure, orderBlocks, zones, indicators, currentSymbol, symbolStats, tradeManager.tradeHistory, signals); | |
| // Update active positions' suggestions based on newly calculated structure & indicators | |
| for (const trade of tradeManager.activeTrades) { | |
| if (trade.symbol === currentSymbol) { | |
| const exitAnalysis = analyzeExit(history, trade); | |
| tradeManager.updateExitAnalysis(trade.id, exitAnalysis); | |
| } | |
| } | |
| // Update account summary display in header | |
| const summary = tradeManager.getAccountSummary(); | |
| header.updateAccountSummary(summary.balance, summary.equity); | |
| } | |
| /// --- State Synchronization from 24/7 Server Autopilot --- | |
| async function syncTerminalState() { | |
| try { | |
| const response = await fetch('/api/terminal-state'); | |
| if (!response.ok) throw new Error('API server returned error'); | |
| const state = await response.json(); | |
| // Synchronize client-side TradeManager properties | |
| tradeManager.activeTrades = state.activeTrades || []; | |
| tradeManager.tradeHistory = state.tradeHistory || []; | |
| tradeManager.accountBalance = state.accountBalance ?? 5000.0; | |
| tradeManager.dailyLosses = state.dailyLosses ?? 0; | |
| tradeManager.lastClosedTime = state.lastClosedTime || {}; | |
| tradeManager.lastExecutedCandleTime = state.lastExecutedCandleTime || {}; | |
| // Update UI elements | |
| header.updateDailyLosses(tradeManager.dailyLosses); | |
| // Calculate equity and update header summary | |
| const summary = tradeManager.getAccountSummary(); | |
| header.updateAccountSummary(summary.balance, summary.equity); | |
| // Re-render trade manager panel card list | |
| tradeManager._render(); | |
| } catch (err) { | |
| console.error('[Main] Failed to sync terminal state with server:', err); | |
| } | |
| } | |
| // Make it globally accessible so manual trade triggers can cause immediate syncs | |
| globalThis.syncTerminalState = syncTerminalState; | |
| // --- Initial Startup --- | |
| async function startApp() { | |
| await syncTerminalState(); // Discover Twelve Data status first | |
| initSymbolTimeframe(currentSymbol, currentTimeframe); | |
| } | |
| startApp(); | |
| // Synchronize state from 24/7 server every 3 seconds | |
| setInterval(syncTerminalState, 3000); | |