import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import path from 'path'; import { fileURLToPath } from 'url'; import WebSocket from 'ws'; // Load environment variables dotenv.config(); // Deriving paths in ES module const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // -------------------------------------------------------------------- // WS & Fetch Polyfill (CORS & Proxy handling) // -------------------------------------------------------------------- globalThis.WebSocket = WebSocket; const originalFetch = globalThis.fetch; globalThis.fetch = async (input, init) => { let url = typeof input === 'string' ? input : input.url; if (url.startsWith('/api/binance')) { url = 'https://api.binance.com' + url.replace(/^\/api\/binance/, ''); } else if (url.startsWith('/api/finnhub')) { url = 'https://finnhub.io' + url.replace(/^\/api\/finnhub/, ''); } else if (url.startsWith('/api/swissquote')) { url = 'https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD'; } else if (url.startsWith('/api/yahoo') || url.includes('query1.finance.yahoo.com')) { if (url.startsWith('/api/yahoo')) { url = 'https://query1.finance.yahoo.com' + url.replace(/^\/api\/yahoo/, ''); } init = init || {}; init.headers = { ...init.headers, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Origin': 'https://finance.yahoo.com', 'Referer': 'https://finance.yahoo.com', }; } return originalFetch(url, init); }; // -------------------------------------------------------------------- // Imports of application code // -------------------------------------------------------------------- import DBManager from './src/data/db.js'; import TradeManager from './src/components/tradeManager.js'; import { BinanceStream } from './src/data/binanceWebSocket.js'; import { YahooStream } from './src/data/yahooWebSocket.js'; import { fetchKlines } from './src/data/binanceAPI.js'; import { fetchCandles, filterForexOutlierWicks } from './src/data/yahooFinanceAPI.js'; import { fetchTwelveCandles } from './src/data/twelveDataAPI.js'; import { generateSignals, LIVE_STRATEGY_OPTS } from './src/analysis/signalGenerator.js'; import { analyzeExit } from './src/analysis/exitManager.js'; import { sendTelegramMessage } from './src/data/telegramNotifier.js'; // -------------------------------------------------------------------- // App Setup // -------------------------------------------------------------------- const app = express(); app.use(cors()); app.use(express.json()); const PORT = process.env.PORT || 7860; const TIMEFRAME = process.env.AUTOPILOT_TIMEFRAME || '15m'; const SYMBOLS = ['BTCUSDT', 'XAUUSD', 'GBPUSD', 'USDCAD']; // Filter active autopilot symbols to maximize returns and eliminate noise const AUTOPILOT_SYMBOLS = ['BTCUSDT', 'XAUUSD', 'GBPUSD', 'USDCAD']; // Per-symbol Twelve Data API keys (OANDA-grade forex/metals that match TradingView). // Read from environment so keys stay server-side and out of the public repo/bundle. // A symbol with no key transparently falls back to Yahoo. const TWELVEDATA_KEYS = { XAUUSD: process.env.TWELVEDATA_KEY_XAUUSD, GBPUSD: process.env.TWELVEDATA_KEY_GBPUSD, USDCAD: process.env.TWELVEDATA_KEY_USDCAD, }; const getTdKey = (symbol) => TWELVEDATA_KEYS[symbol] || null; const dbManager = new DBManager(); const tradeManager = new TradeManager({ isServer: true, db: dbManager }); // State caching for background scanners const historyMap = {}; const livePrices = {}; const streams = {}; const lastUpdate = {}; // last time each symbol's feed delivered data (ms) const staleAlerted = {}; // de-dupe flag for feed-stale Telegram alerts const lastScannedCandleTime = {}; // Helper delay utility const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); // -------------------------------------------------------------------- // Background 24/7 Scanning and Feed Handlers // -------------------------------------------------------------------- async function loadHistoryForSymbol(symbol) { try { const candles = await fetchCandles(symbol, TIMEFRAME); if (candles && candles.length > 0) { historyMap[symbol] = candles; livePrices[symbol] = candles[candles.length - 1].close; console.log(`[Autopilot] Loaded ${candles.length} historical candles for ${symbol}. Current Price: $${livePrices[symbol]}`); } } catch (err) { console.error(`[Autopilot] Failed to load history for ${symbol}:`, err.message); } } function handleTick(symbol, price) { if (livePrices[symbol] === price) return; livePrices[symbol] = price; // 1. Feed to trade manager to check SL/TP/Partial exits in real-time tradeManager.updatePrices(price, symbol); // 2. Perform live exit suggestions / warnings updates on active trades of this symbol const activeTrades = tradeManager.activeTrades.filter(t => t.symbol === symbol && t.status === 'active'); if (activeTrades.length > 0 && historyMap[symbol] && historyMap[symbol].length > 0) { const candlesCopy = [...historyMap[symbol]]; const lastIdx = candlesCopy.length - 1; candlesCopy[lastIdx] = { ...candlesCopy[lastIdx], close: price, high: Math.max(candlesCopy[lastIdx].high, price), low: Math.min(candlesCopy[lastIdx].low, price), }; for (const trade of activeTrades) { const exitAnalysis = analyzeExit(candlesCopy, trade); tradeManager.updateExitAnalysis(trade.id, exitAnalysis); } } } function handleCandleUpdate(symbol, candle) { lastUpdate[symbol] = Date.now(); if (!historyMap[symbol]) historyMap[symbol] = []; const history = historyMap[symbol]; const lastIndex = history.findIndex(c => c.time === candle.time); if (lastIndex !== -1) { history[lastIndex] = { ...candle }; } else { // Transition detected! A new candle has started. // The previous candle in history is now closed and finalized. if (history.length > 0) { const closedCandle = history[history.length - 1]; closedCandle.isClosed = true; console.log(`[Autopilot] Transition detected: Candle CLOSED for ${symbol} at ${new Date(closedCandle.time * 1000).toLocaleTimeString()}. Recalculating setups.`); runScannersForSymbol(symbol); } history.push({ ...candle }); if (history.length > 1000) history.shift(); } // If candle is explicitly marked closed (e.g. from Yahoo polling ticks), run the scan if (candle.isClosed) { console.log(`[Autopilot] Candle CLOSED for ${symbol} at ${new Date().toLocaleTimeString()}. Recalculating setups.`); runScannersForSymbol(symbol); } } function runScannersForSymbol(symbol) { const candles = historyMap[symbol]; if (!candles || candles.length < 50) { console.warn(`[Autopilot] Insufficient candles to scan ${symbol}`); return; } const latestCandle = candles[candles.length - 1]; if (lastScannedCandleTime[symbol] === latestCandle.time) { return; // Already scanned this candle } lastScannedCandleTime[symbol] = latestCandle.time; // Prevent opening trades if symbol has active trade or is in 10-minute cooldown const hasActiveTrade = tradeManager.activeTrades.some(t => t.symbol === symbol && t.status === 'active'); if (hasActiveTrade) return; if (!AUTOPILOT_SYMBOLS.includes(symbol)) { return; // Exclude unprofitable symbols from auto-trading } if (tradeManager.isSymbolCoolingDown(symbol)) { console.log(`[Autopilot] Skipping ${symbol} scan due to 10-minute post-close cooldown.`); return; } try { let signals = generateSignals(candles, symbol, tradeManager.dailyLosses, LIVE_STRATEGY_OPTS); // Strict quality guards signals = signals.filter(sig => ['A', 'B'].includes(sig.quality)); signals = signals.filter(sig => !tradeManager.activeTrades.some(t => t.symbol === sig.symbol && t.status === 'active')); signals = signals.filter(sig => !tradeManager.isSymbolCoolingDown(sig.symbol)); if (signals.length > 0) { const bestSignal = signals[0]; const latestCandle = candles[candles.length - 1]; // Block same-candle executions if (!tradeManager.wasAlreadyExecuted(symbol, latestCandle.time)) { console.log(`[Autopilot] 🎯 high-probability Grade ${bestSignal.quality} setup scanned for ${symbol}! Auto-executing:`, bestSignal); tradeManager.takeTrade(bestSignal, latestCandle.time); } } } catch (err) { console.error(`[Autopilot] Error running signal scan for ${symbol}:`, err); } } // Poll Twelve Data for one symbol on its own dedicated key. Free tier allows // 800 calls/day, so a 120s cadence (~720/day) stays safely under the limit while // still tracking 15m candles. Backs off on errors so a hiccup never hammers the API. function startTwelveDataPoller(symbol, apiKey, basePollMs = 120000) { let currentMs = basePollMs; const maxMs = 600000; let stopped = false; async function loop() { if (stopped) return; let ok = false; try { const candles = await fetchTwelveCandles(symbol, TIMEFRAME, 10, apiKey); if (candles && candles.length > 0) { ok = true; const latest = candles[candles.length - 1]; handleTick(symbol, latest.close); const durationMap = { '1m': 60, '5m': 300, '15m': 900, '1H': 3600, '4H': 14400, '1D': 86400, }; const tfSec = durationMap[TIMEFRAME] || 900; handleCandleUpdate(symbol, { ...latest, isClosed: (Math.floor(Date.now() / 1000) - latest.time) >= tfSec, }); } } catch (err) { console.error(`[TwelveData] poll failed for ${symbol}:`, err.message); } currentMs = ok ? basePollMs : Math.min(maxMs, Math.round(currentMs * 1.8)); if (!stopped) setTimeout(loop, currentMs); } setTimeout(loop, 0); streams[symbol] = { unsubscribe: () => { stopped = true; } }; } // -------------------------------------------------------------------- // Feed health monitoring (weekend-aware) // -------------------------------------------------------------------- function isForexOpen(now = new Date()) { const day = now.getUTCDay(); // 0 Sun .. 6 Sat const h = now.getUTCHours(); if (day === 6) return false; // Saturday: closed if (day === 0 && h < 22) return false; // Sunday before ~22:00 UTC if (day === 5 && h >= 21) return false; // Friday after ~21:00 UTC return true; } function feedExpectedLive(symbol) { return symbol === 'BTCUSDT' ? true : isForexOpen(); } const STALE_SECONDS = 20 * 60; function checkFeedHealth() { for (const symbol of SYMBOLS) { const last = lastUpdate[symbol]; const ageSec = last ? (Date.now() - last) / 1000 : Infinity; const stale = ageSec > STALE_SECONDS && feedExpectedLive(symbol); if (stale && !staleAlerted[symbol]) { staleAlerted[symbol] = true; console.warn(`[FeedHealth] ${symbol} feed stale (${Math.round(ageSec / 60)} min).`); sendTelegramMessage(`⚠️ FEED STALE\n\n${symbol} has not updated in ${Math.round(ageSec / 60)} min while its market should be open. Check the data feed / Twelve Data key.`); } else if (!stale && staleAlerted[symbol]) { staleAlerted[symbol] = false; sendTelegramMessage(`✅ FEED RECOVERED\n\n${symbol} is updating again.`); } } } async function startAutopilot() { console.log('[Autopilot] Initializing feeds (Twelve Data for keyed forex/metals, Yahoo for the rest)...'); // 1. Load initial history for all symbols using Yahoo Finance console.log('[Autopilot] Pre-loading historical caches...'); for (const symbol of SYMBOLS) { const tdKey = getTdKey(symbol); try { let candles; if (tdKey) { try { candles = await fetchTwelveCandles(symbol, '15m', 500, tdKey); console.log(`[Autopilot] Loaded ${candles.length} Twelve Data candles for ${symbol}.`); } catch (tdErr) { console.error(`[Autopilot] Twelve Data pre-load failed for ${symbol}, falling back to Yahoo:`, tdErr.message); candles = await fetchCandles(symbol, '15m'); } } else { candles = await fetchCandles(symbol, '15m'); } if (candles && candles.length > 0) { historyMap[symbol] = candles; livePrices[symbol] = candles[candles.length - 1].close; // Immediately calculate active trades' P&L and R:R with the loaded price tradeManager.updatePrices(livePrices[symbol], symbol); } } catch (err) { console.error(`[Autopilot] Pre-load failed for ${symbol}:`, err.message); } } // 2. Subscribe to real-time Yahoo streams for all 6 symbols in the background console.log('[Autopilot] Establishing background monitoring feeds...'); for (const [idx, symbol] of SYMBOLS.entries()) { const tdKey = getTdKey(symbol); if (tdKey) { // OANDA-grade feed on a dedicated key (broker / TradingView-matching candles). console.log(`[Autopilot] Streaming ${symbol} from Twelve Data (dedicated key).`); startTwelveDataPoller(symbol, tdKey, 120000); } else { const stream = new YahooStream(); stream.subscribe(symbol, TIMEFRAME, { onTick: (price) => { handleTick(symbol, price); }, onCandleUpdate: (candle) => { handleCandleUpdate(symbol, candle); }, }, { pollMs: 10000, staggerMs: idx * 1500 }); streams[symbol] = stream; } } console.log(`[Server Autopilot] Startup complete. Timeframe: ${TIMEFRAME}. Autopilot active for: ${AUTOPILOT_SYMBOLS.join(', ')}.`); // Monitor feed liveness; alert (weekend-aware) if a feed stalls during market hours. setInterval(checkFeedHealth, 5 * 60 * 1000); } // -------------------------------------------------------------------- // API Routing // -------------------------------------------------------------------- app.get('/api/terminal-state', (req, res) => { res.json({ activeTrades: tradeManager.activeTrades, tradeHistory: tradeManager.tradeHistory, accountBalance: tradeManager.accountBalance, dailyLosses: tradeManager.dailyLosses, lastClosedTime: tradeManager.lastClosedTime, lastExecutedCandleTime: tradeManager.lastExecutedCandleTime, twelveDataActive: false, feeds: Object.fromEntries(SYMBOLS.map(s => [s, lastUpdate[s] ? Math.floor((Date.now() - lastUpdate[s]) / 1000) : null])), }); }); // Cached market data: browsers read candles + live price from the server's own // poller cache instead of each hitting Yahoo. Reads in-memory state, no upstream call. app.get('/api/market-data', (req, res) => { const symbol = req.query.symbol; const rawCandles = (symbol && historyMap[symbol]) ? historyMap[symbol].slice(-500) : []; // Re-apply the forex outlier-wick filter on read, so candles cached in memory // before this fix (or any stray glitch bar) are cleaned without a full restart. // Twelve Data is already broker-clean, so only the Yahoo path needs wick scrubbing. const candles = getTdKey(symbol) ? rawCandles : filterForexOutlierWicks(rawCandles, symbol); // Prevent browser caching of real-time market data res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.json({ symbol: symbol || null, candles, price: (symbol && livePrices[symbol] != null) ? livePrices[symbol] : null, serverTime: Math.floor(Date.now() / 1000), }); }); app.post('/api/take-trade', async (req, res) => { const { signal } = req.body; if (!signal) { return res.status(400).json({ error: 'Signal structure required' }); } // takeTrade is async and resolves to null when the trade is rejected // (duplicate symbol, cooldown, etc.) — await it so success reflects reality. const trade = await tradeManager.takeTrade(signal); res.json({ success: !!trade, trade: trade || null }); }); app.post('/api/manual-close', (req, res) => { const { tradeId } = req.body; if (tradeId === undefined) { return res.status(400).json({ error: 'Trade ID required' }); } tradeManager.manualClose(Number(tradeId)); res.json({ success: true, message: `Trade ${tradeId} closed successfully.` }); }); app.post('/api/reset-account', (req, res) => { tradeManager.resetAccount(); res.json({ success: true, message: 'Terminal account state reset completely.' }); }); app.get('/api/test-telegram', async (req, res) => { const token = process.env.TELEGRAM_BOT_TOKEN; const chatId = process.env.TELEGRAM_CHAT_ID; if (!token || !chatId) { return res.status(400).json({ success: false, error: `Missing credentials. Token: ${token ? 'configured' : 'MISSING'}, Chat ID: ${chatId ? 'configured' : 'MISSING'}` }); } try { const url = `https://api.telegram.org/bot${token}/sendMessage`; const msg = `🔔 Trade Analyzer Connection Test\n\n` + `Status: 🟢 Connected successfully!\n` + `Source: 🚀 Triggered from Hugging Face Server\n` + `Time: ${new Date().toLocaleString()}\n\n` + `This confirms your Telegram configuration is working perfectly.`; const telegramRes = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text: msg, parse_mode: 'HTML', }), }); const data = await telegramRes.json(); if (telegramRes.ok && data.ok) { res.json({ success: true, message: 'Test message sent to Telegram successfully.', data }); } else { res.status(400).json({ success: false, error: data.description || 'Telegram API Error', data }); } } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); // Proxy routes for client-side fetches (Bypasses CORS natively) app.use('/api/binance', async (req, res) => { try { const targetUrl = 'https://api.binance.com' + req.originalUrl.replace(/^\/api\/binance/, ''); const response = await fetch(targetUrl); res.status(response.status); const data = await response.text(); res.send(data); } catch (err) { res.status(500).send(err.message); } }); app.use('/api/yahoo', async (req, res) => { try { const targetUrl = 'https://query1.finance.yahoo.com' + req.originalUrl.replace(/^\/api\/yahoo/, ''); const response = await fetch(targetUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Origin': 'https://finance.yahoo.com', 'Referer': 'https://finance.yahoo.com', } }); res.status(response.status); const data = await response.text(); res.send(data); } catch (err) { res.status(500).send(err.message); } }); app.use('/api/swissquote', async (req, res) => { try { const targetUrl = 'https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD'; const response = await fetch(targetUrl); res.status(response.status); const data = await response.text(); res.send(data); } catch (err) { res.status(500).send(err.message); } }); // -------------------------------------------------------------------- // Static Asset hosting (Production deployment build) // -------------------------------------------------------------------- app.use(express.static(path.join(__dirname, 'dist'), { setHeaders: (res, filePath) => { // Hashed JS/CSS bundles are content-addressed and safe to cache forever, but // index.html must never be cached or browsers keep loading the old bundle after // a deploy (the root cause of "I pushed but still see the old version"). if (filePath.endsWith('.html')) { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); } }, })); app.use((req, res) => { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.sendFile(path.join(__dirname, 'dist', 'index.html')); }); // -------------------------------------------------------------------- // Database connection & bootup coordination // -------------------------------------------------------------------- async function bootServer() { const uri = process.env.MONGODB_URI; if (!uri) { console.error('CRITICAL ERROR: MONGODB_URI environment variable is missing.'); process.exit(1); } try { // 1. Establish Mongo connection & Sync state await dbManager.connect(uri); const state = await dbManager.loadState(); // Sync DB into server TradeManager properties tradeManager.activeTrades = state.activeTrades || []; tradeManager.tradeHistory = state.tradeHistory || []; tradeManager.accountBalance = state.accountBalance ?? 5000.0; tradeManager.dailyLosses = state.dailyLosses ?? 0; tradeManager.lastExecutedCandleTime = state.lastExecutedCandleTime || {}; tradeManager.lastClosedTime = state.lastClosedTime || {}; tradeManager.lastResetDate = state.lastResetDate || new Date().toDateString(); console.log('[Server] MongoDB terminal state recovered successfully.'); console.log(`[Server] Active Positions: ${tradeManager.activeTrades.length} | Balance: $${tradeManager.accountBalance.toFixed(2)}`); // 2. Start 24/7 scanning feed loops await startAutopilot(); // 3. Bind HTTP Port listener app.listen(PORT, '0.0.0.0', () => { console.log(`[Server] Trade Analyzer Full-Stack Engine running on port ${PORT}`); }); } catch (err) { console.error('Failed to boot terminal backend:', err); process.exit(1); } } bootServer();