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,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 {
|
export class ATRIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles) {
|
calculate(candles) {
|
||||||
const period = this.params.period || 14;
|
const period = this.params.period || 14;
|
||||||
const results = new Array(candles.length).fill(null);
|
const results = new Array(candles.length).fill(null);
|
||||||
@ -23,16 +87,32 @@ export class ATRIndicator extends BaseIndicator {
|
|||||||
atr = (atr * (period - 1) + tr[i]) / period;
|
atr = (atr * (period - 1) + tr[i]) / period;
|
||||||
results[i] = atr;
|
results[i] = atr;
|
||||||
}
|
}
|
||||||
return results;
|
|
||||||
|
return results.map(atr => ({ atr }));
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetadata() {
|
getMetadata() {
|
||||||
return {
|
return {
|
||||||
name: 'ATR',
|
name: 'ATR',
|
||||||
description: 'Average True Range - measures market volatility',
|
description: 'Average True Range - measures market volatility',
|
||||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
|
inputs: [{
|
||||||
plots: [{ id: 'value', color: '#795548', title: 'ATR' }],
|
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'
|
displayMode: 'pane'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateATRSignal };
|
||||||
@ -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'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 {
|
export class BollingerBandsIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles) {
|
calculate(candles) {
|
||||||
const period = this.params.period || 20;
|
const period = this.params.period || 20;
|
||||||
const stdDevMult = this.params.stdDev || 2;
|
const stdDevMult = this.params.stdDev || 2;
|
||||||
@ -41,3 +111,5 @@ export class BollingerBandsIndicator extends BaseIndicator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateBollingerBandsSignal };
|
||||||
@ -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'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,170 @@
|
|||||||
import { MA } from './ma.js';
|
// Self-contained HTS Trend System indicator
|
||||||
import { BaseIndicator } from './base.js';
|
// 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 {
|
export class HTSIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles, oneMinCandles = null, targetTF = null) {
|
calculate(candles, oneMinCandles = null, targetTF = null) {
|
||||||
const shortPeriod = this.params.short || 33;
|
const shortPeriod = this.params.short || 33;
|
||||||
const longPeriod = this.params.long || 144;
|
const longPeriod = this.params.long || 144;
|
||||||
@ -42,10 +205,10 @@ export class HTSIndicator extends BaseIndicator {
|
|||||||
workingCandles = grouped;
|
workingCandles = grouped;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shortHigh = MA.get(maType, workingCandles, shortPeriod, 'high');
|
const shortHigh = getMA(maType, workingCandles, shortPeriod, 'high');
|
||||||
const shortLow = MA.get(maType, workingCandles, shortPeriod, 'low');
|
const shortLow = getMA(maType, workingCandles, shortPeriod, 'low');
|
||||||
const longHigh = MA.get(maType, workingCandles, longPeriod, 'high');
|
const longHigh = getMA(maType, workingCandles, longPeriod, 'high');
|
||||||
const longLow = MA.get(maType, workingCandles, longPeriod, 'low');
|
const longLow = getMA(maType, workingCandles, longPeriod, 'low');
|
||||||
|
|
||||||
return workingCandles.map((_, i) => ({
|
return workingCandles.map((_, i) => ({
|
||||||
fastHigh: shortHigh[i],
|
fastHigh: shortHigh[i],
|
||||||
@ -82,3 +245,5 @@ export class HTSIndicator extends BaseIndicator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateHTSSignal };
|
||||||
@ -1,34 +1,47 @@
|
|||||||
export { MA } from './ma.js';
|
// Indicator registry and exports for self-contained indicators
|
||||||
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';
|
|
||||||
|
|
||||||
import { HTSIndicator } from './hts.js';
|
// Import all indicator classes and their signal functions
|
||||||
import { MAIndicator } from './ma_indicator.js';
|
export { MAIndicator, calculateMASignal } from './moving_average.js';
|
||||||
import { RSIIndicator } from './rsi.js';
|
export { MACDIndicator, calculateMACDSignal } from './macd.js';
|
||||||
import { BollingerBandsIndicator } from './bb.js';
|
export { HTSIndicator, calculateHTSSignal } from './hts.js';
|
||||||
import { MACDIndicator } from './macd.js';
|
export { RSIIndicator, calculateRSISignal } from './rsi.js';
|
||||||
import { StochasticIndicator } from './stoch.js';
|
export { BollingerBandsIndicator, calculateBollingerBandsSignal } from './bb.js';
|
||||||
import { ATRIndicator } from './atr.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 = {
|
export const IndicatorRegistry = {
|
||||||
hts: HTSIndicator,
|
ma: MAI,
|
||||||
ma: MAIndicator,
|
macd: MACDI,
|
||||||
rsi: RSIIndicator,
|
hts: HTSI,
|
||||||
bb: BollingerBandsIndicator,
|
rsi: RSII,
|
||||||
macd: MACDIndicator,
|
bb: BBI,
|
||||||
stoch: StochasticIndicator,
|
stoch: STOCHI,
|
||||||
atr: ATRIndicator
|
atr: ATRI
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically build the available indicators list from the registry.
|
* Get list of available indicators for the UI catalog
|
||||||
* Each indicator class provides its own name and description via getMetadata().
|
|
||||||
*/
|
*/
|
||||||
export function getAvailableIndicators() {
|
export function getAvailableIndicators() {
|
||||||
return Object.entries(IndicatorRegistry).map(([type, IndicatorClass]) => {
|
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;
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +1,123 @@
|
|||||||
import { MA } from './ma.js';
|
// Self-contained MACD indicator
|
||||||
import { BaseIndicator } from './base.js';
|
// 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 {
|
export class MACDIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles) {
|
calculate(candles) {
|
||||||
const fast = this.params.fast || 12;
|
const fast = this.params.fast || 12;
|
||||||
const slow = this.params.slow || 26;
|
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 closes = candles.map(c => c.close);
|
||||||
const slowEma = MA.ema(candles, slow, '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 sum = 0;
|
||||||
|
let ema = 0;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
for (let i = 0; i < macdLine.length; i++) {
|
const signalLine = macdLine.map(m => {
|
||||||
if (macdLine[i] === null) continue;
|
if (m === null) return null;
|
||||||
count++;
|
count++;
|
||||||
if (count < signal) {
|
if (count < signalPeriod) {
|
||||||
sum += macdLine[i];
|
sum += m;
|
||||||
} else if (count === signal) {
|
return null;
|
||||||
sum += macdLine[i];
|
} else if (count === signalPeriod) {
|
||||||
ema = sum / signal;
|
sum += m;
|
||||||
signalLine[i] = ema;
|
ema = sum / signalPeriod;
|
||||||
|
return ema;
|
||||||
} else {
|
} else {
|
||||||
ema = (macdLine[i] - ema) * multiplier + ema;
|
ema = (m - ema) * (2 / (signalPeriod + 1)) + ema;
|
||||||
signalLine[i] = ema;
|
return ema;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return macdLine.map((m, i) => ({
|
return macdLine.map((m, i) => ({
|
||||||
macd: m,
|
macd: m,
|
||||||
@ -58,3 +144,5 @@ export class MACDIndicator extends BaseIndicator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateMACDSignal };
|
||||||
217
src/api/dashboard/static/js/indicators/moving_average.js
Normal file
217
src/api/dashboard/static/js/indicators/moving_average.js
Normal file
@ -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 };
|
||||||
@ -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 {
|
export class RSIIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles) {
|
calculate(candles) {
|
||||||
const period = this.params.period || 14;
|
const period = this.params.period || 14;
|
||||||
const overbought = this.params.overbought || 70;
|
const overbought = this.params.overbought || 70;
|
||||||
@ -26,7 +91,7 @@ export class RSIIndicator extends BaseIndicator {
|
|||||||
const avgUp = upSum / period;
|
const avgUp = upSum / period;
|
||||||
const avgDown = downSum / period;
|
const avgDown = downSum / period;
|
||||||
rsiValues[i] = avgDown === 0 ? 100 : (avgUp === 0 ? 0 : 100 - (100 / (1 + avgUp / avgDown)));
|
rsiValues[i] = avgDown === 0 ? 100 : (avgUp === 0 ? 0 : 100 - (100 / (1 + avgUp / avgDown)));
|
||||||
upSum = avgUp; // Store for next RMA step
|
upSum = avgUp;
|
||||||
downSum = avgDown;
|
downSum = avgDown;
|
||||||
} else {
|
} else {
|
||||||
upSum = (up - upSum) * rmaAlpha + upSum;
|
upSum = (up - upSum) * rmaAlpha + upSum;
|
||||||
@ -38,7 +103,7 @@ export class RSIIndicator extends BaseIndicator {
|
|||||||
// Combine results
|
// Combine results
|
||||||
return rsiValues.map((rsi, i) => {
|
return rsiValues.map((rsi, i) => {
|
||||||
return {
|
return {
|
||||||
paneBg: 80, // Background lightening trick
|
paneBg: 80,
|
||||||
rsi: rsi,
|
rsi: rsi,
|
||||||
overboughtBand: overbought,
|
overboughtBand: overbought,
|
||||||
oversoldBand: oversold
|
oversoldBand: oversold
|
||||||
@ -56,13 +121,8 @@ export class RSIIndicator extends BaseIndicator {
|
|||||||
{ name: 'oversold', label: 'Oversold Level', type: 'number', default: 30, min: 5, max: 50 }
|
{ name: 'oversold', label: 'Oversold Level', type: 'number', default: 30, min: 5, max: 50 }
|
||||||
],
|
],
|
||||||
plots: [
|
plots: [
|
||||||
// RSI Line - solid, 1px
|
|
||||||
{ id: 'rsi', color: '#7E57C2', title: '', style: 'solid', width: 1, lastValueVisible: true },
|
{ 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 },
|
{ 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 }
|
{ id: 'oversoldBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false }
|
||||||
],
|
],
|
||||||
displayMode: 'pane',
|
displayMode: 'pane',
|
||||||
@ -71,3 +131,5 @@ export class RSIIndicator extends BaseIndicator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateRSISignal };
|
||||||
@ -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'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 {
|
export class StochasticIndicator extends BaseIndicator {
|
||||||
|
constructor(config) {
|
||||||
|
super(config);
|
||||||
|
this.lastSignalTimestamp = null;
|
||||||
|
this.lastSignalType = null;
|
||||||
|
}
|
||||||
|
|
||||||
calculate(candles) {
|
calculate(candles) {
|
||||||
const kPeriod = this.params.kPeriod || 14;
|
const kPeriod = this.params.kPeriod || 14;
|
||||||
const dPeriod = this.params.dPeriod || 3;
|
const dPeriod = this.params.dPeriod || 3;
|
||||||
@ -33,12 +101,28 @@ export class StochasticIndicator extends BaseIndicator {
|
|||||||
name: 'Stochastic',
|
name: 'Stochastic',
|
||||||
description: 'Stochastic Oscillator - compares close to high-low range',
|
description: 'Stochastic Oscillator - compares close to high-low range',
|
||||||
inputs: [
|
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: [
|
plots: [
|
||||||
{ id: 'k', color: '#3f51b5', title: '%K' },
|
{ id: 'k', color: '#3f51b5', title: '%K', style: 'solid', width: 1 },
|
||||||
{ id: 'd', color: '#ff9800', title: '%D' }
|
{ id: 'd', color: '#ff9800', title: '%D', style: 'solid', width: 1 }
|
||||||
],
|
],
|
||||||
displayMode: 'pane',
|
displayMode: 'pane',
|
||||||
paneMin: 0,
|
paneMin: 0,
|
||||||
@ -46,3 +130,5 @@ export class StochasticIndicator extends BaseIndicator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { calculateStochSignal };
|
||||||
@ -1,330 +1,27 @@
|
|||||||
// Signal Calculator for Technical Indicators
|
// Signal Calculator - orchestrates signal calculation using indicator-specific functions
|
||||||
// Calculates buy/hold/sell signals for all active indicators
|
// Signal calculation logic is now in each indicator file
|
||||||
|
|
||||||
import { IndicatorRegistry as IR } from '../indicators/index.js';
|
import { IndicatorRegistry, getSignalFunction } from '../indicators/index.js';
|
||||||
|
|
||||||
const SIGNAL_TYPES = {
|
|
||||||
BUY: 'buy',
|
|
||||||
SELL: 'sell',
|
|
||||||
HOLD: 'hold'
|
|
||||||
};
|
|
||||||
|
|
||||||
const SIGNAL_COLORS = {
|
|
||||||
buy: '#26a69a',
|
|
||||||
hold: '#787b86',
|
|
||||||
sell: '#ef5350'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {Object} indicator - Indicator object with type, params, etc.
|
||||||
* @param {Array} candles - Recent candle data
|
* @param {Array} candles - Recent candle data
|
||||||
* @param {Object} indicatorValues - Computed indicator values for last candle
|
* @param {Object} indicatorValues - Computed indicator values for last candle
|
||||||
* @returns {Object} Signal object with type, strength, value, reasoning
|
* @returns {Object} Signal object with type, strength, value, reasoning
|
||||||
*/
|
*/
|
||||||
function calculateIndicatorSignal(indicator, candles, indicatorValues) {
|
function calculateIndicatorSignal(indicator, candles, indicatorValues) {
|
||||||
const lastCandle = candles[candles.length - 1];
|
const signalFunction = getSignalFunction(indicator.type);
|
||||||
const prevCandle = candles[candles.length - 2];
|
|
||||||
|
|
||||||
console.log('[calculateIndicatorSignal] Type:', indicator.type, 'Values:', indicatorValues, 'LastCandle:', lastCandle?.close);
|
if (!signalFunction) {
|
||||||
|
console.warn('[Signals] No signal function for indicator type:', indicator.type);
|
||||||
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 {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[calculateMASignal] Result:', signalType, strength);
|
const lastCandle = candles[candles.length - 1];
|
||||||
return { type: signalType, strength, value: close, reasoning };
|
const prevCandle = candles[candles.length - 2];
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return signalFunction(indicator, lastCandle, prevCandle, indicatorValues);
|
||||||
* ATR Signal Calculation
|
|
||||||
*/
|
|
||||||
function calculateATRSignal(indicator, values) {
|
|
||||||
const atr = values?.atr;
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -348,11 +45,10 @@ export function calculateAllIndicatorSignals() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Signals] Calculating for', activeIndicators.length, 'indicators with', candles.length, 'candles');
|
|
||||||
const signals = [];
|
const signals = [];
|
||||||
|
|
||||||
for (const indicator of activeIndicators) {
|
for (const indicator of activeIndicators) {
|
||||||
const IndicatorClass = IR?.[indicator.type];
|
const IndicatorClass = IndicatorRegistry[indicator.type];
|
||||||
if (!IndicatorClass) {
|
if (!IndicatorClass) {
|
||||||
console.log('[Signals] No class for indicator type:', indicator.type);
|
console.log('[Signals] No class for indicator type:', indicator.type);
|
||||||
continue;
|
continue;
|
||||||
@ -362,21 +58,14 @@ export function calculateAllIndicatorSignals() {
|
|||||||
let results = indicator.cachedResults;
|
let results = indicator.cachedResults;
|
||||||
let meta = indicator.cachedMeta;
|
let meta = indicator.cachedMeta;
|
||||||
|
|
||||||
console.log(`[Signals] ${indicator.name}: indicator.cachedResults length = ${results?.length || 0}`);
|
|
||||||
|
|
||||||
if (!results || !meta || results.length !== candles.length) {
|
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);
|
const instance = new IndicatorClass(indicator);
|
||||||
meta = instance.getMetadata();
|
meta = instance.getMetadata();
|
||||||
results = instance.calculate(candles);
|
results = instance.calculate(candles);
|
||||||
console.log(`[Signals] ${indicator.name}: New results length = ${results?.length || 0}`);
|
|
||||||
indicator.cachedResults = results;
|
indicator.cachedResults = results;
|
||||||
indicator.cachedMeta = meta;
|
indicator.cachedMeta = meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Signals]', indicator.type, '- Results length:', results?.length, 'Last result:', results?.[results.length - 1]);
|
|
||||||
|
|
||||||
if (!results || results.length === 0) {
|
if (!results || results.length === 0) {
|
||||||
console.log('[Signals] No results for indicator:', indicator.type);
|
console.log('[Signals] No results for indicator:', indicator.type);
|
||||||
continue;
|
continue;
|
||||||
@ -394,14 +83,10 @@ export function calculateAllIndicatorSignals() {
|
|||||||
} else if (typeof lastResult === 'number') {
|
} else if (typeof lastResult === 'number') {
|
||||||
values = { ma: lastResult };
|
values = { ma: lastResult };
|
||||||
} else {
|
} 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (indicator.type === 'sma') {
|
|
||||||
console.log('[Signals] SMA result:', lastResult, 'values:', values);
|
|
||||||
}
|
|
||||||
|
|
||||||
const signal = calculateIndicatorSignal(indicator, candles, values);
|
const signal = calculateIndicatorSignal(indicator, candles, values);
|
||||||
|
|
||||||
let currentSignal = signal;
|
let currentSignal = signal;
|
||||||
@ -434,95 +119,27 @@ export function calculateAllIndicatorSignals() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = indicator.type?.toUpperCase();
|
signals.push({
|
||||||
const params = indicator.params && typeof indicator.params === 'object'
|
id: indicator.id,
|
||||||
|
name: meta?.name || indicator.type,
|
||||||
|
label: indicator.type?.toUpperCase(),
|
||||||
|
params: indicator.params && typeof indicator.params === 'object'
|
||||||
? Object.entries(indicator.params)
|
? Object.entries(indicator.params)
|
||||||
.filter(([k, v]) => !k.startsWith('_') && v !== undefined && v !== null)
|
.filter(([k, v]) => !k.startsWith('_') && v !== undefined && v !== null)
|
||||||
.map(([k, v]) => `${k}=${v}`)
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
: null;
|
: null,
|
||||||
|
|
||||||
signals.push({
|
|
||||||
id: indicator.id,
|
|
||||||
name: meta?.name || indicator.type,
|
|
||||||
label: label,
|
|
||||||
params: params || null,
|
|
||||||
type: indicator.type,
|
type: indicator.type,
|
||||||
signal: currentSignal.type,
|
signal: currentSignal.type,
|
||||||
strength: Math.round(currentSignal.strength),
|
strength: Math.round(currentSignal.strength),
|
||||||
value: currentSignal.value,
|
value: currentSignal.value,
|
||||||
reasoning: currentSignal.reasoning,
|
reasoning: currentSignal.reasoning,
|
||||||
color: SIGNAL_COLORS[currentSignal.type],
|
color: currentSignal.type === 'buy' ? '#26a69a' : currentSignal.type === 'sell' ? '#ef5350' : '#787b86',
|
||||||
lastSignalDate: lastSignalDate
|
lastSignalDate: lastSignalDate
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Signals] ========== calculateAllIndicatorSignals END ==========');
|
console.log('[Signals] ========== calculateAllIndicatorSignals END ==========');
|
||||||
console.log('[Signals] Total signals calculated:', signals.length);
|
console.log('[Signals] Total signals calculated:', signals.length);
|
||||||
return signals;
|
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