Files
btc-trading/src/api/dashboard/static/js/ui/signals-calculator.js

270 lines
11 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 a single indicator using its signal function
* @param {Object} indicator - Indicator object with type, params, etc.
* @param {Array} candles - Recent candle data
* @param {Object} indicatorValues - Computed indicator values for last candle
* @returns {Object} Signal object with type, strength, value, reasoning
*/
function calculateIndicatorSignal(indicator, candles, indicatorValues) {
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);
}
/**
* 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;
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;
// Get MA value (handle both object and number formats)
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;
break;
}
// BUY signal: price crossed from below to above MA
if (!priceAbovePrev && priceAboveNow) {
lastCrossoverTimestamp = candle.time;
break;
}
}
if (lastCrossoverTimestamp) {
console.log(`[HistoricalCross] ${indicatorType}: Found crossover at ${new Date(lastCrossoverTimestamp * 1000).toLocaleString()}`);
indicator.lastSignalTimestamp = lastCrossoverTimestamp;
}
});
}
/**
* 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];
if (lastResult === null || lastResult === undefined) {
console.log('[Signals] No valid last result for indicator:', indicator.type);
continue;
}
let values;
if (typeof lastResult === 'object' && lastResult !== null && !Array.isArray(lastResult)) {
values = lastResult;
} else if (typeof lastResult === 'number') {
values = { ma: lastResult };
} else {
console.log('[Signals] Unexpected result type for', indicator.type, ':', typeof lastResult);
continue;
}
const signal = calculateIndicatorSignal(indicator, candles, values);
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: indicator.params && typeof indicator.params === 'object'
? Object.entries(indicator.params)
.filter(([k, v]) => !k.startsWith('_') && v !== undefined && v !== null)
.map(([k, v]) => `${k}=${v}`)
.join(', ')
: 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;
}