/** * Hybrid Trading System (HTS) Page * Complete implementation with real-time data, WebSocket, and full functionality */ import HTSEngine from './hts-engine.js'; import { TradingIcons } from './icons.js'; import { escapeHtml, safeFormatNumber, safeFormatCurrency } from '../../shared/js/utils/sanitizer.js'; class HTSPage { constructor() { this.engine = new HTSEngine(); this.symbol = 'BTCUSDT'; this.timeframe = '1h'; this.chart = null; this.candlestickSeries = null; this.rsiSeries = null; this.macdSeries = null; this.volumeSeries = null; this.ohlcvData = []; this.analysisResult = null; this.autoAnalysisInterval = null; this.dataUpdateInterval = null; } async init() { try { console.log('[HTS] Initializing Hybrid Trading System...'); this.bindEvents(); await this.initChart(); await this.loadInitialData(); await this.runAnalysis(); this.startDataUpdates(); this.startAutoAnalysis(); console.log('[HTS] Ready'); } catch (error) { console.error('[HTS] Init error:', error); this.showError('Failed to initialize HTS. Please refresh the page.'); } } /** * Bind event listeners */ bindEvents() { // Tab switching document.querySelectorAll('.trading-tab').forEach(tab => { tab.addEventListener('click', (e) => { const view = e.currentTarget.dataset.view; this.switchView(view); }); }); // Symbol change document.getElementById('hts-symbol')?.addEventListener('change', (e) => { this.symbol = e.target.value; this.loadInitialData(); }); // Timeframe change document.getElementById('hts-timeframe')?.addEventListener('change', (e) => { this.timeframe = e.target.value; this.loadInitialData(); }); // Auto-analysis toggle document.getElementById('hts-auto-trade')?.addEventListener('change', (e) => { if (e.target.checked) { this.startAutoAnalysis(); } else { this.stopAutoAnalysis(); } }); // Manual analyze button document.getElementById('hts-analyze-btn')?.addEventListener('click', () => { this.runAnalysis(); }); // Indicator toggles document.getElementById('show-rsi')?.addEventListener('change', () => this.updateChart()); document.getElementById('show-macd')?.addEventListener('change', () => this.updateChart()); document.getElementById('show-volume')?.addEventListener('change', () => this.updateChart()); } /** * Switch between standard and HTS views */ switchView(view) { document.querySelectorAll('.trading-tab').forEach(tab => { tab.classList.remove('active'); }); document.querySelector(`[data-view="${view}"]`)?.classList.add('active'); const standardView = document.getElementById('standard-trading-view'); const htsView = document.getElementById('hts-trading-view'); if (view === 'hts') { standardView.style.display = 'none'; htsView.style.display = 'block'; if (!this.chart) { this.init(); } } else { standardView.style.display = 'block'; htsView.style.display = 'none'; } } /** * Initialize TradingView Lightweight Chart */ async initChart() { const container = document.getElementById('hts-chart-container'); if (!container) { console.warn('[HTS] Chart container not found'); return; } // Wait for LightweightCharts library to load (max 5 seconds) let retries = 0; const maxRetries = 10; while (typeof LightweightCharts === 'undefined' && retries < maxRetries) { await new Promise(resolve => setTimeout(resolve, 500)); retries++; } if (typeof LightweightCharts === 'undefined') { console.error('[HTS] TradingView Lightweight Charts library not loaded after timeout'); this.showError('Charting library not available. Please refresh the page.'); return; } try { this.chart = LightweightCharts.createChart(container, { width: container.clientWidth, height: 500, layout: { background: { color: '#1a1a1a' }, textColor: '#d1d5db', }, grid: { vertLines: { color: '#2a2a2a' }, horzLines: { color: '#2a2a2a' }, }, timeScale: { timeVisible: true, secondsVisible: false, }, }); if (!this.chart) { throw new Error('Failed to create chart instance'); } // Try multiple methods to create candlestick series (compatibility with different library versions) const seriesOptions = { upColor: '#26a69a', downColor: '#ef5350', borderVisible: false, wickUpColor: '#26a69a', wickDownColor: '#ef5350', }; // Method 1: Try addCandlestickSeries (older API) if (typeof this.chart.addCandlestickSeries === 'function') { this.candlestickSeries = this.chart.addCandlestickSeries(seriesOptions); } // Method 2: Try addSeries with CandlestickSeries type (newer API) else if (typeof this.chart.addSeries === 'function' && LightweightCharts.SeriesType && LightweightCharts.SeriesType.Candlestick) { this.candlestickSeries = this.chart.addSeries(LightweightCharts.SeriesType.Candlestick, seriesOptions); } // Method 3: Try addSeries with string type else if (typeof this.chart.addSeries === 'function') { try { this.candlestickSeries = this.chart.addSeries('Candlestick', seriesOptions); } catch (e) { console.warn('[HTS] Failed to create series with string type:', e); } } if (!this.candlestickSeries) { console.error('[HTS] Available chart methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(this.chart))); throw new Error('Failed to create candlestick series - no compatible method found'); } if (typeof this.chart.addHistogramSeries === 'function') { this.volumeSeries = this.chart.addHistogramSeries({ color: '#26a69a', priceFormat: { type: 'volume', }, priceScaleId: 'volume', scaleMargins: { top: 0.8, bottom: 0, }, }); } if (typeof this.chart.addLineSeries === 'function') { this.rsiSeries = this.chart.addLineSeries({ color: '#ff9800', lineWidth: 2, priceScaleId: 'rsi', scaleMargins: { top: 0.7, bottom: 0, }, }); this.macdSeries = this.chart.addLineSeries({ color: '#2196f3', lineWidth: 2, priceScaleId: 'macd', scaleMargins: { top: 0.5, bottom: 0.3, }, }); } // Handle resize window.addEventListener('resize', () => { if (this.chart && container) { this.chart.applyOptions({ width: container.clientWidth }); } }); console.log('[HTS] Chart initialized successfully'); } catch (error) { console.error('[HTS] Chart initialization error:', error); this.showError(`Failed to initialize chart: ${error.message}`); this.chart = null; this.candlestickSeries = null; this.volumeSeries = null; this.rsiSeries = null; this.macdSeries = null; } } /** * Start periodic data updates from API */ startDataUpdates() { this.stopDataUpdates(); // Update data every 30 seconds this.dataUpdateInterval = setInterval(async () => { try { await this.loadInitialData(); if (document.getElementById('hts-auto-trade')?.checked) { await this.runAnalysis(); } } catch (error) { console.warn('[HTS] Data update error:', error); } }, 30000); } /** * Stop data updates */ stopDataUpdates() { if (this.dataUpdateInterval) { clearInterval(this.dataUpdateInterval); this.dataUpdateInterval = null; } } /** * Load initial OHLCV data from API */ async loadInitialData() { try { this.updateConnectionStatus('Loading data...', 'info'); const symbol = this.symbol.replace('USDT', ''); // Get base API URL - use relative URLs for HuggingFace compatibility const baseUrl = window.location.origin; const apiUrl = `${baseUrl}/api/market?symbol=${symbol}&limit=100`; // Try multiple API endpoints with retry logic let data = null; let response = null; let retries = 0; const maxRetries = 2; // Try /api/market endpoint first while (retries <= maxRetries) { try { if (retries > 0) { const delay = Math.min(1000 * Math.pow(2, retries - 1), 5000); await new Promise(resolve => setTimeout(resolve, delay)); } response = await fetch(apiUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', }, signal: AbortSignal.timeout(10000) }); if (response.ok) { break; } if (retries < maxRetries && response.status >= 500) { retries++; continue; } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } catch (error) { if (retries < maxRetries && (error.name === 'AbortError' || error.message.includes('timeout') || error.message.includes('network'))) { retries++; continue; } throw error; } } if (!response || !response.ok) { throw new Error('Failed to fetch data after retries'); } data = await response.json(); if (!data || typeof data !== 'object') { throw new Error('Invalid response format'); } if (data && data.success && Array.isArray(data.items) && data.items.length > 0) { const item = data.items.find(i => i && i.symbol === symbol) || data.items[0]; if (item && typeof item === 'object') { const price = parseFloat(item.price); if (!isNaN(price) && price > 0) { // Generate OHLCV from price data this.ohlcvData = this.generateOHLCVFromPrice(price, 100); this.updateChart(); this.updateConnectionStatus('Data loaded', 'success'); return; } } } } catch (e) { console.warn('[HTS] Primary API failed, trying fallback:', e); // Log the error for debugging if (e.message && e.message.includes('ERR_CONNECTION_REFUSED')) { console.warn('[HTS] Connection refused - ensure backend is running or use correct API URL'); } } // Fallback: Generate synthetic OHLCV data this.generateFallbackData(); this.updateConnectionStatus('Using synthetic data', 'warning'); } /** * Generate OHLCV data from single price point */ generateOHLCVFromPrice(basePrice, count) { const data = []; const now = Math.floor(Date.now() / 1000); const interval = 3600; // 1 hour intervals for (let i = count; i >= 0; i--) { const priceVariation = (Math.random() - 0.5) * basePrice * 0.02; // ±1% variation const open = basePrice + priceVariation; const close = open + (Math.random() - 0.5) * basePrice * 0.01; const high = Math.max(open, close) + Math.random() * basePrice * 0.005; const low = Math.min(open, close) - Math.random() * basePrice * 0.005; data.push({ time: now - (i * interval), open: Math.max(0, open), high: Math.max(open, high, close), low: Math.min(open, low, close), close: Math.max(0, close), volume: Math.random() * 1000000 }); } return data; } /** * Generate fallback OHLCV data for testing */ generateFallbackData() { const basePrice = 50000; const data = []; const now = Math.floor(Date.now() / 1000); for (let i = 100; i >= 0; i--) { const priceChange = (Math.random() - 0.5) * 1000; const open = basePrice + priceChange; const close = open + (Math.random() - 0.5) * 500; const high = Math.max(open, close) + Math.random() * 200; const low = Math.min(open, close) - Math.random() * 200; data.push({ time: now - (i * 3600), // 1 hour intervals open: open, high: high, low: low, close: close, volume: Math.random() * 1000000 }); } this.ohlcvData = data; this.updateChart(); } /** * Update chart with current data */ updateChart() { if (!this.chart || !this.candlestickSeries || this.ohlcvData.length === 0) { if (!this.chart) { console.warn('[HTS] Chart not initialized, skipping update'); } return; } try { // Update candlestick data const candlestickData = this.ohlcvData.map(d => ({ time: d.time, open: d.open, high: d.high, low: d.low, close: d.close })); if (typeof this.candlestickSeries.setData === 'function') { this.candlestickSeries.setData(candlestickData); } // Update volume if (this.volumeSeries && document.getElementById('show-volume')?.checked) { if (typeof this.volumeSeries.setData === 'function') { const volumeData = this.ohlcvData.map(d => ({ time: d.time, value: d.volume, color: d.close >= d.open ? '#26a69a80' : '#ef535080' })); this.volumeSeries.setData(volumeData); } } // Calculate and update RSI if (this.rsiSeries && document.getElementById('show-rsi')?.checked) { if (typeof this.rsiSeries.setData === 'function') { const rsiValues = this.calculateRSIForChart(); if (rsiValues.length > 0) { this.rsiSeries.setData(rsiValues); } } } // Calculate and update MACD if (this.macdSeries && document.getElementById('show-macd')?.checked) { if (typeof this.macdSeries.setData === 'function') { const macdValues = this.calculateMACDForChart(); if (macdValues.length > 0) { this.macdSeries.setData(macdValues); } } } // Fit content to view if (typeof this.chart.timeScale === 'function') { const timeScale = this.chart.timeScale(); if (timeScale && typeof timeScale.fitContent === 'function') { timeScale.fitContent(); } } } catch (error) { console.error('[HTS] Chart update error:', error); } } /** * Calculate RSI for chart display */ calculateRSIForChart() { if (this.ohlcvData.length < 15) return []; const closes = this.ohlcvData.map(d => d.close); const rsiValues = []; for (let i = 14; i < closes.length; i++) { const rsi = this.engine.calculateRSI(closes.slice(0, i + 1), 14); if (rsi !== null) { rsiValues.push({ time: this.ohlcvData[i].time, value: rsi }); } } return rsiValues; } /** * Calculate MACD for chart display */ calculateMACDForChart() { if (this.ohlcvData.length < 26) return []; const closes = this.ohlcvData.map(d => d.close); const macdValues = []; for (let i = 26; i < closes.length; i++) { const macd = this.engine.calculateMACD(closes.slice(0, i + 1)); if (macd && macd.macd !== null) { macdValues.push({ time: this.ohlcvData[i].time, value: macd.macd }); } } return macdValues; } /** * Run HTS analysis */ async runAnalysis() { try { if (this.ohlcvData.length < 30) { this.showError('Insufficient data for analysis. Please wait...'); return; } const symbol = this.symbol.replace('USDT', ''); this.analysisResult = await this.engine.analyze(this.ohlcvData, symbol); this.renderAnalysisResult(); this.renderComponents(); this.renderSMCLevels(); this.renderPatterns(); } catch (error) { console.error('[HTS] Analysis error:', error); this.showError('Analysis failed: ' + error.message); } } /** * Render analysis result */ renderAnalysisResult() { if (!this.analysisResult) return; const container = document.getElementById('hts-signal-content'); if (!container) return; if (!this.analysisResult || typeof this.analysisResult !== 'object') { container.innerHTML = '
Invalid analysis result
'; return; } const { finalScore, finalSignal, confidence, currentPrice, stopLoss, takeProfitLevels, riskReward, marketRegime } = this.analysisResult; const signal = String(finalSignal || 'hold').toLowerCase(); const signalColor = signal === 'buy' ? '#22c55e' : signal === 'sell' ? '#ef4444' : '#eab308'; const signalIcon = signal === 'buy' ? TradingIcons.buy : signal === 'sell' ? TradingIcons.sell : TradingIcons.hold; const validScore = typeof finalScore === 'number' && !isNaN(finalScore) ? finalScore : 0; const validConfidence = typeof confidence === 'number' && !isNaN(confidence) ? Math.max(0, Math.min(100, confidence)) : 0; const validPrice = typeof currentPrice === 'number' && !isNaN(currentPrice) && currentPrice > 0 ? currentPrice : 0; const validStopLoss = typeof stopLoss === 'number' && !isNaN(stopLoss) && stopLoss > 0 ? stopLoss : 0; const validTakeProfits = Array.isArray(takeProfitLevels) ? takeProfitLevels.filter(tp => tp && typeof tp === 'object' && typeof tp.level === 'number' && !isNaN(tp.level)) : []; const validRiskReward = typeof riskReward === 'number' && !isNaN(riskReward) ? riskReward : 0; const regimeColors = { 'trending': '#3b82f6', 'ranging': '#8b5cf6', 'volatile': '#f59e0b', 'volatile-trending': '#ef4444', 'neutral': '#6b7280' }; const regimeLabels = { 'trending': 'Trending Market', 'ranging': 'Ranging Market', 'volatile': 'Volatile Market', 'volatile-trending': 'Volatile Trending', 'neutral': 'Neutral Market' }; container.innerHTML = `
${marketRegime ? `
Market Regime: ${regimeLabels[marketRegime.regime || 'neutral']} Volatility: ${(marketRegime.volatility || 0).toFixed(2)}% | Trend: ${(marketRegime.trendStrength || 0).toFixed(0)}%
` : ''}
${escapeHtml(safeFormatNumber(validScore, { minimumFractionDigits: 1, maximumFractionDigits: 1 }))}
Final Score
Signal: ${signalIcon} ${escapeHtml(signal.toUpperCase())}
Confidence: ${escapeHtml(safeFormatNumber(validConfidence, { minimumFractionDigits: 1, maximumFractionDigits: 1 }))}%
Current Price: ${validPrice > 0 ? safeFormatCurrency(validPrice) : '—'}
Stop Loss: ${validStopLoss > 0 ? safeFormatCurrency(validStopLoss) : '—'}
Risk/Reward: 1:${escapeHtml(safeFormatNumber(validRiskReward, { minimumFractionDigits: 2, maximumFractionDigits: 2 }))}

Take Profit Levels

${validTakeProfits.length > 0 ? validTakeProfits.map(tp => { const tpType = escapeHtml(String(tp.type || 'TP')); const tpLevel = safeFormatCurrency(tp.level); const tpRR = typeof tp.riskReward === 'number' && !isNaN(tp.riskReward) ? escapeHtml(safeFormatNumber(tp.riskReward, { minimumFractionDigits: 2, maximumFractionDigits: 2 })) : '—'; return `
${tpType}: ${tpLevel} R:R ${tpRR}
`; }).join('') : '
No take profit levels available
'}
`; // Update signal badge const badge = document.getElementById('hts-signal-badge'); if (badge) { badge.textContent = finalSignal.toUpperCase(); badge.className = `signal-badge signal-${finalSignal}`; } } /** * Render component scores */ renderComponents() { if (!this.analysisResult || !this.analysisResult.components) return; const container = document.getElementById('hts-components-grid'); if (!container) return; const components = this.analysisResult.components; if (!components || typeof components !== 'object') { container.innerHTML = '
No component data available
'; return; } container.innerHTML = Object.entries(components) .filter(([key, comp]) => comp && typeof comp === 'object') .map(([key, comp]) => { const validScore = typeof comp.score === 'number' && !isNaN(comp.score) ? Math.max(0, Math.min(100, comp.score)) : 50; const validWeight = typeof comp.weight === 'number' && !isNaN(comp.weight) ? Math.max(0, Math.min(1, comp.weight)) : 0; const validBaseWeight = (comp.baseWeight && typeof comp.baseWeight === 'number' && !isNaN(comp.baseWeight)) ? Math.max(0, Math.min(1, comp.baseWeight)) : validWeight; const validConfidence = typeof comp.confidence === 'number' && !isNaN(comp.confidence) ? Math.max(0, Math.min(100, comp.confidence)) : 0; const scoreColor = validScore > 60 ? '#22c55e' : validScore < 40 ? '#ef4444' : '#eab308'; const weightPercent = (validWeight * 100).toFixed(1); const baseWeightPercent = (validBaseWeight * 100).toFixed(1); const weightChange = validBaseWeight ? validWeight - validBaseWeight : 0; const weightChangePercent = (weightChange * 100).toFixed(1); const weightChangeColor = weightChange > 0.001 ? '#22c55e' : weightChange < -0.001 ? '#ef4444' : '#6b7280'; const signal = escapeHtml(String(comp.signal || 'hold').toUpperCase()); const signalClass = escapeHtml(String(comp.signal || 'hold')); const keyDisplay = escapeHtml(String(key).toUpperCase()); const detailsHtml = (key === 'rsiMacd' && comp.details && typeof comp.details === 'object') ? `
RSI: ${escapeHtml(String(comp.details.rsi || '—'))}
MACD: ${escapeHtml(String(comp.details.macd || '—'))}
Histogram: ${escapeHtml(String(comp.details.histogram || '—'))}
` : ''; return `

${keyDisplay}

${escapeHtml(weightPercent)}% ${Math.abs(weightChange) > 0.001 ? ` ${weightChange > 0 ? '↑' : '↓'} ${escapeHtml(String(Math.abs(weightChangePercent)))}% ` : ''}
${escapeHtml(safeFormatNumber(validScore, { minimumFractionDigits: 1, maximumFractionDigits: 1 }))}
${signal}
Confidence: ${escapeHtml(safeFormatNumber(validConfidence, { minimumFractionDigits: 1, maximumFractionDigits: 1 }))}%
${detailsHtml}
`; }).filter(html => html.length > 0).join('') || '
No component data available
'; } /** * Render SMC levels */ renderSMCLevels() { if (!this.analysisResult || !this.analysisResult.smcLevels) return; const container = document.getElementById('hts-smc-content'); if (!container) return; const smcLevels = this.analysisResult.smcLevels; if (!smcLevels || typeof smcLevels !== 'object') { container.innerHTML = '
No SMC levels available
'; return; } const orderBlocks = Array.isArray(smcLevels.orderBlocks) ? smcLevels.orderBlocks : []; const liquidityZones = Array.isArray(smcLevels.liquidityZones) ? smcLevels.liquidityZones : []; const breakerBlocks = Array.isArray(smcLevels.breakerBlocks) ? smcLevels.breakerBlocks : []; container.innerHTML = `

Order Blocks: ${escapeHtml(String(orderBlocks.length))}

${orderBlocks.slice(-3) .filter(block => block && typeof block === 'object' && typeof block.high === 'number' && !isNaN(block.high) && typeof block.low === 'number' && !isNaN(block.low)) .map(block => { const volume = typeof block.volume === 'number' && !isNaN(block.volume) ? (block.volume / 1000000).toFixed(2) : '0.00'; return `
High: ${safeFormatCurrency(block.high)} Low: ${safeFormatCurrency(block.low)} Volume: ${escapeHtml(volume)}M
`; }).join('') || '
No order blocks
'}

Liquidity Zones: ${escapeHtml(String(liquidityZones.length))}

${liquidityZones .filter(zone => zone && typeof zone === 'object' && typeof zone.level === 'number' && !isNaN(zone.level)) .map(zone => { const zoneType = escapeHtml(String(zone.type || 'unknown').toUpperCase()); const zoneTypeClass = escapeHtml(String(zone.type || 'unknown')); const zoneStrength = escapeHtml(String(zone.strength || 'Medium')); return `
${zoneType}: ${safeFormatCurrency(zone.level)} Strength: ${zoneStrength}
`; }).join('') || '
No liquidity zones
'}

Breaker Blocks: ${escapeHtml(String(breakerBlocks.length))}

${breakerBlocks .filter(block => block && typeof block === 'object' && typeof block.level === 'number' && !isNaN(block.level)) .map(block => { const blockType = escapeHtml(String(block.type || 'unknown').toUpperCase()); const blockTypeClass = escapeHtml(String(block.type || 'unknown')); return `
${blockType} Level: ${safeFormatCurrency(block.level)}
`; }).join('') || '
No breaker blocks
'}
`; } /** * Render detected patterns */ renderPatterns() { if (!this.analysisResult || !this.analysisResult.patterns) return; const container = document.getElementById('hts-patterns-content'); if (!container) return; const patterns = Array.isArray(this.analysisResult.patterns) ? this.analysisResult.patterns : []; if (patterns.length === 0) { container.innerHTML = '

No patterns detected

'; return; } container.innerHTML = `
${patterns .filter(pattern => pattern && typeof pattern === 'object') .map(pattern => { const patternName = escapeHtml(String(pattern.name || 'Unknown Pattern')); const patternType = escapeHtml(String(pattern.type || 'neutral').toUpperCase()); const patternTypeClass = escapeHtml(String(pattern.type || 'neutral')); const patternConfidence = typeof pattern.confidence === 'number' && !isNaN(pattern.confidence) ? escapeHtml(safeFormatNumber(pattern.confidence, { minimumFractionDigits: 0, maximumFractionDigits: 0 })) : '0'; return `
${patternName}
${patternType}
Confidence: ${patternConfidence}%
`; }).filter(html => html.length > 0).join('') || '

No valid patterns detected

'}
`; } /** * Update connection status */ updateConnectionStatus(status, type) { const statusEl = document.getElementById('hts-connection-status'); if (statusEl) { statusEl.textContent = status; statusEl.className = `status-indicator status-${type}`; } } /** * Show error message */ showError(message) { const container = document.getElementById('hts-signal-content'); if (container) { container.innerHTML = `
${TradingIcons.risk}

${message}

`; } } /** * Start auto-analysis */ startAutoAnalysis() { this.stopAutoAnalysis(); this.autoAnalysisInterval = setInterval(async () => { if (this.ohlcvData.length >= 30) { await this.runAnalysis(); } }, 60000); // Every minute } /** * Stop auto-analysis */ stopAutoAnalysis() { if (this.autoAnalysisInterval) { clearInterval(this.autoAnalysisInterval); this.autoAnalysisInterval = null; } } } // Initialize HTS Page when DOM is ready let htsPageInstance = null; document.addEventListener('DOMContentLoaded', () => { // Only initialize if we're on the trading assistant page if (document.getElementById('hts-trading-view')) { htsPageInstance = new HTSPage(); window.htsPage = htsPageInstance; } }); // Export for module use export default HTSPage;