Files
btc-trading/src/api/dashboard/static/js/ui/signals-calculator.js
DiTus a344a7f0da Refactor: Convert indicators to self-contained files
- Created moving_average.js consolidating ma.js, ma_indicator.js, sma.js, ema.js
- Made all indicators self-contained with embedded:
  * Math logic (no external dependencies)
  * Metadata (getMetadata())
  * Signal calculation (calculateXXXSignal)
  * Base class (inline BaseIndicator)
- Updated macd.js, hts.js to inline EMA/MA calculations
- Added signal functions to RSI, BB, Stochastic, ATR indicators
- Updated indicators/index.js to export both classes and signal functions
- Simplified signals-calculator.js to orchestrate using indicator signal functions
- Removed obsolete files: ma.js, base.js, ma_indicator.js, sma.js, ema.js

All indicators now fully self-contained with no external file dependencies
for math, signal calculation, or base class.
2026-03-01 19:39:28 +01:00

145 lines
5.9 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 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 = [];
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 = 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;
}