// Signal Calculator - orchestrates signal calculation using indicator-specific functions // Signal calculation logic is now in each indicator file import { IndicatorRegistry, getSignalFunction } from '../indicators/index.js'; /** * Calculate signal for an indicator * @param {Object} indicator - Indicator configuration * @param {Array} candles - Candle data array * @param {Object} indicatorValues - Computed indicator values for last candle * @param {Object} prevIndicatorValues - Computed indicator values for previous candle * @returns {Object} Signal object with type, strength, value, reasoning */ function calculateIndicatorSignal(indicator, candles, indicatorValues, prevIndicatorValues) { const signalFunction = getSignalFunction(indicator.type); if (!signalFunction) { console.warn('[Signals] No signal function for indicator type:', indicator.type); return null; } const lastCandle = candles[candles.length - 1]; const prevCandle = candles[candles.length - 2]; return signalFunction(indicator, lastCandle, prevCandle, indicatorValues, prevIndicatorValues); } /** * Calculate aggregate summary signal from all indicators */ export function calculateSummarySignal(signals) { console.log('[calculateSummarySignal] Input signals:', signals?.length); if (!signals || signals.length === 0) { return { signal: 'hold', strength: 0, reasoning: 'No active indicators', buyCount: 0, sellCount: 0, holdCount: 0 }; } const buySignals = signals.filter(s => s.signal === 'buy'); const sellSignals = signals.filter(s => s.signal === 'sell'); const holdSignals = signals.filter(s => s.signal === 'hold'); const buyCount = buySignals.length; const sellCount = sellSignals.length; const holdCount = holdSignals.length; const total = signals.length; console.log('[calculateSummarySignal] BUY:', buyCount, 'SELL:', sellCount, 'HOLD:', holdCount); const buyWeight = buySignals.reduce((sum, s) => sum + (s.strength || 0), 0); const sellWeight = sellSignals.reduce((sum, s) => sum + (s.strength || 0), 0); let summarySignal, strength, reasoning; if (buyCount > sellCount && buyCount > holdCount) { summarySignal = 'buy'; const avgBuyStrength = buyWeight / buyCount; strength = Math.round(avgBuyStrength * (buyCount / total)); reasoning = `${buyCount} buy signals, ${sellCount} sell, ${holdCount} hold`; } else if (sellCount > buyCount && sellCount > holdCount) { summarySignal = 'sell'; const avgSellStrength = sellWeight / sellCount; strength = Math.round(avgSellStrength * (sellCount / total)); reasoning = `${sellCount} sell signals, ${buyCount} buy, ${holdCount} hold`; } else { summarySignal = 'hold'; strength = 30; reasoning = `Mixed signals: ${buyCount} buy, ${sellCount} sell, ${holdCount} hold`; } const result = { signal: summarySignal, strength: Math.min(Math.max(strength, 0), 100), reasoning, buyCount, sellCount, holdCount, color: summarySignal === 'buy' ? '#26a69a' : summarySignal === 'sell' ? '#ef5350' : '#787b86' }; console.log('[calculateSummarySignal] Result:', result); return result; } /** * Calculate historical crossovers for all indicators based on full candle history * Finds the last time each indicator crossed from BUY to SELL or SELL to BUY */ function calculateHistoricalCrossovers(activeIndicators, candles) { activeIndicators.forEach(indicator => { const indicatorType = indicator.type || indicator.indicatorType; // Recalculate indicator values for all candles const IndicatorClass = IndicatorRegistry[indicatorType]; if (!IndicatorClass) return; const instance = new IndicatorClass(indicator); const results = instance.calculate(candles); if (!results || results.length === 0) return; // Find the most recent crossover by going backwards from the newest candle // candles are sorted oldest first, newest last let lastCrossoverTimestamp = null; let lastSignalType = null; // Get indicator-specific parameters const overbought = indicator.params?.overbought || 70; const oversold = indicator.params?.oversold || 30; for (let i = candles.length - 1; i > 0; i--) { const candle = candles[i]; // newer candle const prevCandle = candles[i-1]; // older candle const result = results[i]; const prevResult = results[i-1]; if (!result || !prevResult) continue; // Handle different indicator types if (indicatorType === 'rsi' || indicatorType === 'stoch') { // RSI/Stochastic: check crossing overbought/oversold levels const rsi = result.rsi !== undefined ? result.rsi : result; const prevRsi = prevResult.rsi !== undefined ? prevResult.rsi : prevResult; if (rsi === undefined || prevRsi === undefined) continue; // SELL: crossed down through overbought (was above, now below) if (prevRsi > overbought && rsi <= overbought) { lastCrossoverTimestamp = candle.time; lastSignalType = 'sell'; break; } // BUY: crossed up through oversold (was below, now above) if (prevRsi < oversold && rsi >= oversold) { lastCrossoverTimestamp = candle.time; lastSignalType = 'buy'; break; } } else { // MA-style: check price crossing MA const ma = result.ma !== undefined ? result.ma : result; const prevMa = prevResult.ma !== undefined ? prevResult.ma : prevResult; if (ma === undefined || prevMa === undefined) continue; // Check crossover: price was on one side of MA, now on the other side const priceAbovePrev = prevCandle.close > prevMa; const priceAboveNow = candle.close > ma; // SELL signal: price crossed from above to below MA if (priceAbovePrev && !priceAboveNow) { lastCrossoverTimestamp = candle.time; lastSignalType = 'sell'; break; } // BUY signal: price crossed from below to above MA if (!priceAbovePrev && priceAboveNow) { lastCrossoverTimestamp = candle.time; lastSignalType = 'buy'; break; } } } // Always update the timestamp based on current data // If crossover found use that time, otherwise use last candle time if (lastCrossoverTimestamp) { console.log(`[HistoricalCross] ${indicatorType}: Found ${lastSignalType} crossover at ${new Date(lastCrossoverTimestamp * 1000).toLocaleString()}`); indicator.lastSignalTimestamp = lastCrossoverTimestamp; indicator.lastSignalType = lastSignalType; } else { // No crossover found - use last candle time const lastCandleTime = candles[candles.length - 1]?.time; if (lastCandleTime) { const lastResult = results[results.length - 1]; if (indicatorType === 'rsi' || indicatorType === 'stoch') { // RSI/Stochastic: use RSI level to determine signal const rsi = lastResult?.rsi !== undefined ? lastResult.rsi : lastResult; if (rsi !== undefined) { indicator.lastSignalType = rsi > overbought ? 'sell' : (rsi < oversold ? 'buy' : null); indicator.lastSignalTimestamp = lastCandleTime; } } else { // MA-style: use price vs MA const ma = lastResult?.ma !== undefined ? lastResult.ma : lastResult; if (ma !== undefined) { const isAbove = candles[candles.length - 1].close > ma; indicator.lastSignalType = isAbove ? 'buy' : 'sell'; indicator.lastSignalTimestamp = lastCandleTime; } } } } }); } /** * Calculate signals for all active indicators * @returns {Array} Array of indicator signals */ export function calculateAllIndicatorSignals() { const activeIndicators = window.getActiveIndicators?.() || []; const candles = window.dashboard?.allData?.get(window.dashboard?.currentInterval); //console.log('[Signals] ========== calculateAllIndicatorSignals START =========='); console.log('[Signals] Active indicators:', activeIndicators.length, 'Candles:', candles?.length || 0); if (!candles || candles.length < 2) { //console.log('[Signals] Insufficient candles available:', candles?.length || 0); return []; } if (!activeIndicators || activeIndicators.length === 0) { //console.log('[Signals] No active indicators'); return []; } const signals = []; // Calculate crossovers for all indicators based on historical data calculateHistoricalCrossovers(activeIndicators, candles); for (const indicator of activeIndicators) { const IndicatorClass = IndicatorRegistry[indicator.type]; if (!IndicatorClass) { console.log('[Signals] No class for indicator type:', indicator.type); continue; } // Use cached results if available, otherwise calculate let results = indicator.cachedResults; let meta = indicator.cachedMeta; if (!results || !meta || results.length !== candles.length) { const instance = new IndicatorClass(indicator); meta = instance.getMetadata(); results = instance.calculate(candles); indicator.cachedResults = results; indicator.cachedMeta = meta; } if (!results || results.length === 0) { console.log('[Signals] No results for indicator:', indicator.type); continue; } const lastResult = results[results.length - 1]; const prevResult = results[results.length - 2]; if (lastResult === null || lastResult === undefined) { console.log('[Signals] No valid last result for indicator:', indicator.type); continue; } let values; let prevValues; if (typeof lastResult === 'object' && lastResult !== null && !Array.isArray(lastResult)) { values = lastResult; prevValues = prevResult; } else if (typeof lastResult === 'number') { values = { ma: lastResult }; prevValues = prevResult ? { ma: prevResult } : undefined; } else { console.log('[Signals] Unexpected result type for', indicator.type, ':', typeof lastResult); continue; } const signal = calculateIndicatorSignal(indicator, candles, values, prevValues); let currentSignal = signal; let lastSignalDate = indicator.lastSignalTimestamp || null; let lastSignalType = indicator.lastSignalType || null; if (!currentSignal || !currentSignal.type) { console.log('[Signals] No valid signal for', indicator.type, '- Using last signal if available'); if (lastSignalType && lastSignalDate) { currentSignal = { type: lastSignalType, strength: 50, value: candles[candles.length - 1]?.close, reasoning: `No crossover (price equals MA)` }; } else { console.log('[Signals] No previous signal available - Skipping'); continue; } } else { const currentCandleTimestamp = candles[candles.length - 1].time; if (currentSignal.type !== lastSignalType || !lastSignalType) { console.log('[Signals] Signal changed for', indicator.type, ':', lastSignalType, '->', currentSignal.type); lastSignalDate = indicator.lastSignalTimestamp || currentCandleTimestamp; lastSignalType = currentSignal.type; indicator.lastSignalTimestamp = lastSignalDate; indicator.lastSignalType = lastSignalType; } } signals.push({ id: indicator.id, name: meta?.name || indicator.type, label: indicator.type?.toUpperCase(), params: meta?.inputs && meta.inputs.length > 0 ? indicator.params[meta.inputs[0].name] : null, type: indicator.type, signal: currentSignal.type, strength: Math.round(currentSignal.strength), value: currentSignal.value, reasoning: currentSignal.reasoning, color: currentSignal.type === 'buy' ? '#26a69a' : currentSignal.type === 'sell' ? '#ef5350' : '#787b86', lastSignalDate: lastSignalDate }); } //console.log('[Signals] ========== calculateAllIndicatorSignals END =========='); console.log('[Signals] Total signals calculated:', signals.length); return signals; }