| |
| |
| |
| |
| |
| |
|
|
| class HTSEngine { |
| constructor() { |
| |
| this.baseWeights = { |
| rsiMacd: 0.40, |
| smc: 0.25, |
| patterns: 0.20, |
| sentiment: 0.10, |
| ml: 0.05 |
| }; |
|
|
| 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'; |
| this.volatility = 0; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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 |
| }; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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; |
|
|
| |
| if (rsi < 30 && macd.histogram > 0) { |
| const rsiStrength = (30 - rsi) / 30; |
| const macdStrength = Math.min(macd.histogram / (macd.macd * 0.1), 1); |
| score = (rsiStrength * 0.5 + macdStrength * 0.5) * 100; |
| signal = 'buy'; |
| confidence = Math.min(score, 100); |
| } |
| |
| else if (rsi > 70 && macd.histogram < 0) { |
| const rsiStrength = (rsi - 70) / 30; |
| 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); |
| } |
| |
| else { |
| score = 50; |
| 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) |
| } |
| }; |
| } |
|
|
| |
| |
| |
| 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); |
|
|
| |
| const orderBlocks = this.identifyOrderBlocks(ohlcvData); |
|
|
| |
| const liquidityZones = this.identifyLiquidityZones(highs, lows, closes); |
|
|
| |
| const breakerBlocks = this.identifyBreakerBlocks(ohlcvData); |
|
|
| |
| const currentPrice = closes[closes.length - 1]; |
| let smcScore = 50; |
| let smcSignal = 'hold'; |
|
|
| |
| const nearOrderBlock = orderBlocks.some(block => |
| currentPrice >= block.low && currentPrice <= block.high |
| ); |
|
|
| |
| 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 |
| } |
| }; |
| } |
|
|
| |
| |
| |
| 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); |
| } |
|
|
| |
| |
| |
| 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); |
|
|
| |
| if (closes[i] < maxHigh * 0.98) { |
| zones.push({ |
| level: maxHigh, |
| type: 'resistance', |
| strength: this.calculateZoneStrength(highs, maxHigh, i) |
| }); |
| } |
|
|
| |
| if (closes[i] > minLow * 1.02) { |
| zones.push({ |
| level: minLow, |
| type: 'support', |
| strength: this.calculateZoneStrength(lows, minLow, i) |
| }); |
| } |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| |
| |
| calculateZoneStrength(prices, level, currentIndex) { |
| let touches = 0; |
| const tolerance = level * 0.01; |
|
|
| for (let i = Math.max(0, currentIndex - 20); i < currentIndex; i++) { |
| if (Math.abs(prices[i] - level) < tolerance) { |
| touches++; |
| } |
| } |
|
|
| return touches; |
| } |
|
|
| |
| |
| |
| 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)); |
|
|
| |
| if (closes[i] > recentHigh * 1.01) { |
| breakers.push({ |
| type: 'bullish', |
| level: recentHigh, |
| index: i, |
| timestamp: ohlcvData[i].timestamp |
| }); |
| } |
|
|
| |
| if (closes[i] < recentLow * 0.99) { |
| breakers.push({ |
| type: 'bearish', |
| level: recentLow, |
| index: i, |
| timestamp: ohlcvData[i].timestamp |
| }); |
| } |
| } |
|
|
| return breakers.slice(-5); |
| } |
|
|
| |
| |
| |
| 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 |
| }; |
| } |
|
|
| |
| |
| |
| 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); |
|
|
| |
| if (closes.length >= 20) { |
| const hns = this.detectHeadAndShoulders(highs, lows); |
| if (hns) patterns.push(hns); |
| } |
|
|
| |
| const doublePattern = this.detectDoubleTopBottom(highs, lows); |
| if (doublePattern) patterns.push(doublePattern); |
|
|
| |
| const triangle = this.detectTriangle(highs, lows); |
| if (triangle) patterns.push(triangle); |
|
|
| |
| const candlestickPatterns = this.detectCandlestickPatterns(ohlcvData); |
| patterns.push(...candlestickPatterns); |
|
|
| return patterns; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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)); |
|
|
| |
| if (Math.abs(max1 - max2) / max1 < 0.02) { |
| return { |
| type: 'bearish', |
| name: 'Double Top', |
| confidence: 65 |
| }; |
| } |
|
|
| |
| if (Math.abs(min1 - min2) / min1 < 0.02) { |
| return { |
| type: 'bullish', |
| name: 'Double Bottom', |
| confidence: 65 |
| }; |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| 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); |
|
|
| |
| if (highTrend > -0.001 && lowTrend > 0.001) { |
| return { |
| type: 'bullish', |
| name: 'Ascending Triangle', |
| confidence: 60 |
| }; |
| } |
|
|
| |
| if (highTrend < 0.001 && lowTrend < -0.001) { |
| return { |
| type: 'bearish', |
| name: 'Descending Triangle', |
| confidence: 60 |
| }; |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| calculateTrend(values) { |
| if (values.length < 2) return 0; |
| return (values[values.length - 1] - values[0]) / values.length; |
| } |
|
|
| |
| |
| |
| 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]; |
|
|
| |
| 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; |
| } |
|
|
| |
| if (current.high < current.low || |
| current.high < Math.max(current.open, current.close) || |
| current.low > Math.min(current.open, current.close)) { |
| continue; |
| } |
|
|
| |
| 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 |
| }); |
| } |
|
|
| |
| if (body > 0 && upperShadow > body * 2 && lowerShadow < body * 0.5 && current.close < current.open) { |
| patterns.push({ |
| type: 'bearish', |
| name: 'Shooting Star', |
| confidence: 55 |
| }); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| |
| |
| 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)); |
| this.sentimentScore = sentimentScore; |
| return { |
| score: 50 + (sentimentScore * 50), |
| signal: sentimentScore > 0 ? 'buy' : sentimentScore < 0 ? 'sell' : 'hold', |
| confidence: Math.abs(sentimentScore) * 50, |
| sentiment: sentimentScore |
| }; |
| } |
| } else { |
| if (attempt < retries && response.status >= 500) { |
| continue; |
| } |
| 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; |
| } |
| console.warn('[HTS] Sentiment API unavailable:', error); |
| break; |
| } |
| } |
|
|
| |
| return { score: 50, signal: 'hold', confidence: 0, sentiment: 0 }; |
| } |
|
|
| |
| |
| |
| calculateMLScore(ohlcvData, rsiMacdScore, smcScore, patternScore, sentimentScore) { |
| |
| |
|
|
| 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) |
| }; |
|
|
| |
| 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 |
| }; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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'; |
|
|
| |
| 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; |
|
|
| |
| const trendStrength = this.calculateTrendStrength(ohlcvData); |
|
|
| |
| const recentHigh = Math.max(...highs.slice(-20)); |
| const recentLow = Math.min(...lows.slice(-20)); |
| const rangePercent = (avgPrice > 0) ? ((recentHigh - recentLow) / avgPrice) * 100 : 0; |
|
|
| |
| 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'; |
| } |
| } |
|
|
| |
| |
| |
| 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); |
| } |
|
|
| |
| |
| |
| adjustWeightsForMarketRegime(regime, volatility, trendStrength) { |
| |
| this.weights = { ...this.baseWeights }; |
|
|
| switch (regime) { |
| case 'trending': |
| |
| 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': |
| |
| 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': |
| |
| 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: |
| |
| break; |
| } |
|
|
| |
| if (volatility > 4) { |
| this.weights.ml = Math.min(0.10, this.baseWeights.ml * 1.50); |
| } else { |
| this.weights.ml = this.baseWeights.ml; |
| } |
|
|
| |
| 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; |
| }); |
|
|
| |
| if (this.weights.rsiMacd < 0.30) { |
| const diff = 0.30 - this.weights.rsiMacd; |
| this.weights.rsiMacd = 0.30; |
| |
| 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; |
| |
| 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); |
| } |
| }); |
| } |
| } |
|
|
| |
| |
| |
| async analyze(ohlcvData, symbol = 'BTC') { |
| if (!ohlcvData || ohlcvData.length < 30) { |
| throw new Error('Insufficient data for analysis'); |
| } |
|
|
| this.priceHistory = ohlcvData; |
|
|
| |
| this.marketRegime = this.detectMarketRegime(ohlcvData); |
| const trendStrength = this.calculateTrendStrength(ohlcvData); |
| this.adjustWeightsForMarketRegime(this.marketRegime, this.volatility, trendStrength); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| let finalSignal = 'hold'; |
| if (finalScore > 60) { |
| finalSignal = 'buy'; |
| } else if (finalScore < 40) { |
| finalSignal = 'sell'; |
| } |
|
|
| |
| 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 |
| ); |
|
|
| |
| 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; |
|
|
|
|