Files
winterfail/js/ui/signals-calculator.js

367 lines
16 KiB
JavaScript

// 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 (use cache if valid)
let results = indicator.cachedResults;
if (!results || !Array.isArray(results) || results.length !== candles.length) {
const IndicatorClass = IndicatorRegistry[indicatorType];
if (!IndicatorClass) return;
const instance = new IndicatorClass(indicator);
results = instance.calculate(candles);
// Don't save back to cache here, let drawIndicatorsOnChart be the source of truth for cache
}
if (!results || !Array.isArray(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 if (indicatorType === 'hurst') {
// Hurst Bands: check price crossing bands
const upper = result.upper;
const lower = result.lower;
const prevUpper = prevResult.upper;
const prevLower = prevResult.lower;
if (upper === undefined || lower === undefined ||
prevUpper === undefined || prevLower === undefined) continue;
// BUY: price crossed down below lower band
if (prevCandle.close > prevLower && candle.close < lower) {
lastCrossoverTimestamp = candle.time;
lastSignalType = 'buy';
break;
}
// SELL: price crossed down below upper band
if (prevCandle.close > prevUpper && candle.close < upper) {
lastCrossoverTimestamp = candle.time;
lastSignalType = 'sell';
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 if (indicatorType === 'hurst') {
// Hurst Bands: use price vs bands
const upper = lastResult?.upper;
const lower = lastResult?.lower;
const currentPrice = candles[candles.length - 1]?.close;
if (upper !== undefined && lower !== undefined && currentPrice !== undefined) {
if (currentPrice < lower) {
indicator.lastSignalType = 'buy';
} else if (currentPrice > upper) {
indicator.lastSignalType = 'sell';
} else {
indicator.lastSignalType = 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 || !Array.isArray(results) || results.length !== candles.length) {
const instance = new IndicatorClass(indicator);
meta = instance.getMetadata();
results = instance.calculate(candles);
indicator.cachedResults = results;
indicator.cachedMeta = meta;
}
if (!results || !Array.isArray(results) || results.length === 0) {
console.log('[Signals] No valid 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;
}