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.
This commit is contained in:
@ -1,330 +1,27 @@
|
||||
// Signal Calculator for Technical Indicators
|
||||
// Calculates buy/hold/sell signals for all active indicators
|
||||
// Signal Calculator - orchestrates signal calculation using indicator-specific functions
|
||||
// Signal calculation logic is now in each indicator file
|
||||
|
||||
import { IndicatorRegistry as IR } from '../indicators/index.js';
|
||||
|
||||
const SIGNAL_TYPES = {
|
||||
BUY: 'buy',
|
||||
SELL: 'sell',
|
||||
HOLD: 'hold'
|
||||
};
|
||||
|
||||
const SIGNAL_COLORS = {
|
||||
buy: '#26a69a',
|
||||
hold: '#787b86',
|
||||
sell: '#ef5350'
|
||||
};
|
||||
import { IndicatorRegistry, getSignalFunction } from '../indicators/index.js';
|
||||
|
||||
/**
|
||||
* Calculate signal for a single indicator
|
||||
* 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 lastCandle = candles[candles.length - 1];
|
||||
const prevCandle = candles[candles.length - 2];
|
||||
const signalFunction = getSignalFunction(indicator.type);
|
||||
|
||||
console.log('[calculateIndicatorSignal] Type:', indicator.type, 'Values:', indicatorValues, 'LastCandle:', lastCandle?.close);
|
||||
|
||||
if (!lastCandle) {
|
||||
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No data' };
|
||||
}
|
||||
|
||||
switch (indicator.type) {
|
||||
case 'sma':
|
||||
case 'ema':
|
||||
case 'ma':
|
||||
return calculateMASignal(indicator, lastCandle, prevCandle, indicatorValues);
|
||||
case 'rsi':
|
||||
return calculateRSISignal(indicator, lastCandle, indicatorValues);
|
||||
case 'macd':
|
||||
return calculateMACDSignal(indicator, lastCandle, prevCandle, indicatorValues);
|
||||
case 'stoch':
|
||||
return calculateStochSignal(indicator, lastCandle, prevCandle, indicatorValues);
|
||||
case 'bb':
|
||||
return calculateBollingerBandsSignal(indicator, lastCandle, indicatorValues);
|
||||
case 'sma':
|
||||
case 'ema':
|
||||
return calculateMASignal(indicator, lastCandle, prevCandle, indicatorValues);
|
||||
case 'atr':
|
||||
return calculateATRSignal(indicator, indicatorValues);
|
||||
case 'hts':
|
||||
return calculateHTSSignal(indicator, lastCandle, prevCandle, indicatorValues);
|
||||
default:
|
||||
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'Unknown indicator type' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RSI Signal Calculation
|
||||
*/
|
||||
function calculateRSISignal(indicator, lastCandle, indicatorValues) {
|
||||
const rsi = indicatorValues?.rsi;
|
||||
const overbought = indicator.params.overbought || 70;
|
||||
const oversold = indicator.params.oversold || 30;
|
||||
|
||||
if (rsi === null || rsi === undefined) {
|
||||
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No RSI data' };
|
||||
}
|
||||
|
||||
let signal, strength, reasoning;
|
||||
|
||||
if (rsi <= oversold) {
|
||||
signal = SIGNAL_TYPES.BUY;
|
||||
strength = 80 + Math.min((oversold - rsi) * 0.5, 20);
|
||||
reasoning = `RSI ${rsi.toFixed(1)} is extremely oversold (${oversold}), suggesting the price may be approaching a bottom and potential rebound`;
|
||||
} else if (rsi >= overbought) {
|
||||
signal = SIGNAL_TYPES.SELL;
|
||||
strength = 80 + Math.min((rsi - overbought) * 0.5, 20);
|
||||
reasoning = `RSI ${rsi.toFixed(1)} is overbought (${overbought}), indicating the asset may be overvalued due for a correction`;
|
||||
} else if (rsi < 50) {
|
||||
signal = SIGNAL_TYPES.HOLD;
|
||||
strength = 30;
|
||||
reasoning = `RSI ${rsi.toFixed(1)} shows bearish momentum below 50, sellers currently in control`;
|
||||
} else if (rsi > 50) {
|
||||
signal = SIGNAL_TYPES.HOLD;
|
||||
strength = 30;
|
||||
reasoning = `RSI ${rsi.toFixed(1)} shows bullish momentum above 50, buyers currently in control`;
|
||||
} else {
|
||||
signal = SIGNAL_TYPES.HOLD;
|
||||
strength = 0;
|
||||
reasoning = 'RSI at 50 indicates neutral market conditions with balanced buying/selling pressure';
|
||||
}
|
||||
|
||||
return { type: signal, strength, value: rsi, reasoning };
|
||||
}
|
||||
|
||||
/**
|
||||
* MACD Signal Calculation
|
||||
*/
|
||||
function calculateMACDSignal(indicator, lastCandle, prevCandle, values) {
|
||||
const macd = values?.macd;
|
||||
const signalLine = values?.signal;
|
||||
const histogram = values?.histogram;
|
||||
|
||||
if (macd === null || signalLine === null) {
|
||||
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No MACD data' };
|
||||
}
|
||||
|
||||
let macdSignal, strength, reasoning;
|
||||
|
||||
if (macd > signalLine && histogram > 0) {
|
||||
macdSignal = SIGNAL_TYPES.BUY;
|
||||
strength = 75 + Math.min((macd - signalLine) * 10, 25);
|
||||
reasoning = `MACD (${macd.toFixed(2)}) is above signal line (${signalLine.toFixed(2)}) with positive histogram (${histogram.toFixed(2)}), indicating strong bullish momentum`;
|
||||
} else if (macd < signalLine && histogram < 0) {
|
||||
macdSignal = SIGNAL_TYPES.SELL;
|
||||
strength = 75 + Math.min((signalLine - macd) * 10, 25);
|
||||
reasoning = `MACD (${macd.toFixed(2)}) is below signal line (${signalLine.toFixed(2)}) with negative histogram (${histogram.toFixed(2)}), indicating strong bearish momentum`;
|
||||
} else if (macd > 0 && signalLine < 0) {
|
||||
macdSignal = SIGNAL_TYPES.BUY;
|
||||
strength = 85;
|
||||
reasoning = `Bullish crossover: MACD (${macd.toFixed(2)}) crossed above zero while signal (${signalLine.toFixed(2)}) is still negative, potential trend reversal upward`;
|
||||
} else if (macd < 0 && signalLine > 0) {
|
||||
macdSignal = SIGNAL_TYPES.SELL;
|
||||
strength = 85;
|
||||
reasoning = `Bearish crossover: MACD (${macd.toFixed(2)}) crossed below zero while signal (${signalLine.toFixed(2)}) is still positive, potential trend reversal downward`;
|
||||
} else {
|
||||
macdSignal = SIGNAL_TYPES.HOLD;
|
||||
strength = 30;
|
||||
reasoning = `MACD (${macd.toFixed(2)}) and signal (${signalLine.toFixed(2)}) are close together with no clear directional bias`;
|
||||
}
|
||||
|
||||
return { type: macdSignal, strength, value: histogram, reasoning };
|
||||
}
|
||||
|
||||
/**
|
||||
* Stochastic Signal Calculation
|
||||
*/
|
||||
function calculateStochSignal(indicator, lastCandle, prevCandle, values) {
|
||||
const k = values?.k;
|
||||
const d = values?.d;
|
||||
|
||||
if (k === null || d === null) {
|
||||
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No Stochastic data' };
|
||||
}
|
||||
|
||||
const prevK = prevCandle?.values?.k;
|
||||
|
||||
let signalType, strength, reasoning;
|
||||
|
||||
if (k < 20 && prevK < 20 && k > d) {
|
||||
signalType = SIGNAL_TYPES.BUY;
|
||||
strength = 80;
|
||||
reasoning = `Strong buy signal: %K (${k.toFixed(1)}) crossed above %D (${d.toFixed(1)}) in oversold territory (<20), likely upward reversal`;
|
||||
} else if (k > 80 && prevK > 80 && k < d) {
|
||||
signalType = SIGNAL_TYPES.SELL;
|
||||
strength = 80;
|
||||
reasoning = `Strong sell signal: %K (${k.toFixed(1)}) crossed below %D (${d.toFixed(1)}) in overbought territory (>80), likely downward reversal`;
|
||||
} else if (k < 20) {
|
||||
signalType = SIGNAL_TYPES.BUY;
|
||||
strength = 60;
|
||||
reasoning = `%K (${k.toFixed(1)}) is in oversold zone (<20), price may be near a bottom and ready to bounce`;
|
||||
} else if (k > 80) {
|
||||
signalType = SIGNAL_TYPES.SELL;
|
||||
strength = 60;
|
||||
reasoning = `%K (${k.toFixed(1)}) is in overbought zone (>80), price may be overextended and ready for correction`;
|
||||
} else {
|
||||
signalType = SIGNAL_TYPES.HOLD;
|
||||
strength = 30;
|
||||
reasoning = `Stochastic (${k.toFixed(1)}) is in neutral range (20-80) with no clear directional signal`;
|
||||
}
|
||||
|
||||
return { type: signalType, strength, value: k, reasoning };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bollinger Bands Signal Calculation
|
||||
*/
|
||||
function calculateBollingerBandsSignal(indicator, lastCandle, values) {
|
||||
const upper = values?.upper;
|
||||
const lower = values?.lower;
|
||||
const middle = values?.middle;
|
||||
|
||||
if (!upper || !lower || !middle) {
|
||||
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No BB data' };
|
||||
}
|
||||
|
||||
const price = lastCandle.close;
|
||||
const range = upper - lower;
|
||||
const position = (price - lower) / range;
|
||||
|
||||
let signalType, strength, reasoning;
|
||||
|
||||
if (position <= 0.1 || price <= lower) {
|
||||
signalType = SIGNAL_TYPES.BUY;
|
||||
strength = Math.floor(70 + (0.1 - position) * 300);
|
||||
reasoning = `Price (${price.toFixed(2)}) is at or touching the lower Bollinger Band (${lower.toFixed(2)}), potential oversold bounce opportunity`;
|
||||
} else if (position >= 0.9 || price >= upper) {
|
||||
signalType = SIGNAL_TYPES.SELL;
|
||||
strength = Math.floor(70 + (position - 0.9) * 300);
|
||||
reasoning = `Price (${price.toFixed(2)}) is at or touching the upper Bollinger Band (${upper.toFixed(2)}), potential overextended sell signal`;
|
||||
} else if (middle && price > middle) {
|
||||
signalType = SIGNAL_TYPES.HOLD;
|
||||
strength = 40;
|
||||
reasoning = `Price (${price.toFixed(2)}) is above the middle band (${middle.toFixed(2)}), generally bullish but not extreme`;
|
||||
} else {
|
||||
signalType = SIGNAL_TYPES.HOLD;
|
||||
strength = 40;
|
||||
reasoning = `Price (${price.toFixed(2)}) is within normal Bollinger Band range, no extreme signals`;
|
||||
}
|
||||
|
||||
strength = Math.min(Math.max(strength, 0), 100);
|
||||
|
||||
return { type: signalType, strength, value: position * 100, reasoning };
|
||||
}
|
||||
|
||||
/**
|
||||
* Moving Average Signal Calculation (SMA/EMA)
|
||||
*/
|
||||
function calculateMASignal(indicator, lastCandle, prevCandle, values) {
|
||||
const close = lastCandle.close;
|
||||
const ma = values?.ma;
|
||||
|
||||
console.log('[calculateMASignal] values:', values, 'ma:', ma, 'close:', close);
|
||||
|
||||
if (!ma && ma !== 0) {
|
||||
console.log('[calculateMASignal] No valid MA value');
|
||||
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No MA data' };
|
||||
}
|
||||
|
||||
const prevClose = prevCandle?.close;
|
||||
const period = indicator.params?.period || 44;
|
||||
const maLabel = indicator.name || `MA (${period})`;
|
||||
|
||||
let signalType, strength, reasoning;
|
||||
|
||||
if (close > ma) {
|
||||
signalType = SIGNAL_TYPES.BUY;
|
||||
strength = Math.min(60 + ((close - ma) / ma) * 500, 100);
|
||||
reasoning = `Price (${close.toFixed(2)}) is above ${maLabel} (${ma.toFixed(2)})`;
|
||||
} else if (close < ma) {
|
||||
signalType = SIGNAL_TYPES.SELL;
|
||||
strength = Math.min(60 + ((ma - close) / ma) * 500, 100);
|
||||
reasoning = `Price (${close.toFixed(2)}) is below ${maLabel} (${ma.toFixed(2)})`;
|
||||
} else {
|
||||
if (!signalFunction) {
|
||||
console.warn('[Signals] No signal function for indicator type:', indicator.type);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('[calculateMASignal] Result:', signalType, strength);
|
||||
return { type: signalType, strength, value: close, reasoning };
|
||||
}
|
||||
|
||||
/**
|
||||
* ATR Signal Calculation
|
||||
*/
|
||||
function calculateATRSignal(indicator, values) {
|
||||
const atr = values?.atr;
|
||||
const lastCandle = candles[candles.length - 1];
|
||||
const prevCandle = candles[candles.length - 2];
|
||||
|
||||
if (!atr) {
|
||||
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No ATR data' };
|
||||
}
|
||||
|
||||
const period = indicator.params?.period || 14;
|
||||
|
||||
// ATR is volatility indicator, used with other signals
|
||||
let signalType, strength, reasoning;
|
||||
|
||||
if (atr > 0) {
|
||||
signalType = SIGNAL_TYPES.HOLD;
|
||||
strength = Math.min(atr * 10, 100);
|
||||
|
||||
if (atr > 100) {
|
||||
reasoning = `High volatility detected (ATR: ${atr.toFixed(2)}), expect larger moves and wider stop-losses`;
|
||||
} else if (atr > 50) {
|
||||
reasoning = `Moderate volatility (ATR: ${atr.toFixed(2)}), normal market conditions`;
|
||||
} else {
|
||||
reasoning = `Low volatility (ATR: ${atr.toFixed(2)}), market may be consolidating`;
|
||||
}
|
||||
} else {
|
||||
signalType = SIGNAL_TYPES.HOLD;
|
||||
strength = 0;
|
||||
reasoning = 'No volatility data available';
|
||||
}
|
||||
|
||||
return { type: signalType, strength, value: atr, reasoning };
|
||||
}
|
||||
|
||||
/**
|
||||
* HTS (Hull Trend System) Signal Calculation
|
||||
*/
|
||||
function calculateHTSSignal(indicator, lastCandle, prevCandle, values) {
|
||||
const fastHigh = values?.fastHigh;
|
||||
const fastLow = values?.fastLow;
|
||||
const slowHigh = values?.slowHigh;
|
||||
const slowLow = values?.slowLow;
|
||||
|
||||
if (!fastHigh || !slowLow) {
|
||||
return { type: SIGNAL_TYPES.HOLD, strength: 0, value: null, reasoning: 'No HTS data' };
|
||||
}
|
||||
|
||||
const price = lastCandle.close;
|
||||
const midpointLow = (slowHigh[slowHigh.length - 1] + slowLow[slowLow.length - 1]) / 2;
|
||||
const midpointHigh = (fastHigh[fastHigh.length - 1] + fastLow[fastLow.length - 1]) / 2;
|
||||
|
||||
let signalType, strength, reasoning;
|
||||
|
||||
if (price > midpointHigh) {
|
||||
signalType = SIGNAL_TYPES.BUY;
|
||||
strength = Math.min(50 + ((price - midpointHigh) / midpointHigh) * 200, 100);
|
||||
reasoning = `Price (${price.toFixed(2)}) is above the fast channel (${midpointHigh.toFixed(2)}), strong bullish trend in place`;
|
||||
} else if (price < midpointLow) {
|
||||
signalType = SIGNAL_TYPES.SELL;
|
||||
strength = Math.min(50 + ((midpointLow - price) / midpointLow) * 200, 100);
|
||||
reasoning = `Price (${price.toFixed(2)}) is below the slow channel (${midpointLow.toFixed(2)}), strong bearish trend in place`;
|
||||
} else if (midpointHigh > midpointLow) {
|
||||
signalType = SIGNAL_TYPES.HOLD;
|
||||
strength = 40;
|
||||
reasoning = `Fast and slow channels are wide apart (${midpointHigh.toFixed(2)} vs ${midpointLow.toFixed(2)}), trend is established but price is in neutral zone`;
|
||||
} else {
|
||||
signalType = SIGNAL_TYPES.HOLD;
|
||||
strength = 30;
|
||||
reasoning = `Channels are close together, no clear directional trend yet`;
|
||||
}
|
||||
|
||||
return { type: signalType, strength, value: price, reasoning };
|
||||
return signalFunction(indicator, lastCandle, prevCandle, indicatorValues);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -348,11 +45,10 @@ export function calculateAllIndicatorSignals() {
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('[Signals] Calculating for', activeIndicators.length, 'indicators with', candles.length, 'candles');
|
||||
const signals = [];
|
||||
|
||||
for (const indicator of activeIndicators) {
|
||||
const IndicatorClass = IR?.[indicator.type];
|
||||
const IndicatorClass = IndicatorRegistry[indicator.type];
|
||||
if (!IndicatorClass) {
|
||||
console.log('[Signals] No class for indicator type:', indicator.type);
|
||||
continue;
|
||||
@ -362,21 +58,14 @@ export function calculateAllIndicatorSignals() {
|
||||
let results = indicator.cachedResults;
|
||||
let meta = indicator.cachedMeta;
|
||||
|
||||
console.log(`[Signals] ${indicator.name}: indicator.cachedResults length = ${results?.length || 0}`);
|
||||
|
||||
if (!results || !meta || results.length !== candles.length) {
|
||||
console.log(`[Signals] ${indicator.name}: Results mismatch or missing - recalculating`);
|
||||
console.log(`[Signals] ${indicator.name}: candles.length=${candles.length}, results.length=${results?.length || 0}`);
|
||||
const instance = new IndicatorClass(indicator);
|
||||
meta = instance.getMetadata();
|
||||
results = instance.calculate(candles);
|
||||
console.log(`[Signals] ${indicator.name}: New results length = ${results?.length || 0}`);
|
||||
indicator.cachedResults = results;
|
||||
indicator.cachedMeta = meta;
|
||||
}
|
||||
|
||||
console.log('[Signals]', indicator.type, '- Results length:', results?.length, 'Last result:', results?.[results.length - 1]);
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
console.log('[Signals] No results for indicator:', indicator.type);
|
||||
continue;
|
||||
@ -394,14 +83,10 @@ export function calculateAllIndicatorSignals() {
|
||||
} else if (typeof lastResult === 'number') {
|
||||
values = { ma: lastResult };
|
||||
} else {
|
||||
console.log('[Signals] Unexpected result type for', indicator.type, ':', typeof lastResult, lastResult);
|
||||
console.log('[Signals] Unexpected result type for', indicator.type, ':', typeof lastResult);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (indicator.type === 'sma') {
|
||||
console.log('[Signals] SMA result:', lastResult, 'values:', values);
|
||||
}
|
||||
|
||||
const signal = calculateIndicatorSignal(indicator, candles, values);
|
||||
|
||||
let currentSignal = signal;
|
||||
@ -434,95 +119,27 @@ export function calculateAllIndicatorSignals() {
|
||||
}
|
||||
}
|
||||
|
||||
const label = indicator.type?.toUpperCase();
|
||||
const 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;
|
||||
|
||||
signals.push({
|
||||
id: indicator.id,
|
||||
name: meta?.name || indicator.type,
|
||||
label: label,
|
||||
params: params || null,
|
||||
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: SIGNAL_COLORS[currentSignal.type],
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: SIGNAL_TYPES.HOLD,
|
||||
strength: 0,
|
||||
reasoning: 'No active indicators',
|
||||
buyCount: 0,
|
||||
sellCount: 0,
|
||||
holdCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
const buySignals = signals.filter(s => s.signal === SIGNAL_TYPES.BUY);
|
||||
const sellSignals = signals.filter(s => s.signal === SIGNAL_TYPES.SELL);
|
||||
const holdSignals = signals.filter(s => s.signal === SIGNAL_TYPES.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 = SIGNAL_TYPES.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 = SIGNAL_TYPES.SELL;
|
||||
const avgSellStrength = sellWeight / sellCount;
|
||||
strength = Math.round(avgSellStrength * (sellCount / total));
|
||||
reasoning = `${sellCount} sell signals, ${buyCount} buy, ${holdCount} hold`;
|
||||
} else {
|
||||
summarySignal = SIGNAL_TYPES.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: SIGNAL_COLORS[summarySignal]
|
||||
};
|
||||
|
||||
console.log('[calculateSummarySignal] Result:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
export { SIGNAL_TYPES, SIGNAL_COLORS };
|
||||
}
|
||||
Reference in New Issue
Block a user