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:
DiTus
2026-03-01 19:39:28 +01:00
parent fdab0a3faa
commit a344a7f0da
14 changed files with 883 additions and 644 deletions

View File

@ -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 };

View File

@ -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'
};
}
}

View File

@ -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 };

View File

@ -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'
};
}
}

View File

@ -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 };

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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'
};
}
}

View File

@ -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 };

View 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 };

View File

@ -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 };

View File

@ -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'
};
}
}

View File

@ -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 };