diff --git a/src/api/dashboard/static/js/indicators/atr.js b/src/api/dashboard/static/js/indicators/atr.js index 7bfb6f9..0f45697 100644 --- a/src/api/dashboard/static/js/indicators/atr.js +++ b/src/api/dashboard/static/js/indicators/atr.js @@ -1,6 +1,70 @@ -import { BaseIndicator } from './base.js'; +// Self-contained ATR indicator +// Includes math, metadata, signal calculation, and base class +// Signal constants (defined in each indicator file) +const SIGNAL_TYPES = { + BUY: 'buy', + SELL: 'sell', + HOLD: 'hold' +}; + +const SIGNAL_COLORS = { + buy: '#26a69a', + hold: '#787b86', + sell: '#ef5350' +}; + +// Base class (inline replacement for BaseIndicator) +class BaseIndicator { + constructor(config) { + this.id = config.id; + this.type = config.type; + this.name = config.name; + this.params = config.params || {}; + this.timeframe = config.timeframe || '1m'; + this.series = []; + this.visible = config.visible !== false; + this.cachedResults = null; + this.cachedMeta = null; + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } +} + +// Signal calculation for ATR +function calculateATRSignal(indicator, lastCandle, prevCandle, values) { + const atr = values?.atr; + const close = lastCandle.close; + const prevClose = prevCandle?.close; + + if (!atr || atr === null || !prevClose) { + return null; + } + + const atrPercent = atr / close * 100; + const priceChange = Math.abs(close - prevClose); + const atrRatio = priceChange / atr; + + if (atrRatio > 1.5) { + return { + type: SIGNAL_TYPES.HOLD, + strength: 70, + value: atr, + reasoning: `High volatility: ATR (${atr.toFixed(2)}, ${atrPercent.toFixed(2)}%)` + }; + } + + return null; +} + +// ATR Indicator class export class ATRIndicator extends BaseIndicator { + constructor(config) { + super(config); + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } + calculate(candles) { const period = this.params.period || 14; const results = new Array(candles.length).fill(null); @@ -23,16 +87,32 @@ export class ATRIndicator extends BaseIndicator { atr = (atr * (period - 1) + tr[i]) / period; results[i] = atr; } - return results; + + return results.map(atr => ({ atr })); } getMetadata() { return { name: 'ATR', description: 'Average True Range - measures market volatility', - inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }], - plots: [{ id: 'value', color: '#795548', title: 'ATR' }], + inputs: [{ + name: 'period', + label: 'Period', + type: 'number', + default: 14, + min: 1, + max: 100, + description: 'Period for ATR calculation' + }], + plots: [{ + id: 'value', + color: '#795548', + title: 'ATR', + lineWidth: 1 + }], displayMode: 'pane' }; } } + +export { calculateATRSignal }; \ No newline at end of file diff --git a/src/api/dashboard/static/js/indicators/base.js b/src/api/dashboard/static/js/indicators/base.js deleted file mode 100644 index 50520a3..0000000 --- a/src/api/dashboard/static/js/indicators/base.js +++ /dev/null @@ -1,18 +0,0 @@ -export class BaseIndicator { - constructor(config) { - this.name = config.name; - this.type = config.type; - this.params = config.params || {}; - this.timeframe = config.timeframe || '1m'; - } - calculate(candles) { throw new Error("Not implemented"); } - - getMetadata() { - return { - name: this.name, - inputs: [], - plots: [], - displayMode: 'overlay' - }; - } -} diff --git a/src/api/dashboard/static/js/indicators/bb.js b/src/api/dashboard/static/js/indicators/bb.js index 07d34c3..dceb879 100644 --- a/src/api/dashboard/static/js/indicators/bb.js +++ b/src/api/dashboard/static/js/indicators/bb.js @@ -1,6 +1,76 @@ -import { BaseIndicator } from './base.js'; +// Self-contained Bollinger Bands indicator +// Includes math, metadata, signal calculation, and base class +// Signal constants (defined in each indicator file) +const SIGNAL_TYPES = { + BUY: 'buy', + SELL: 'sell', + HOLD: 'hold' +}; + +const SIGNAL_COLORS = { + buy: '#26a69a', + hold: '#787b86', + sell: '#ef5350' +}; + +// Base class (inline replacement for BaseIndicator) +class BaseIndicator { + constructor(config) { + this.id = config.id; + this.type = config.type; + this.name = config.name; + this.params = config.params || {}; + this.timeframe = config.timeframe || '1m'; + this.series = []; + this.visible = config.visible !== false; + this.cachedResults = null; + this.cachedMeta = null; + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } +} + +// Signal calculation for Bollinger Bands +function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values) { + const close = lastCandle.close; + const upper = values?.upper; + const lower = values?.lower; + const middle = values?.middle; + + if (!upper || !lower || !middle) { + return null; + } + + const bandwidth = (upper - lower) / middle * 100; + + if (close <= lower) { + return { + type: SIGNAL_TYPES.BUY, + strength: Math.min(50 + (lower - close) / close * 1000, 100), + value: close, + reasoning: `Price (${close.toFixed(2)}) at or below lower band (${lower.toFixed(2)}), bandwidth: ${bandwidth.toFixed(1)}%` + }; + } else if (close >= upper) { + return { + type: SIGNAL_TYPES.SELL, + strength: Math.min(50 + (close - upper) / close * 1000, 100), + value: close, + reasoning: `Price (${close.toFixed(2)}) at or above upper band (${upper.toFixed(2)}), bandwidth: ${bandwidth.toFixed(1)}%` + }; + } else { + return null; + } +} + +// Bollinger Bands Indicator class export class BollingerBandsIndicator extends BaseIndicator { + constructor(config) { + super(config); + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } + calculate(candles) { const period = this.params.period || 20; const stdDevMult = this.params.stdDev || 2; @@ -41,3 +111,5 @@ export class BollingerBandsIndicator extends BaseIndicator { }; } } + +export { calculateBollingerBandsSignal }; \ No newline at end of file diff --git a/src/api/dashboard/static/js/indicators/ema.js b/src/api/dashboard/static/js/indicators/ema.js deleted file mode 100644 index c928d4b..0000000 --- a/src/api/dashboard/static/js/indicators/ema.js +++ /dev/null @@ -1,18 +0,0 @@ -import { MA } from './ma.js'; -import { BaseIndicator } from './base.js'; - -export class EMAIndicator extends BaseIndicator { - calculate(candles) { - const period = this.params.period || 44; - return MA.ema(candles, period, 'close'); - } - - getMetadata() { - return { - name: 'EMA', - inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }], - plots: [{ id: 'value', color: '#ff9800', title: 'EMA' }], - displayMode: 'overlay' - }; - } -} diff --git a/src/api/dashboard/static/js/indicators/hts.js b/src/api/dashboard/static/js/indicators/hts.js index 4dc5247..79f12ef 100644 --- a/src/api/dashboard/static/js/indicators/hts.js +++ b/src/api/dashboard/static/js/indicators/hts.js @@ -1,7 +1,170 @@ -import { MA } from './ma.js'; -import { BaseIndicator } from './base.js'; +// Self-contained HTS Trend System indicator +// Includes math, metadata, signal calculation, and base class +// Signal constants (defined in each indicator file) +const SIGNAL_TYPES = { + BUY: 'buy', + SELL: 'sell', + HOLD: 'hold' +}; + +const SIGNAL_COLORS = { + buy: '#26a69a', + hold: '#787b86', + sell: '#ef5350' +}; + +// Base class (inline replacement for BaseIndicator) +class BaseIndicator { + constructor(config) { + this.id = config.id; + this.type = config.type; + this.name = config.name; + this.params = config.params || {}; + this.timeframe = config.timeframe || '1m'; + this.series = []; + this.visible = config.visible !== false; + this.cachedResults = null; + this.cachedMeta = null; + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } +} + +// MA calculations inline (SMA/EMA/RMA/WMA/VWMA) +function calculateSMA(candles, period, source = 'close') { + const results = new Array(candles.length).fill(null); + let sum = 0; + for (let i = 0; i < candles.length; i++) { + sum += candles[i][source]; + if (i >= period) sum -= candles[i - period][source]; + if (i >= period - 1) results[i] = sum / period; + } + return results; +} + +function calculateEMA(candles, period, source = 'close') { + const multiplier = 2 / (period + 1); + const results = new Array(candles.length).fill(null); + let ema = 0; + let sum = 0; + for (let i = 0; i < candles.length; i++) { + if (i < period) { + sum += candles[i][source]; + if (i === period - 1) { + ema = sum / period; + results[i] = ema; + } + } else { + ema = (candles[i][source] - ema) * multiplier + ema; + results[i] = ema; + } + } + return results; +} + +function calculateRMA(candles, period, source = 'close') { + const multiplier = 1 / period; + const results = new Array(candles.length).fill(null); + let rma = 0; + let sum = 0; + + for (let i = 0; i < candles.length; i++) { + if (i < period) { + sum += candles[i][source]; + if (i === period - 1) { + rma = sum / period; + results[i] = rma; + } + } else { + rma = (candles[i][source] - rma) * multiplier + rma; + results[i] = rma; + } + } + return results; +} + +function calculateWMA(candles, period, source = 'close') { + const results = new Array(candles.length).fill(null); + const weightSum = (period * (period + 1)) / 2; + + for (let i = period - 1; i < candles.length; i++) { + let sum = 0; + for (let j = 0; j < period; j++) { + sum += candles[i - j][source] * (period - j); + } + results[i] = sum / weightSum; + } + return results; +} + +function calculateVWMA(candles, period, source = 'close') { + const results = new Array(candles.length).fill(null); + + for (let i = period - 1; i < candles.length; i++) { + let sumPV = 0; + let sumV = 0; + for (let j = 0; j < period; j++) { + sumPV += candles[i - j][source] * candles[i - j].volume; + sumV += candles[i - j].volume; + } + results[i] = sumV !== 0 ? sumPV / sumV : null; + } + return results; +} + +// MA dispatcher function +function getMA(type, candles, period, source = 'close') { + switch (type.toUpperCase()) { + case 'SMA': return calculateSMA(candles, period, source); + case 'EMA': return calculateEMA(candles, period, source); + case 'RMA': return calculateRMA(candles, period, source); + case 'WMA': return calculateWMA(candles, period, source); + case 'VWMA': return calculateVWMA(candles, period, source); + default: return calculateSMA(candles, period, source); + } +} + +// Signal calculation for HTS +function calculateHTSSignal(indicator, lastCandle, prevCandle, values) { + const fastHigh = values?.fastHigh; + const fastLow = values?.fastLow; + const slowHigh = values?.slowHigh; + const slowLow = values?.slowLow; + + if (!fastHigh || !fastLow || !slowHigh || !slowLow) { + return null; + } + + const close = lastCandle.close; + + if (close > slowLow) { + return { + type: SIGNAL_TYPES.BUY, + strength: Math.min(60 + (close - slowLow) / slowLow * 500, 100), + value: close, + reasoning: `Price (${close.toFixed(2)}) is above slow low (${slowLow.toFixed(2)})` + }; + } else if (close < slowHigh) { + return { + type: SIGNAL_TYPES.SELL, + strength: Math.min(60 + (slowHigh - close) / close * 500, 100), + value: close, + reasoning: `Price (${close.toFixed(2)}) is below slow high (${slowHigh.toFixed(2)})` + }; + } else { + return null; + } +} + +// HTS Indicator class export class HTSIndicator extends BaseIndicator { + constructor(config) { + super(config); + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } + calculate(candles, oneMinCandles = null, targetTF = null) { const shortPeriod = this.params.short || 33; const longPeriod = this.params.long || 144; @@ -42,10 +205,10 @@ export class HTSIndicator extends BaseIndicator { workingCandles = grouped; } - const shortHigh = MA.get(maType, workingCandles, shortPeriod, 'high'); - const shortLow = MA.get(maType, workingCandles, shortPeriod, 'low'); - const longHigh = MA.get(maType, workingCandles, longPeriod, 'high'); - const longLow = MA.get(maType, workingCandles, longPeriod, 'low'); + const shortHigh = getMA(maType, workingCandles, shortPeriod, 'high'); + const shortLow = getMA(maType, workingCandles, shortPeriod, 'low'); + const longHigh = getMA(maType, workingCandles, longPeriod, 'high'); + const longLow = getMA(maType, workingCandles, longPeriod, 'low'); return workingCandles.map((_, i) => ({ fastHigh: shortHigh[i], @@ -82,3 +245,5 @@ export class HTSIndicator extends BaseIndicator { }; } } + +export { calculateHTSSignal }; \ No newline at end of file diff --git a/src/api/dashboard/static/js/indicators/index.js b/src/api/dashboard/static/js/indicators/index.js index da6fb20..b4a950d 100644 --- a/src/api/dashboard/static/js/indicators/index.js +++ b/src/api/dashboard/static/js/indicators/index.js @@ -1,34 +1,47 @@ -export { MA } from './ma.js'; -export { BaseIndicator } from './base.js'; -export { HTSIndicator } from './hts.js'; -export { MAIndicator } from './ma_indicator.js'; -export { RSIIndicator } from './rsi.js'; -export { BollingerBandsIndicator } from './bb.js'; -export { MACDIndicator } from './macd.js'; -export { StochasticIndicator } from './stoch.js'; -export { ATRIndicator } from './atr.js'; +// Indicator registry and exports for self-contained indicators -import { HTSIndicator } from './hts.js'; -import { MAIndicator } from './ma_indicator.js'; -import { RSIIndicator } from './rsi.js'; -import { BollingerBandsIndicator } from './bb.js'; -import { MACDIndicator } from './macd.js'; -import { StochasticIndicator } from './stoch.js'; -import { ATRIndicator } from './atr.js'; +// Import all indicator classes and their signal functions +export { MAIndicator, calculateMASignal } from './moving_average.js'; +export { MACDIndicator, calculateMACDSignal } from './macd.js'; +export { HTSIndicator, calculateHTSSignal } from './hts.js'; +export { RSIIndicator, calculateRSISignal } from './rsi.js'; +export { BollingerBandsIndicator, calculateBollingerBandsSignal } from './bb.js'; +export { StochasticIndicator, calculateStochSignal } from './stoch.js'; +export { ATRIndicator, calculateATRSignal } from './atr.js'; +// Import for registry +import { MAIndicator as MAI, calculateMASignal as CMA } from './moving_average.js'; +import { MACDIndicator as MACDI, calculateMACDSignal as CMC } from './macd.js'; +import { HTSIndicator as HTSI, calculateHTSSignal as CHTS } from './hts.js'; +import { RSIIndicator as RSII, calculateRSISignal as CRSI } from './rsi.js'; +import { BollingerBandsIndicator as BBI, calculateBollingerBandsSignal as CBB } from './bb.js'; +import { StochasticIndicator as STOCHI, calculateStochSignal as CST } from './stoch.js'; +import { ATRIndicator as ATRI, calculateATRSignal as CATR } from './atr.js'; + +// Signal function registry for easy dispatch +export const SignalFunctionRegistry = { + ma: CMA, + macd: CMC, + hts: CHTS, + rsi: CRSI, + bb: CBB, + stoch: CST, + atr: CATR +}; + +// Indicator registry for UI export const IndicatorRegistry = { - hts: HTSIndicator, - ma: MAIndicator, - rsi: RSIIndicator, - bb: BollingerBandsIndicator, - macd: MACDIndicator, - stoch: StochasticIndicator, - atr: ATRIndicator + ma: MAI, + macd: MACDI, + hts: HTSI, + rsi: RSII, + bb: BBI, + stoch: STOCHI, + atr: ATRI }; /** - * Dynamically build the available indicators list from the registry. - * Each indicator class provides its own name and description via getMetadata(). + * Get list of available indicators for the UI catalog */ export function getAvailableIndicators() { return Object.entries(IndicatorRegistry).map(([type, IndicatorClass]) => { @@ -41,3 +54,12 @@ export function getAvailableIndicators() { }; }); } + +/** + * Get signal function for an indicator type + * @param {string} indicatorType - The type of indicator (e.g., 'ma', 'rsi') + * @returns {Function|null} The signal calculation function or null if not found + */ +export function getSignalFunction(indicatorType) { + return SignalFunctionRegistry[indicatorType] || null; +} \ No newline at end of file diff --git a/src/api/dashboard/static/js/indicators/ma.js b/src/api/dashboard/static/js/indicators/ma.js deleted file mode 100644 index 2d0c5f6..0000000 --- a/src/api/dashboard/static/js/indicators/ma.js +++ /dev/null @@ -1,93 +0,0 @@ -export class MA { - static get(type, candles, period, source = 'close') { - switch (type.toUpperCase()) { - case 'SMA': return MA.sma(candles, period, source); - case 'EMA': return MA.ema(candles, period, source); - case 'RMA': return MA.rma(candles, period, source); - case 'WMA': return MA.wma(candles, period, source); - case 'VWMA': return MA.vwma(candles, period, source); - default: return MA.sma(candles, period, source); - } - } - - static sma(candles, period, source = 'close') { - const results = new Array(candles.length).fill(null); - let sum = 0; - for (let i = 0; i < candles.length; i++) { - sum += candles[i][source]; - if (i >= period) sum -= candles[i - period][source]; - if (i >= period - 1) results[i] = sum / period; - } - return results; - } - - static ema(candles, period, source = 'close') { - const multiplier = 2 / (period + 1); - const results = new Array(candles.length).fill(null); - let ema = 0; - let sum = 0; - for (let i = 0; i < candles.length; i++) { - if (i < period) { - sum += candles[i][source]; - if (i === period - 1) { - ema = sum / period; - results[i] = ema; - } - } else { - ema = (candles[i][source] - ema) * multiplier + ema; - results[i] = ema; - } - } - return results; - } - - static rma(candles, period, source = 'close') { - const multiplier = 1 / period; - const results = new Array(candles.length).fill(null); - let rma = 0; - let sum = 0; - - for (let i = 0; i < candles.length; i++) { - if (i < period) { - sum += candles[i][source]; - if (i === period - 1) { - rma = sum / period; - results[i] = rma; - } - } else { - rma = (candles[i][source] - rma) * multiplier + rma; - results[i] = rma; - } - } - return results; - } - - static wma(candles, period, source = 'close') { - const results = new Array(candles.length).fill(null); - const weightSum = (period * (period + 1)) / 2; - - for (let i = period - 1; i < candles.length; i++) { - let sum = 0; - for (let j = 0; j < period; j++) { - sum += candles[i - j][source] * (period - j); - } - results[i] = sum / weightSum; - } - return results; - } - - static vwma(candles, period, source = 'close') { - const results = new Array(candles.length).fill(null); - - for (let i = period - 1; i < candles.length; i++) { - let sumPV = 0; - let sumV = 0; - for (let j = 0; j < period; j++) { - sumPV += candles[i - j][source] * candles[i - j].volume; - sumV += candles[i - j].volume; - } - results[i] = sumV !== 0 ? sumPV / sumV : null; - } - return results; - } -} diff --git a/src/api/dashboard/static/js/indicators/ma_indicator.js b/src/api/dashboard/static/js/indicators/ma_indicator.js deleted file mode 100644 index b8222e7..0000000 --- a/src/api/dashboard/static/js/indicators/ma_indicator.js +++ /dev/null @@ -1,23 +0,0 @@ -import { MA } from './ma.js'; -import { BaseIndicator } from './base.js'; - -export class MAIndicator extends BaseIndicator { - calculate(candles) { - const period = this.params.period || 44; - const maType = this.params.maType || 'SMA'; - return MA.get(maType, candles, period, 'close'); - } - - getMetadata() { - return { - name: 'MA', - description: 'Moving Average (SMA/EMA/RMA/WMA/VWMA)', - inputs: [ - { name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }, - { name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'SMA' } - ], - plots: [{ id: 'value', color: '#2962ff', title: 'MA' }], - displayMode: 'overlay' - }; - } -} diff --git a/src/api/dashboard/static/js/indicators/macd.js b/src/api/dashboard/static/js/indicators/macd.js index b83acda..aa80fbc 100644 --- a/src/api/dashboard/static/js/indicators/macd.js +++ b/src/api/dashboard/static/js/indicators/macd.js @@ -1,37 +1,123 @@ -import { MA } from './ma.js'; -import { BaseIndicator } from './base.js'; +// Self-contained MACD indicator +// Includes math, metadata, signal calculation, and base class +// Signal constants (defined in each indicator file) +const SIGNAL_TYPES = { + BUY: 'buy', + SELL: 'sell', + HOLD: 'hold' +}; + +const SIGNAL_COLORS = { + buy: '#26a69a', + hold: '#787b86', + sell: '#ef5350' +}; + +// Base class (inline replacement for BaseIndicator) +class BaseIndicator { + constructor(config) { + this.id = config.id; + this.type = config.type; + this.name = config.name; + this.params = config.params || {}; + this.timeframe = config.timeframe || '1m'; + this.series = []; + this.visible = config.visible !== false; + this.cachedResults = null; + this.cachedMeta = null; + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } +} + +// EMA calculation inline (needed for MACD) +function calculateEMAInline(data, period) { + const multiplier = 2 / (period + 1); + const ema = []; + + for (let i = 0; i < data.length; i++) { + if (i < period - 1) { + ema.push(null); + } else if (i === period - 1) { + ema.push(data[i]); + } else { + ema.push((data[i] - ema[i - 1]) * multiplier + ema[i - 1]); + } + } + + return ema; +} + +// Signal calculation for MACD +function calculateMACDSignal(indicator, lastCandle, prevCandle, values) { + const macd = values?.macd; + const signal = values?.signal; + const histogram = values?.histogram; + + if (!macd || macd === null || !signal || signal === null) { + return null; + } + + let signalType, strength, reasoning; + + const prevCandleHistogram = prevCandle ? values?.histogram : null; + + if (macd > signal) { + signalType = SIGNAL_TYPES.BUY; + strength = Math.min(50 + histogram * 500, 100); + reasoning = `MACD (${macd.toFixed(2)}) is above Signal (${signal.toFixed(2)})`; + } else if (macd < signal) { + signalType = SIGNAL_TYPES.SELL; + strength = Math.min(50 + Math.abs(histogram) * 500, 100); + reasoning = `MACD (${macd.toFixed(2)}) is below Signal (${signal.toFixed(2)})`; + } else { + return null; + } + + return { type: signalType, strength, value: macd, reasoning }; +} + +// MACD Indicator class export class MACDIndicator extends BaseIndicator { + constructor(config) { + super(config); + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } + calculate(candles) { const fast = this.params.fast || 12; const slow = this.params.slow || 26; - const signal = this.params.signal || 9; + const signalPeriod = this.params.signal || 9; - const fastEma = MA.ema(candles, fast, 'close'); - const slowEma = MA.ema(candles, slow, 'close'); + const closes = candles.map(c => c.close); - const macdLine = fastEma.map((f, i) => (f !== null && slowEma[i] !== null) ? f - slowEma[i] : null); + // Use inline EMA calculation instead of MA.ema() + const fastEMA = calculateEMAInline(closes, fast); + const slowEMA = calculateEMAInline(closes, slow); + + const macdLine = fastEMA.map((f, i) => (f !== null && slowEMA[i] !== null) ? f - slowEMA[i] : null); - const signalLine = new Array(candles.length).fill(null); - const multiplier = 2 / (signal + 1); - let ema = 0; let sum = 0; + let ema = 0; let count = 0; - for (let i = 0; i < macdLine.length; i++) { - if (macdLine[i] === null) continue; + const signalLine = macdLine.map(m => { + if (m === null) return null; count++; - if (count < signal) { - sum += macdLine[i]; - } else if (count === signal) { - sum += macdLine[i]; - ema = sum / signal; - signalLine[i] = ema; + if (count < signalPeriod) { + sum += m; + return null; + } else if (count === signalPeriod) { + sum += m; + ema = sum / signalPeriod; + return ema; } else { - ema = (macdLine[i] - ema) * multiplier + ema; - signalLine[i] = ema; + ema = (m - ema) * (2 / (signalPeriod + 1)) + ema; + return ema; } - } + }); return macdLine.map((m, i) => ({ macd: m, @@ -58,3 +144,5 @@ export class MACDIndicator extends BaseIndicator { }; } } + +export { calculateMACDSignal }; \ No newline at end of file diff --git a/src/api/dashboard/static/js/indicators/moving_average.js b/src/api/dashboard/static/js/indicators/moving_average.js new file mode 100644 index 0000000..83dd61c --- /dev/null +++ b/src/api/dashboard/static/js/indicators/moving_average.js @@ -0,0 +1,217 @@ +// Self-contained Moving Average indicator with SMA/EMA/RMA/WMA/VWMA support +// Includes math, metadata, signal calculation, and base class + +// Signal constants (defined in each indicator file) +const SIGNAL_TYPES = { + BUY: 'buy', + SELL: 'sell', + HOLD: 'hold' +}; + +const SIGNAL_COLORS = { + buy: '#26a69a', + hold: '#787b86', + sell: '#ef5350' +}; + +// Base class (inline replacement for BaseIndicator) +class BaseIndicator { + constructor(config) { + this.id = config.id; + this.type = config.type; + this.name = config.name; + this.params = config.params || {}; + this.timeframe = config.timeframe || '1m'; + this.series = []; + this.visible = config.visible !== false; + this.cachedResults = null; + this.cachedMeta = null; + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } +} + +// Moving Average math (SMA/EMA/RMA/WMA/VWMA) +function calculateSMA(candles, period, source = 'close') { + const results = new Array(candles.length).fill(null); + let sum = 0; + for (let i = 0; i < candles.length; i++) { + sum += candles[i][source]; + if (i >= period) sum -= candles[i - period][source]; + if (i >= period - 1) results[i] = sum / period; + } + return results; +} + +function calculateEMA(candles, period, source = 'close') { + const multiplier = 2 / (period + 1); + const results = new Array(candles.length).fill(null); + let ema = 0; + let sum = 0; + for (let i = 0; i < candles.length; i++) { + if (i < period) { + sum += candles[i][source]; + if (i === period - 1) { + ema = sum / period; + results[i] = ema; + } + } else { + ema = (candles[i][source] - ema) * multiplier + ema; + results[i] = ema; + } + } + return results; +} + +function calculateRMA(candles, period, source = 'close') { + const multiplier = 1 / period; + const results = new Array(candles.length).fill(null); + let rma = 0; + let sum = 0; + + for (let i = 0; i < candles.length; i++) { + if (i < period) { + sum += candles[i][source]; + if (i === period - 1) { + rma = sum / period; + results[i] = rma; + } + } else { + rma = (candles[i][source] - rma) * multiplier + rma; + results[i] = rma; + } + } + return results; +} + +function calculateWMA(candles, period, source = 'close') { + const results = new Array(candles.length).fill(null); + const weightSum = (period * (period + 1)) / 2; + + for (let i = period - 1; i < candles.length; i++) { + let sum = 0; + for (let j = 0; j < period; j++) { + sum += candles[i - j][source] * (period - j); + } + results[i] = sum / weightSum; + } + return results; +} + +function calculateVWMA(candles, period, source = 'close') { + const results = new Array(candles.length).fill(null); + + for (let i = period - 1; i < candles.length; i++) { + let sumPV = 0; + let sumV = 0; + for (let j = 0; j < period; j++) { + sumPV += candles[i - j][source] * candles[i - j].volume; + sumV += candles[i - j].volume; + } + results[i] = sumV !== 0 ? sumPV / sumV : null; + } + return results; +} + +// Signal calculation for Moving Average +function calculateMASignal(indicator, lastCandle, prevCandle, values) { + const close = lastCandle.close; + const ma = values?.ma; + + if (!ma && ma !== 0) { + return null; + } + + if (close > ma) { + return { + type: SIGNAL_TYPES.BUY, + strength: Math.min(60 + ((close - ma) / ma) * 500, 100), + value: close, + reasoning: `Price (${close.toFixed(2)}) is above MA (${ma.toFixed(2)})` + }; + } else if (close < ma) { + return { + type: SIGNAL_TYPES.SELL, + strength: Math.min(60 + ((ma - close) / ma) * 500, 100), + value: close, + reasoning: `Price (${close.toFixed(2)}) is below MA (${ma.toFixed(2)})` + }; + } else { + return null; + } +} + +// MA Indicator class +export class MAIndicator extends BaseIndicator { + constructor(config) { + super(config); + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } + + calculate(candles) { + const maType = (this.params.maType || 'SMA').toLowerCase(); + const period = this.params.period || 44; + + let maValues; + + switch (maType) { + case 'sma': + maValues = calculateSMA(candles, period, this.params.source || 'close'); + break; + case 'ema': + maValues = calculateEMA(candles, period, this.params.source || 'close'); + break; + case 'rma': + maValues = calculateRMA(candles, period, this.params.source || 'close'); + break; + case 'wma': + maValues = calculateWMA(candles, period, this.params.source || 'close'); + break; + case 'vwma': + maValues = calculateVWMA(candles, period, this.params.source || 'close'); + break; + default: + maValues = calculateSMA(candles, period, this.params.source || 'close'); + } + + return maValues.map(ma => ({ ma })); + } + + getMetadata() { + return { + name: 'MA', + description: 'Moving Average (SMA/EMA/RMA/WMA/VWMA)', + inputs: [ + { + name: 'period', + label: 'Period', + type: 'number', + default: 44, + min: 1, + max: 500 + }, + { + name: 'maType', + label: 'MA Type', + type: 'select', + options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], + default: 'SMA' + } + ], + plots: [ + { + id: 'value', + color: '#2962ff', + title: 'MA', + style: 'solid', + width: 1 + } + ], + displayMode: 'overlay' + }; + } +} + +// Export signal function for external use +export { calculateMASignal }; \ No newline at end of file diff --git a/src/api/dashboard/static/js/indicators/rsi.js b/src/api/dashboard/static/js/indicators/rsi.js index 6dab8c9..69c2f92 100644 --- a/src/api/dashboard/static/js/indicators/rsi.js +++ b/src/api/dashboard/static/js/indicators/rsi.js @@ -1,6 +1,71 @@ -import { BaseIndicator } from './base.js'; +// Self-contained RSI indicator +// Includes math, metadata, signal calculation, and base class +// Signal constants (defined in each indicator file) +const SIGNAL_TYPES = { + BUY: 'buy', + SELL: 'sell', + HOLD: 'hold' +}; + +const SIGNAL_COLORS = { + buy: '#26a69a', + hold: '#787b86', + sell: '#ef5350' +}; + +// Base class (inline replacement for BaseIndicator) +class BaseIndicator { + constructor(config) { + this.id = config.id; + this.type = config.type; + this.name = config.name; + this.params = config.params || {}; + this.timeframe = config.timeframe || '1m'; + this.series = []; + this.visible = config.visible !== false; + this.cachedResults = null; + this.cachedMeta = null; + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } +} + +// Signal calculation for RSI +function calculateRSISignal(indicator, lastCandle, prevCandle, values) { + const rsi = values?.rsi; + const overbought = indicator.params?.overbought || 70; + const oversold = indicator.params?.oversold || 30; + + if (!rsi || rsi === null) { + return null; + } + + let signalType, strength, reasoning; + + if (rsi < oversold) { + signalType = SIGNAL_TYPES.BUY; + strength = Math.min(50 + (oversold - rsi) * 2, 100); + reasoning = `RSI (${rsi.toFixed(2)}) is oversold (<${oversold})`; + } else if (rsi > overbought) { + signalType = SIGNAL_TYPES.SELL; + strength = Math.min(50 + (rsi - overbought) * 2, 100); + reasoning = `RSI (${rsi.toFixed(2)}) is overbought (>${overbought})`; + } else { + return null; + } + + return { type: signalType, strength, value: rsi, reasoning }; +} + +// RSI Indicator class export class RSIIndicator extends BaseIndicator { + constructor(config) { + super(config); + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } + calculate(candles) { const period = this.params.period || 14; const overbought = this.params.overbought || 70; @@ -26,7 +91,7 @@ export class RSIIndicator extends BaseIndicator { const avgUp = upSum / period; const avgDown = downSum / period; rsiValues[i] = avgDown === 0 ? 100 : (avgUp === 0 ? 0 : 100 - (100 / (1 + avgUp / avgDown))); - upSum = avgUp; // Store for next RMA step + upSum = avgUp; downSum = avgDown; } else { upSum = (up - upSum) * rmaAlpha + upSum; @@ -38,7 +103,7 @@ export class RSIIndicator extends BaseIndicator { // Combine results return rsiValues.map((rsi, i) => { return { - paneBg: 80, // Background lightening trick + paneBg: 80, rsi: rsi, overboughtBand: overbought, oversoldBand: oversold @@ -56,13 +121,8 @@ export class RSIIndicator extends BaseIndicator { { name: 'oversold', label: 'Oversold Level', type: 'number', default: 30, min: 5, max: 50 } ], plots: [ - // RSI Line - solid, 1px { id: 'rsi', color: '#7E57C2', title: '', style: 'solid', width: 1, lastValueVisible: true }, - - // Overbought Band - dashed, 1px { id: 'overboughtBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false }, - - // Oversold Band - dashed, 1px { id: 'oversoldBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false } ], displayMode: 'pane', @@ -71,3 +131,5 @@ export class RSIIndicator extends BaseIndicator { }; } } + +export { calculateRSISignal }; \ No newline at end of file diff --git a/src/api/dashboard/static/js/indicators/sma.js b/src/api/dashboard/static/js/indicators/sma.js deleted file mode 100644 index 836e84d..0000000 --- a/src/api/dashboard/static/js/indicators/sma.js +++ /dev/null @@ -1,18 +0,0 @@ -import { MA } from './ma.js'; -import { BaseIndicator } from './base.js'; - -export class SMAIndicator extends BaseIndicator { - calculate(candles) { - const period = this.params.period || 44; - return MA.sma(candles, period, 'close'); - } - - getMetadata() { - return { - name: 'SMA', - inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }], - plots: [{ id: 'value', color: '#2962ff', title: 'SMA' }], - displayMode: 'overlay' - }; - } -} diff --git a/src/api/dashboard/static/js/indicators/stoch.js b/src/api/dashboard/static/js/indicators/stoch.js index e4c5733..ca15fe1 100644 --- a/src/api/dashboard/static/js/indicators/stoch.js +++ b/src/api/dashboard/static/js/indicators/stoch.js @@ -1,6 +1,74 @@ -import { BaseIndicator } from './base.js'; +// Self-contained Stochastic Oscillator indicator +// Includes math, metadata, signal calculation, and base class +// Signal constants (defined in each indicator file) +const SIGNAL_TYPES = { + BUY: 'buy', + SELL: 'sell', + HOLD: 'hold' +}; + +const SIGNAL_COLORS = { + buy: '#26a69a', + hold: '#787b86', + sell: '#ef5350' +}; + +// Base class (inline replacement for BaseIndicator) +class BaseIndicator { + constructor(config) { + this.id = config.id; + this.type = config.type; + this.name = config.name; + this.params = config.params || {}; + this.timeframe = config.timeframe || '1m'; + this.series = []; + this.visible = config.visible !== false; + this.cachedResults = null; + this.cachedMeta = null; + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } +} + +// Signal calculation for Stochastic +function calculateStochSignal(indicator, lastCandle, prevCandle, values) { + const k = values?.k; + const d = values?.d; + const overbought = indicator.params?.overbought || 80; + const oversold = indicator.params?.oversold || 20; + + if (!k || !d) { + return null; + } + + if (k < oversold && d < oversold) { + return { + type: SIGNAL_TYPES.BUY, + strength: Math.min(50 + (oversold - k) * 2, 100), + value: k, + reasoning: `Stochastic %K (${k.toFixed(2)}) and %D (${d.toFixed(2)}) oversold (<${oversold})` + }; + } else if (k > overbought && d > overbought) { + return { + type: SIGNAL_TYPES.SELL, + strength: Math.min(50 + (k - overbought) * 2, 100), + value: k, + reasoning: `Stochastic %K (${k.toFixed(2)}) and %D (${d.toFixed(2)}) overbought (>${overbought})` + }; + } else { + return null; + } +} + +// Stochastic Oscillator Indicator class export class StochasticIndicator extends BaseIndicator { + constructor(config) { + super(config); + this.lastSignalTimestamp = null; + this.lastSignalType = null; + } + calculate(candles) { const kPeriod = this.params.kPeriod || 14; const dPeriod = this.params.dPeriod || 3; @@ -33,12 +101,28 @@ export class StochasticIndicator extends BaseIndicator { name: 'Stochastic', description: 'Stochastic Oscillator - compares close to high-low range', inputs: [ - { name: 'kPeriod', label: 'K Period', type: 'number', default: 14 }, - { name: 'dPeriod', label: 'D Period', type: 'number', default: 3 } + { + name: 'kPeriod', + label: '%K Period', + type: 'number', + default: 14, + min: 1, + max: 100, + description: 'Lookback period for %K calculation' + }, + { + name: 'dPeriod', + label: '%D Period', + type: 'number', + default: 3, + min: 1, + max: 20, + description: 'Smoothing period for %D (SMA of %K)' + } ], plots: [ - { id: 'k', color: '#3f51b5', title: '%K' }, - { id: 'd', color: '#ff9800', title: '%D' } + { id: 'k', color: '#3f51b5', title: '%K', style: 'solid', width: 1 }, + { id: 'd', color: '#ff9800', title: '%D', style: 'solid', width: 1 } ], displayMode: 'pane', paneMin: 0, @@ -46,3 +130,5 @@ export class StochasticIndicator extends BaseIndicator { }; } } + +export { calculateStochSignal }; \ No newline at end of file diff --git a/src/api/dashboard/static/js/ui/signals-calculator.js b/src/api/dashboard/static/js/ui/signals-calculator.js index 7e17e24..c54d093 100644 --- a/src/api/dashboard/static/js/ui/signals-calculator.js +++ b/src/api/dashboard/static/js/ui/signals-calculator.js @@ -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 }; \ No newline at end of file +} \ No newline at end of file