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

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

217 lines
6.3 KiB
JavaScript

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