Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
| /** | |
| * Hybrid Trading System (HTS) Engine | |
| * Core Algorithm: RSI+MACD (40% weight) + SMC (25%) + Patterns (20%) + Sentiment (10%) + ML (5%) | |
| * | |
| * CRITICAL: RSI+MACD weight is IMMUTABLE at 40% | |
| */ | |
| class HTSEngine { | |
| constructor() { | |
| // Base weights (will be adjusted dynamically) | |
| this.baseWeights = { | |
| rsiMacd: 0.40, // Core algorithm - minimum 30%, maximum 50% | |
| smc: 0.25, // Smart Money Concepts | |
| patterns: 0.20, // Pattern Recognition | |
| sentiment: 0.10, // Sentiment Analysis | |
| ml: 0.05 // Machine Learning | |
| }; | |
| this.weights = { ...this.baseWeights }; | |
| this.rsiPeriod = 14; | |
| this.macdFast = 12; | |
| this.macdSlow = 26; | |
| this.macdSignal = 9; | |
| this.atrPeriod = 14; | |
| this.priceHistory = []; | |
| this.indicators = {}; | |
| this.smcLevels = { | |
| orderBlocks: [], | |
| liquidityZones: [], | |
| breakerBlocks: [] | |
| }; | |
| this.patterns = []; | |
| this.sentimentScore = 0; | |
| this.mlScore = 0; | |
| this.marketRegime = 'neutral'; // trending, ranging, volatile, neutral | |
| this.volatility = 0; | |
| } | |
| /** | |
| * Calculate RSI (Relative Strength Index) | |
| */ | |
| calculateRSI(prices, period = 14) { | |
| if (prices.length < period + 1) return null; | |
| const gains = []; | |
| const losses = []; | |
| for (let i = 1; i < prices.length; i++) { | |
| const change = prices[i] - prices[i - 1]; | |
| gains.push(change > 0 ? change : 0); | |
| losses.push(change < 0 ? Math.abs(change) : 0); | |
| } | |
| const avgGain = gains.slice(-period).reduce((a, b) => a + b, 0) / period; | |
| const avgLoss = losses.slice(-period).reduce((a, b) => a + b, 0) / period; | |
| if (avgLoss === 0) return 100; | |
| const rs = avgGain / avgLoss; | |
| const rsi = 100 - (100 / (1 + rs)); | |
| return rsi; | |
| } | |
| /** | |
| * Calculate EMA (Exponential Moving Average) | |
| */ | |
| calculateEMA(prices, period) { | |
| if (prices.length < period) return null; | |
| const multiplier = 2 / (period + 1); | |
| let ema = prices.slice(0, period).reduce((a, b) => a + b, 0) / period; | |
| for (let i = period; i < prices.length; i++) { | |
| ema = (prices[i] - ema) * multiplier + ema; | |
| } | |
| return ema; | |
| } | |
| /** | |
| * Calculate MACD (Moving Average Convergence Divergence) | |
| */ | |
| calculateMACD(prices) { | |
| if (prices.length < this.macdSlow + this.macdSignal) return null; | |
| const fastEMA = this.calculateEMA(prices, this.macdFast); | |
| const slowEMA = this.calculateEMA(prices, this.macdSlow); | |
| if (!fastEMA || !slowEMA) return null; | |
| const macdLine = fastEMA - slowEMA; | |
| const macdHistory = []; | |
| for (let i = this.macdSlow; i < prices.length; i++) { | |
| const fast = this.calculateEMA(prices.slice(0, i + 1), this.macdFast); | |
| const slow = this.calculateEMA(prices.slice(0, i + 1), this.macdSlow); | |
| if (fast && slow) { | |
| macdHistory.push(fast - slow); | |
| } | |
| } | |
| const signalLine = macdHistory.length >= this.macdSignal | |
| ? this.calculateEMA(macdHistory.slice(-this.macdSignal), this.macdSignal) | |
| : null; | |
| const histogram = signalLine !== null ? macdLine - signalLine : null; | |
| return { | |
| macd: macdLine, | |
| signal: signalLine, | |
| histogram: histogram, | |
| bullish: histogram !== null && histogram > 0, | |
| bearish: histogram !== null && histogram < 0 | |
| }; | |
| } | |
| /** | |
| * Calculate ATR (Average True Range) | |
| */ | |
| calculateATR(highs, lows, closes, period = 14) { | |
| if (highs.length < period + 1) return null; | |
| const trueRanges = []; | |
| for (let i = 1; i < highs.length; i++) { | |
| const tr1 = highs[i] - lows[i]; | |
| const tr2 = Math.abs(highs[i] - closes[i - 1]); | |
| const tr3 = Math.abs(lows[i] - closes[i - 1]); | |
| trueRanges.push(Math.max(tr1, tr2, tr3)); | |
| } | |
| const atr = trueRanges.slice(-period).reduce((a, b) => a + b, 0) / period; | |
| return atr; | |
| } | |
| /** | |
| * Core RSI+MACD Algorithm (40% weight - IMMUTABLE) | |
| */ | |
| calculateRSIMACDScore(ohlcvData) { | |
| if (!ohlcvData || ohlcvData.length < 30) return { score: 0, signal: 'hold', confidence: 0 }; | |
| const closes = ohlcvData.map(c => c.close); | |
| const rsi = this.calculateRSI(closes, this.rsiPeriod); | |
| const macd = this.calculateMACD(closes); | |
| if (!rsi || !macd || macd.histogram === null) { | |
| return { score: 0, signal: 'hold', confidence: 0 }; | |
| } | |
| let score = 0; | |
| let signal = 'hold'; | |
| let confidence = 0; | |
| // BUY Condition: RSI < 30 AND MACD histogram > 0 | |
| if (rsi < 30 && macd.histogram > 0) { | |
| const rsiStrength = (30 - rsi) / 30; // 0 to 1, stronger when RSI is lower | |
| const macdStrength = Math.min(macd.histogram / (macd.macd * 0.1), 1); // Normalized | |
| score = (rsiStrength * 0.5 + macdStrength * 0.5) * 100; | |
| signal = 'buy'; | |
| confidence = Math.min(score, 100); | |
| } | |
| // SELL Condition: RSI > 70 AND MACD histogram < 0 | |
| else if (rsi > 70 && macd.histogram < 0) { | |
| const rsiStrength = (rsi - 70) / 30; // 0 to 1, stronger when RSI is higher | |
| const macdStrength = Math.min(Math.abs(macd.histogram) / (Math.abs(macd.macd) * 0.1), 1); | |
| score = (rsiStrength * 0.5 + macdStrength * 0.5) * 100; | |
| signal = 'sell'; | |
| confidence = Math.min(score, 100); | |
| } | |
| // HOLD: All other conditions | |
| else { | |
| score = 50; // Neutral | |
| signal = 'hold'; | |
| confidence = 30; | |
| } | |
| return { | |
| score: score, | |
| signal: signal, | |
| confidence: confidence, | |
| rsi: rsi, | |
| macd: macd, | |
| details: { | |
| rsi: rsi.toFixed(2), | |
| macd: macd.macd.toFixed(4), | |
| signal: macd.signal ? macd.signal.toFixed(4) : 'N/A', | |
| histogram: macd.histogram.toFixed(4) | |
| } | |
| }; | |
| } | |
| /** | |
| * Smart Money Concepts (SMC) Analysis (25% weight) | |
| */ | |
| calculateSMCScore(ohlcvData) { | |
| if (!ohlcvData || ohlcvData.length < 50) return { score: 50, signal: 'hold', confidence: 0 }; | |
| const highs = ohlcvData.map(c => c.high); | |
| const lows = ohlcvData.map(c => c.low); | |
| const closes = ohlcvData.map(c => c.close); | |
| const volumes = ohlcvData.map(c => c.volume); | |
| // Identify Order Blocks (areas of high volume) | |
| const orderBlocks = this.identifyOrderBlocks(ohlcvData); | |
| // Identify Liquidity Zones (support/resistance) | |
| const liquidityZones = this.identifyLiquidityZones(highs, lows, closes); | |
| // Identify Breaker Blocks (failed support/resistance) | |
| const breakerBlocks = this.identifyBreakerBlocks(ohlcvData); | |
| // Current price position relative to SMC levels | |
| const currentPrice = closes[closes.length - 1]; | |
| let smcScore = 50; | |
| let smcSignal = 'hold'; | |
| // Check if price is near order block | |
| const nearOrderBlock = orderBlocks.some(block => | |
| currentPrice >= block.low && currentPrice <= block.high | |
| ); | |
| // Check liquidity zones | |
| const nearSupport = liquidityZones.some(zone => | |
| currentPrice >= zone.level * 0.995 && currentPrice <= zone.level * 1.005 && zone.type === 'support' | |
| ); | |
| const nearResistance = liquidityZones.some(zone => | |
| currentPrice >= zone.level * 0.995 && currentPrice <= zone.level * 1.005 && zone.type === 'resistance' | |
| ); | |
| if (nearOrderBlock && nearSupport) { | |
| smcScore = 75; | |
| smcSignal = 'buy'; | |
| } else if (nearOrderBlock && nearResistance) { | |
| smcScore = 25; | |
| smcSignal = 'sell'; | |
| } else if (nearSupport) { | |
| smcScore = 65; | |
| smcSignal = 'buy'; | |
| } else if (nearResistance) { | |
| smcScore = 35; | |
| smcSignal = 'sell'; | |
| } | |
| this.smcLevels = { | |
| orderBlocks: orderBlocks, | |
| liquidityZones: liquidityZones, | |
| breakerBlocks: breakerBlocks | |
| }; | |
| return { | |
| score: smcScore, | |
| signal: smcSignal, | |
| confidence: Math.abs(smcScore - 50) * 2, | |
| levels: { | |
| orderBlocks: orderBlocks.length, | |
| liquidityZones: liquidityZones.length, | |
| breakerBlocks: breakerBlocks.length | |
| } | |
| }; | |
| } | |
| /** | |
| * Identify Order Blocks | |
| */ | |
| identifyOrderBlocks(ohlcvData) { | |
| const blocks = []; | |
| const volumes = ohlcvData.map(c => c.volume); | |
| const avgVolume = volumes.reduce((a, b) => a + b, 0) / volumes.length; | |
| for (let i = 0; i < ohlcvData.length - 1; i++) { | |
| if (ohlcvData[i].volume > avgVolume * 1.5) { | |
| blocks.push({ | |
| index: i, | |
| high: ohlcvData[i].high, | |
| low: ohlcvData[i].low, | |
| volume: ohlcvData[i].volume, | |
| timestamp: ohlcvData[i].timestamp | |
| }); | |
| } | |
| } | |
| return blocks.slice(-10); // Last 10 order blocks | |
| } | |
| /** | |
| * Identify Liquidity Zones (Support/Resistance) | |
| */ | |
| identifyLiquidityZones(highs, lows, closes) { | |
| const zones = []; | |
| const lookback = 20; | |
| for (let i = lookback; i < closes.length; i++) { | |
| const recentHighs = highs.slice(i - lookback, i); | |
| const recentLows = lows.slice(i - lookback, i); | |
| const maxHigh = Math.max(...recentHighs); | |
| const minLow = Math.min(...recentLows); | |
| // Resistance zone | |
| if (closes[i] < maxHigh * 0.98) { | |
| zones.push({ | |
| level: maxHigh, | |
| type: 'resistance', | |
| strength: this.calculateZoneStrength(highs, maxHigh, i) | |
| }); | |
| } | |
| // Support zone | |
| if (closes[i] > minLow * 1.02) { | |
| zones.push({ | |
| level: minLow, | |
| type: 'support', | |
| strength: this.calculateZoneStrength(lows, minLow, i) | |
| }); | |
| } | |
| } | |
| // Remove duplicates and keep strongest | |
| const uniqueZones = []; | |
| const seenLevels = new Set(); | |
| zones.sort((a, b) => b.strength - a.strength); | |
| for (const zone of zones) { | |
| const key = Math.round(zone.level * 100) / 100; | |
| if (!seenLevels.has(key)) { | |
| seenLevels.add(key); | |
| uniqueZones.push(zone); | |
| } | |
| } | |
| return uniqueZones.slice(-5); // Top 5 zones | |
| } | |
| /** | |
| * Calculate zone strength | |
| */ | |
| calculateZoneStrength(prices, level, currentIndex) { | |
| let touches = 0; | |
| const tolerance = level * 0.01; // 1% tolerance | |
| for (let i = Math.max(0, currentIndex - 20); i < currentIndex; i++) { | |
| if (Math.abs(prices[i] - level) < tolerance) { | |
| touches++; | |
| } | |
| } | |
| return touches; | |
| } | |
| /** | |
| * Identify Breaker Blocks | |
| */ | |
| identifyBreakerBlocks(ohlcvData) { | |
| const breakers = []; | |
| const closes = ohlcvData.map(c => c.close); | |
| for (let i = 10; i < closes.length - 5; i++) { | |
| const recentHigh = Math.max(...closes.slice(i - 10, i)); | |
| const recentLow = Math.min(...closes.slice(i - 10, i)); | |
| // Bullish breaker (resistance broken) | |
| if (closes[i] > recentHigh * 1.01) { | |
| breakers.push({ | |
| type: 'bullish', | |
| level: recentHigh, | |
| index: i, | |
| timestamp: ohlcvData[i].timestamp | |
| }); | |
| } | |
| // Bearish breaker (support broken) | |
| if (closes[i] < recentLow * 0.99) { | |
| breakers.push({ | |
| type: 'bearish', | |
| level: recentLow, | |
| index: i, | |
| timestamp: ohlcvData[i].timestamp | |
| }); | |
| } | |
| } | |
| return breakers.slice(-5); // Last 5 breakers | |
| } | |
| /** | |
| * Pattern Recognition (20% weight) | |
| */ | |
| calculatePatternScore(ohlcvData) { | |
| if (!ohlcvData || ohlcvData.length < 20) return { score: 50, signal: 'hold', confidence: 0 }; | |
| const patterns = this.detectPatterns(ohlcvData); | |
| let patternScore = 50; | |
| let patternSignal = 'hold'; | |
| const bullishPatterns = patterns.filter(p => p.type === 'bullish').length; | |
| const bearishPatterns = patterns.filter(p => p.type === 'bearish').length; | |
| if (bullishPatterns > bearishPatterns) { | |
| patternScore = 50 + (bullishPatterns * 10); | |
| patternSignal = 'buy'; | |
| } else if (bearishPatterns > bullishPatterns) { | |
| patternScore = 50 - (bearishPatterns * 10); | |
| patternSignal = 'sell'; | |
| } | |
| this.patterns = patterns; | |
| return { | |
| score: Math.max(0, Math.min(100, patternScore)), | |
| signal: patternSignal, | |
| confidence: Math.abs(patternScore - 50) * 2, | |
| patterns: patterns.length, | |
| bullish: bullishPatterns, | |
| bearish: bearishPatterns | |
| }; | |
| } | |
| /** | |
| * Detect Trading Patterns | |
| */ | |
| detectPatterns(ohlcvData) { | |
| const patterns = []; | |
| const closes = ohlcvData.map(c => c.close); | |
| const highs = ohlcvData.map(c => c.high); | |
| const lows = ohlcvData.map(c => c.low); | |
| // Head and Shoulders | |
| if (closes.length >= 20) { | |
| const hns = this.detectHeadAndShoulders(highs, lows); | |
| if (hns) patterns.push(hns); | |
| } | |
| // Double Top/Bottom | |
| const doublePattern = this.detectDoubleTopBottom(highs, lows); | |
| if (doublePattern) patterns.push(doublePattern); | |
| // Triangle Patterns | |
| const triangle = this.detectTriangle(highs, lows); | |
| if (triangle) patterns.push(triangle); | |
| // Candlestick Patterns | |
| const candlestickPatterns = this.detectCandlestickPatterns(ohlcvData); | |
| patterns.push(...candlestickPatterns); | |
| return patterns; | |
| } | |
| /** | |
| * Detect Head and Shoulders Pattern | |
| */ | |
| detectHeadAndShoulders(highs, lows) { | |
| if (highs.length < 20) return null; | |
| const recentHighs = highs.slice(-20); | |
| const maxIndex = recentHighs.indexOf(Math.max(...recentHighs)); | |
| if (maxIndex > 5 && maxIndex < 15) { | |
| const leftShoulder = Math.max(...recentHighs.slice(0, maxIndex - 2)); | |
| const head = recentHighs[maxIndex]; | |
| const rightShoulder = Math.max(...recentHighs.slice(maxIndex + 2)); | |
| if (head > leftShoulder * 1.02 && head > rightShoulder * 1.02) { | |
| return { | |
| type: 'bearish', | |
| name: 'Head and Shoulders', | |
| confidence: 70 | |
| }; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Detect Double Top/Bottom | |
| */ | |
| detectDoubleTopBottom(highs, lows) { | |
| if (highs.length < 15) return null; | |
| const recentHighs = highs.slice(-15); | |
| const recentLows = lows.slice(-15); | |
| const max1 = Math.max(...recentHighs.slice(0, 7)); | |
| const max2 = Math.max(...recentHighs.slice(7)); | |
| const min1 = Math.min(...recentLows.slice(0, 7)); | |
| const min2 = Math.min(...recentLows.slice(7)); | |
| // Double Top | |
| if (Math.abs(max1 - max2) / max1 < 0.02) { | |
| return { | |
| type: 'bearish', | |
| name: 'Double Top', | |
| confidence: 65 | |
| }; | |
| } | |
| // Double Bottom | |
| if (Math.abs(min1 - min2) / min1 < 0.02) { | |
| return { | |
| type: 'bullish', | |
| name: 'Double Bottom', | |
| confidence: 65 | |
| }; | |
| } | |
| return null; | |
| } | |
| /** | |
| * Detect Triangle Patterns | |
| */ | |
| detectTriangle(highs, lows) { | |
| if (highs.length < 10) return null; | |
| const recentHighs = highs.slice(-10); | |
| const recentLows = lows.slice(-10); | |
| const highTrend = this.calculateTrend(recentHighs); | |
| const lowTrend = this.calculateTrend(recentLows); | |
| // Ascending Triangle | |
| if (highTrend > -0.001 && lowTrend > 0.001) { | |
| return { | |
| type: 'bullish', | |
| name: 'Ascending Triangle', | |
| confidence: 60 | |
| }; | |
| } | |
| // Descending Triangle | |
| if (highTrend < 0.001 && lowTrend < -0.001) { | |
| return { | |
| type: 'bearish', | |
| name: 'Descending Triangle', | |
| confidence: 60 | |
| }; | |
| } | |
| return null; | |
| } | |
| /** | |
| * Calculate Trend | |
| */ | |
| calculateTrend(values) { | |
| if (values.length < 2) return 0; | |
| return (values[values.length - 1] - values[0]) / values.length; | |
| } | |
| /** | |
| * Detect Candlestick Patterns | |
| */ | |
| detectCandlestickPatterns(ohlcvData) { | |
| const patterns = []; | |
| if (ohlcvData.length < 3) return patterns; | |
| for (let i = 2; i < ohlcvData.length; i++) { | |
| const current = ohlcvData[i]; | |
| const prev = ohlcvData[i - 1]; | |
| const prev2 = ohlcvData[i - 2]; | |
| // Validate candle data | |
| if (!current || !prev || !prev2 || | |
| typeof current.open !== 'number' || isNaN(current.open) || | |
| typeof current.high !== 'number' || isNaN(current.high) || | |
| typeof current.low !== 'number' || isNaN(current.low) || | |
| typeof current.close !== 'number' || isNaN(current.close) || | |
| typeof prev.open !== 'number' || isNaN(prev.open) || | |
| typeof prev.close !== 'number' || isNaN(prev.close)) { | |
| continue; // Skip invalid candles | |
| } | |
| // Validate OHLC relationships | |
| if (current.high < current.low || | |
| current.high < Math.max(current.open, current.close) || | |
| current.low > Math.min(current.open, current.close)) { | |
| continue; // Skip invalid OHLC | |
| } | |
| // Hammer (Bullish) | |
| const body = Math.abs(current.close - current.open); | |
| const lowerShadow = Math.min(current.open, current.close) - current.low; | |
| const upperShadow = current.high - Math.max(current.open, current.close); | |
| if (body > 0 && lowerShadow > body * 2 && upperShadow < body * 0.5 && current.close > current.open) { | |
| patterns.push({ | |
| type: 'bullish', | |
| name: 'Hammer', | |
| confidence: 55 | |
| }); | |
| } | |
| // Shooting Star (Bearish) | |
| if (body > 0 && upperShadow > body * 2 && lowerShadow < body * 0.5 && current.close < current.open) { | |
| patterns.push({ | |
| type: 'bearish', | |
| name: 'Shooting Star', | |
| confidence: 55 | |
| }); | |
| } | |
| // Engulfing Pattern | |
| if (prev.close < prev.open && current.close > current.open && | |
| current.open < prev.close && current.close > prev.open) { | |
| patterns.push({ | |
| type: 'bullish', | |
| name: 'Bullish Engulfing', | |
| confidence: 60 | |
| }); | |
| } | |
| if (prev.close > prev.open && current.close < current.open && | |
| current.open > prev.close && current.close < prev.open) { | |
| patterns.push({ | |
| type: 'bearish', | |
| name: 'Bearish Engulfing', | |
| confidence: 60 | |
| }); | |
| } | |
| } | |
| return patterns.slice(-5); // Last 5 patterns | |
| } | |
| /** | |
| * Sentiment Analysis (10% weight) | |
| */ | |
| async calculateSentimentScore(symbol, retries = 2) { | |
| const baseUrl = window.location.origin; | |
| const apiUrl = `${baseUrl}/api/ai/sentiment?symbol=${symbol}`; | |
| for (let attempt = 0; attempt <= retries; attempt++) { | |
| try { | |
| if (attempt > 0) { | |
| const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); | |
| await new Promise(resolve => setTimeout(resolve, delay)); | |
| } | |
| const response = await fetch(apiUrl, { | |
| method: 'GET', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| signal: AbortSignal.timeout(10000) | |
| }); | |
| if (response.ok) { | |
| const contentType = response.headers.get('content-type'); | |
| if (!contentType || !contentType.includes('application/json')) { | |
| throw new Error('Invalid response type'); | |
| } | |
| const data = await response.json(); | |
| if (!data || typeof data !== 'object') { | |
| throw new Error('Invalid response format'); | |
| } | |
| if (typeof data.sentiment_score === 'number' && !isNaN(data.sentiment_score)) { | |
| const sentimentScore = Math.max(-1, Math.min(1, data.sentiment_score)); // Clamp to -1 to 1 | |
| this.sentimentScore = sentimentScore; | |
| return { | |
| score: 50 + (sentimentScore * 50), // Convert -1 to 1 range to 0-100 | |
| signal: sentimentScore > 0 ? 'buy' : sentimentScore < 0 ? 'sell' : 'hold', | |
| confidence: Math.abs(sentimentScore) * 50, | |
| sentiment: sentimentScore | |
| }; | |
| } | |
| } else { | |
| if (attempt < retries && response.status >= 500) { | |
| continue; // Retry on server errors | |
| } | |
| console.warn(`[HTS] Sentiment API returned status ${response.status}`); | |
| } | |
| } catch (error) { | |
| if (attempt < retries && (error.name === 'AbortError' || error.message.includes('timeout') || error.message.includes('network'))) { | |
| continue; // Retry on network errors | |
| } | |
| console.warn('[HTS] Sentiment API unavailable:', error); | |
| break; // Don't retry on other errors | |
| } | |
| } | |
| // Return neutral sentiment on failure | |
| return { score: 50, signal: 'hold', confidence: 0, sentiment: 0 }; | |
| } | |
| /** | |
| * Machine Learning Score (5% weight) | |
| */ | |
| calculateMLScore(ohlcvData, rsiMacdScore, smcScore, patternScore, sentimentScore) { | |
| // Simple ML-like scoring based on ensemble of other indicators | |
| // In production, this would use a trained model | |
| const features = { | |
| rsiMacdStrength: Math.abs(rsiMacdScore.score - 50) / 50, | |
| smcStrength: Math.abs(smcScore.score - 50) / 50, | |
| patternStrength: Math.abs(patternScore.score - 50) / 50, | |
| sentimentStrength: Math.abs(sentimentScore.score - 50) / 50, | |
| volumeTrend: this.calculateVolumeTrend(ohlcvData), | |
| priceMomentum: this.calculatePriceMomentum(ohlcvData) | |
| }; | |
| // Weighted ensemble | |
| const mlScore = 50 + ( | |
| features.rsiMacdStrength * 20 + | |
| features.smcStrength * 15 + | |
| features.patternStrength * 10 + | |
| features.sentimentStrength * 5 + | |
| features.volumeTrend * 5 + | |
| features.priceMomentum * 5 | |
| ); | |
| this.mlScore = mlScore; | |
| return { | |
| score: Math.max(0, Math.min(100, mlScore)), | |
| signal: mlScore > 55 ? 'buy' : mlScore < 45 ? 'sell' : 'hold', | |
| confidence: Math.abs(mlScore - 50) * 2, | |
| features: features | |
| }; | |
| } | |
| /** | |
| * Calculate Volume Trend | |
| */ | |
| calculateVolumeTrend(ohlcvData) { | |
| if (ohlcvData.length < 10) return 0; | |
| const volumes = ohlcvData.map(c => c.volume); | |
| const recentAvg = volumes.slice(-5).reduce((a, b) => a + b, 0) / 5; | |
| const olderAvg = volumes.slice(-10, -5).reduce((a, b) => a + b, 0) / 5; | |
| return (recentAvg - olderAvg) / olderAvg; // Percentage change | |
| } | |
| /** | |
| * Calculate Price Momentum | |
| */ | |
| calculatePriceMomentum(ohlcvData) { | |
| if (ohlcvData.length < 10) return 0; | |
| const closes = ohlcvData.map(c => c.close); | |
| const recent = closes.slice(-5).reduce((a, b) => a + b, 0) / 5; | |
| const older = closes.slice(-10, -5).reduce((a, b) => a + b, 0) / 5; | |
| return (recent - older) / older; // Percentage change | |
| } | |
| /** | |
| * Detect Market Regime (Trending, Ranging, Volatile, Neutral) | |
| */ | |
| detectMarketRegime(ohlcvData) { | |
| if (!ohlcvData || !Array.isArray(ohlcvData) || ohlcvData.length < 50) return 'neutral'; | |
| const closes = ohlcvData | |
| .map(c => (c && typeof c.close === 'number' && !isNaN(c.close) && c.close > 0) ? c.close : null) | |
| .filter(c => c !== null); | |
| const highs = ohlcvData | |
| .map(c => (c && typeof c.high === 'number' && !isNaN(c.high) && c.high > 0) ? c.high : null) | |
| .filter(h => h !== null); | |
| const lows = ohlcvData | |
| .map(c => (c && typeof c.low === 'number' && !isNaN(c.low) && c.low > 0) ? c.low : null) | |
| .filter(l => l !== null); | |
| if (closes.length < 20 || highs.length < 20 || lows.length < 20) return 'neutral'; | |
| // Calculate volatility (ATR normalized) | |
| const atr = this.calculateATR(highs, lows, closes, this.atrPeriod); | |
| const avgPrice = closes.slice(-20).reduce((a, b) => a + b, 0) / 20; | |
| this.volatility = (atr && avgPrice > 0) ? (atr / avgPrice) * 100 : 0; | |
| // Calculate trend strength using ADX-like logic | |
| const trendStrength = this.calculateTrendStrength(ohlcvData); | |
| // Calculate price range (for ranging detection) | |
| const recentHigh = Math.max(...highs.slice(-20)); | |
| const recentLow = Math.min(...lows.slice(-20)); | |
| const rangePercent = (avgPrice > 0) ? ((recentHigh - recentLow) / avgPrice) * 100 : 0; | |
| // Determine regime | |
| if (this.volatility > 5 && trendStrength > 60) { | |
| return 'volatile-trending'; | |
| } else if (this.volatility > 5) { | |
| return 'volatile'; | |
| } else if (trendStrength > 60) { | |
| return 'trending'; | |
| } else if (rangePercent < 3 && trendStrength < 30) { | |
| return 'ranging'; | |
| } else { | |
| return 'neutral'; | |
| } | |
| } | |
| /** | |
| * Calculate Trend Strength (ADX-like) | |
| */ | |
| calculateTrendStrength(ohlcvData) { | |
| if (ohlcvData.length < 14) return 0; | |
| const closes = ohlcvData.map(c => c.close); | |
| const highs = ohlcvData.map(c => c.high); | |
| const lows = ohlcvData.map(c => c.low); | |
| let plusDM = 0; | |
| let minusDM = 0; | |
| for (let i = 1; i < closes.length; i++) { | |
| const highDiff = highs[i] - highs[i - 1]; | |
| const lowDiff = lows[i - 1] - lows[i]; | |
| if (highDiff > lowDiff && highDiff > 0) { | |
| plusDM += highDiff; | |
| } else if (lowDiff > highDiff && lowDiff > 0) { | |
| minusDM += lowDiff; | |
| } | |
| } | |
| const totalDM = plusDM + minusDM; | |
| if (totalDM === 0) return 0; | |
| const dx = Math.abs(plusDM - minusDM) / totalDM * 100; | |
| return Math.min(100, dx); | |
| } | |
| /** | |
| * Adjust weights dynamically based on market regime | |
| */ | |
| adjustWeightsForMarketRegime(regime, volatility, trendStrength) { | |
| // Reset to base weights | |
| this.weights = { ...this.baseWeights }; | |
| switch (regime) { | |
| case 'trending': | |
| // In trending markets, increase RSI+MACD and SMC weights | |
| this.weights.rsiMacd = Math.min(0.50, this.baseWeights.rsiMacd * 1.15); | |
| this.weights.smc = Math.min(0.30, this.baseWeights.smc * 1.20); | |
| this.weights.patterns = this.baseWeights.patterns * 0.90; | |
| this.weights.sentiment = this.baseWeights.sentiment * 0.85; | |
| break; | |
| case 'ranging': | |
| // In ranging markets, increase pattern recognition | |
| this.weights.rsiMacd = Math.max(0.30, this.baseWeights.rsiMacd * 0.85); | |
| this.weights.patterns = Math.min(0.30, this.baseWeights.patterns * 1.30); | |
| this.weights.smc = this.baseWeights.smc * 1.10; | |
| this.weights.sentiment = this.baseWeights.sentiment * 0.90; | |
| break; | |
| case 'volatile': | |
| case 'volatile-trending': | |
| // In volatile markets, increase SMC and sentiment | |
| this.weights.rsiMacd = Math.max(0.30, this.baseWeights.rsiMacd * 0.90); | |
| this.weights.smc = Math.min(0.35, this.baseWeights.smc * 1.40); | |
| this.weights.sentiment = Math.min(0.20, this.baseWeights.sentiment * 2.00); | |
| this.weights.patterns = this.baseWeights.patterns * 0.80; | |
| break; | |
| case 'neutral': | |
| default: | |
| // Keep base weights | |
| break; | |
| } | |
| // Adjust ML weight based on volatility (higher volatility = more ML) | |
| if (volatility > 4) { | |
| this.weights.ml = Math.min(0.10, this.baseWeights.ml * 1.50); | |
| } else { | |
| this.weights.ml = this.baseWeights.ml; | |
| } | |
| // Normalize weights to sum to 1.0 | |
| const total = Object.values(this.weights).reduce((a, b) => a + b, 0); | |
| Object.keys(this.weights).forEach(key => { | |
| this.weights[key] = this.weights[key] / total; | |
| }); | |
| // Ensure RSI+MACD stays within bounds (30% - 50%) | |
| if (this.weights.rsiMacd < 0.30) { | |
| const diff = 0.30 - this.weights.rsiMacd; | |
| this.weights.rsiMacd = 0.30; | |
| // Redistribute difference proportionally | |
| const otherTotal = 1.0 - this.weights.rsiMacd; | |
| Object.keys(this.weights).forEach(key => { | |
| if (key !== 'rsiMacd') { | |
| this.weights[key] = (this.weights[key] / otherTotal) * (1.0 - this.weights.rsiMacd); | |
| } | |
| }); | |
| } else if (this.weights.rsiMacd > 0.50) { | |
| const diff = this.weights.rsiMacd - 0.50; | |
| this.weights.rsiMacd = 0.50; | |
| // Redistribute difference proportionally | |
| const otherTotal = 1.0 - this.weights.rsiMacd; | |
| Object.keys(this.weights).forEach(key => { | |
| if (key !== 'rsiMacd') { | |
| this.weights[key] = (this.weights[key] / otherTotal) * (1.0 - this.weights.rsiMacd); | |
| } | |
| }); | |
| } | |
| } | |
| /** | |
| * Main Analysis Function - Combines all components with dynamic weight adjustment | |
| */ | |
| async analyze(ohlcvData, symbol = 'BTC') { | |
| if (!ohlcvData || ohlcvData.length < 30) { | |
| throw new Error('Insufficient data for analysis'); | |
| } | |
| this.priceHistory = ohlcvData; | |
| // Detect market regime and adjust weights dynamically | |
| this.marketRegime = this.detectMarketRegime(ohlcvData); | |
| const trendStrength = this.calculateTrendStrength(ohlcvData); | |
| this.adjustWeightsForMarketRegime(this.marketRegime, this.volatility, trendStrength); | |
| // Calculate all components | |
| const rsiMacdResult = this.calculateRSIMACDScore(ohlcvData); | |
| const smcResult = this.calculateSMCScore(ohlcvData); | |
| const patternResult = this.calculatePatternScore(ohlcvData); | |
| const sentimentResult = await this.calculateSentimentScore(symbol); | |
| const mlResult = this.calculateMLScore(ohlcvData, rsiMacdResult, smcResult, patternResult, sentimentResult); | |
| // Calculate final weighted score with dynamic weights | |
| const finalScore = | |
| (rsiMacdResult.score * this.weights.rsiMacd) + | |
| (smcResult.score * this.weights.smc) + | |
| (patternResult.score * this.weights.patterns) + | |
| (sentimentResult.score * this.weights.sentiment) + | |
| (mlResult.score * this.weights.ml); | |
| // Determine final signal | |
| let finalSignal = 'hold'; | |
| if (finalScore > 60) { | |
| finalSignal = 'buy'; | |
| } else if (finalScore < 40) { | |
| finalSignal = 'sell'; | |
| } | |
| // Calculate overall confidence | |
| const confidence = ( | |
| rsiMacdResult.confidence * this.weights.rsiMacd + | |
| smcResult.confidence * this.weights.smc + | |
| patternResult.confidence * this.weights.patterns + | |
| sentimentResult.confidence * this.weights.sentiment + | |
| mlResult.confidence * this.weights.ml | |
| ); | |
| // Calculate risk/reward | |
| const currentPrice = ohlcvData[ohlcvData.length - 1].close; | |
| const atr = this.calculateATR( | |
| ohlcvData.map(c => c.high), | |
| ohlcvData.map(c => c.low), | |
| ohlcvData.map(c => c.close) | |
| ); | |
| const stopLoss = finalSignal === 'buy' | |
| ? currentPrice - (atr * 2) | |
| : currentPrice + (atr * 2); | |
| const takeProfit1 = finalSignal === 'buy' | |
| ? currentPrice + (atr * 1.5) | |
| : currentPrice - (atr * 1.5); | |
| const takeProfit2 = finalSignal === 'buy' | |
| ? currentPrice + (atr * 2.5) | |
| : currentPrice - (atr * 2.5); | |
| const takeProfit3 = finalSignal === 'buy' | |
| ? currentPrice + (atr * 4) | |
| : currentPrice - (atr * 4); | |
| const riskReward = atr ? Math.abs(takeProfit1 - currentPrice) / Math.abs(stopLoss - currentPrice) : 0; | |
| return { | |
| finalScore: finalScore, | |
| finalSignal: finalSignal, | |
| confidence: Math.min(100, confidence), | |
| currentPrice: currentPrice, | |
| stopLoss: stopLoss, | |
| takeProfitLevels: [ | |
| { level: takeProfit1, type: 'TP1', riskReward: riskReward }, | |
| { level: takeProfit2, type: 'TP2', riskReward: riskReward * 1.67 }, | |
| { level: takeProfit3, type: 'TP3', riskReward: riskReward * 2.67 } | |
| ], | |
| riskReward: riskReward, | |
| components: { | |
| rsiMacd: { | |
| score: rsiMacdResult.score, | |
| signal: rsiMacdResult.signal, | |
| confidence: rsiMacdResult.confidence, | |
| weight: this.weights.rsiMacd, | |
| details: rsiMacdResult.details | |
| }, | |
| smc: { | |
| score: smcResult.score, | |
| signal: smcResult.signal, | |
| confidence: smcResult.confidence, | |
| weight: this.weights.smc, | |
| levels: smcResult.levels | |
| }, | |
| patterns: { | |
| score: patternResult.score, | |
| signal: patternResult.signal, | |
| confidence: patternResult.confidence, | |
| weight: this.weights.patterns, | |
| detected: patternResult.patterns, | |
| bullish: patternResult.bullish, | |
| bearish: patternResult.bearish | |
| }, | |
| sentiment: { | |
| score: sentimentResult.score, | |
| signal: sentimentResult.signal, | |
| confidence: sentimentResult.confidence, | |
| weight: this.weights.sentiment, | |
| sentiment: sentimentResult.sentiment | |
| }, | |
| ml: { | |
| score: mlResult.score, | |
| signal: mlResult.signal, | |
| confidence: mlResult.confidence, | |
| weight: this.weights.ml, | |
| features: mlResult.features | |
| } | |
| }, | |
| indicators: { | |
| rsi: rsiMacdResult.rsi, | |
| macd: rsiMacdResult.macd, | |
| atr: atr | |
| }, | |
| smcLevels: this.smcLevels, | |
| patterns: this.patterns | |
| }; | |
| } | |
| } | |
| export default HTSEngine; | |