SoftTraderFrontend/components/candlestick-chart/indicatorUtils.ts
2026-03-20 11:10:13 -04:00

272 lines
7.4 KiB
TypeScript

import type { Candle, ChartIndicator, IndicatorType } from './chartTypes';
export interface IndicatorData {
index: number;
values: number[];
}
/**
* Calculate Simple Moving Average
*/
export function calculateSMA(candles: Candle[], period: number): IndicatorData[] {
const result: IndicatorData[] = [];
if (!candles || !Array.isArray(candles) || candles.length === 0 || candles.length < period || period < 1) return result;
for (let i = period - 1; i < candles.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) {
sum += candles[i - j].close;
}
result.push({ index: i, values: [sum / period] });
}
return result;
}
/**
* Calculate Exponential Moving Average
*/
export function calculateEMA(candles: Candle[], period: number): IndicatorData[] {
const result: IndicatorData[] = [];
if (!candles || !Array.isArray(candles) || candles.length === 0 || candles.length < period || period < 1) return result;
const multiplier = 2 / (period + 1);
// Start with SMA for first value
let sum = 0;
for (let i = 0; i < period; i++) {
if (!candles[i]) return result;
sum += candles[i].close;
}
let ema = sum / period;
result.push({ index: period - 1, values: [ema] });
// Calculate EMA for remaining candles
for (let i = period; i < candles.length; i++) {
ema = (candles[i].close - ema) * multiplier + ema;
result.push({ index: i, values: [ema] });
}
return result;
}
/**
* Calculate Bollinger Bands
* Returns [middle, upper, lower] bands
*/
export function calculateBollingerBands(
candles: Candle[],
period: number,
stdDev: number
): IndicatorData[] {
const result: IndicatorData[] = [];
if (!candles || !Array.isArray(candles) || candles.length === 0 || candles.length < period || period < 1) return result;
for (let i = period - 1; i < candles.length; i++) {
// Calculate SMA (middle band)
let sum = 0;
for (let j = 0; j < period; j++) {
sum += candles[i - j].close;
}
const middle = sum / period;
// Calculate standard deviation
let sumSquares = 0;
for (let j = 0; j < period; j++) {
const diff = candles[i - j].close - middle;
sumSquares += diff * diff;
}
const std = Math.sqrt(sumSquares / period);
const upper = middle + stdDev * std;
const lower = middle - stdDev * std;
result.push({ index: i, values: [middle, upper, lower] });
}
return result;
}
/**
* Calculate RSI (Relative Strength Index)
*/
export function calculateRSI(candles: Candle[], period: number): IndicatorData[] {
const result: IndicatorData[] = [];
if (!candles || !Array.isArray(candles) || candles.length === 0 || candles.length < period + 1 || period < 1) return result;
// Calculate initial average gains and losses
let avgGain = 0;
let avgLoss = 0;
for (let i = 1; i <= period; i++) {
const change = candles[i].close - candles[i - 1].close;
if (change > 0) {
avgGain += change;
} else {
avgLoss += Math.abs(change);
}
}
avgGain /= period;
avgLoss /= period;
// First RSI value
let rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
let rsi = 100 - 100 / (1 + rs);
result.push({ index: period, values: [rsi] });
// Calculate remaining RSI values using smoothed averages
for (let i = period + 1; i < candles.length; i++) {
const change = candles[i].close - candles[i - 1].close;
const gain = change > 0 ? change : 0;
const loss = change < 0 ? Math.abs(change) : 0;
avgGain = (avgGain * (period - 1) + gain) / period;
avgLoss = (avgLoss * (period - 1) + loss) / period;
rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
rsi = 100 - 100 / (1 + rs);
result.push({ index: i, values: [rsi] });
}
return result;
}
/**
* Calculate MACD (Moving Average Convergence Divergence)
* Returns [macdLine, signalLine, histogram]
*/
export function calculateMACD(
candles: Candle[],
fastPeriod: number,
slowPeriod: number,
signalPeriod: number
): IndicatorData[] {
const result: IndicatorData[] = [];
if (!candles || !Array.isArray(candles) || candles.length === 0 || candles.length < slowPeriod || slowPeriod < 1) return result;
// Calculate fast and slow EMAs
const fastEMA = calculateEMA(candles, fastPeriod);
const slowEMA = calculateEMA(candles, slowPeriod);
if (slowEMA.length === 0) return result;
// Calculate MACD line (fast EMA - slow EMA)
const macdValues: { index: number; value: number }[] = [];
for (const slow of slowEMA) {
const fast = fastEMA.find(f => f.index === slow.index);
if (fast) {
macdValues.push({
index: slow.index,
value: fast.values[0] - slow.values[0],
});
}
}
if (macdValues.length < signalPeriod) return result;
// Calculate signal line (EMA of MACD)
const signalMultiplier = 2 / (signalPeriod + 1);
let signalSum = 0;
for (let i = 0; i < signalPeriod; i++) {
signalSum += macdValues[i].value;
}
let signal = signalSum / signalPeriod;
const macd = macdValues[signalPeriod - 1].value;
result.push({
index: macdValues[signalPeriod - 1].index,
values: [macd, signal, macd - signal],
});
for (let i = signalPeriod; i < macdValues.length; i++) {
const macdVal = macdValues[i].value;
signal = (macdVal - signal) * signalMultiplier + signal;
result.push({
index: macdValues[i].index,
values: [macdVal, signal, macdVal - signal],
});
}
return result;
}
/**
* Get indicator data based on type
*/
export function getIndicatorData(
indicator: ChartIndicator,
candles: Candle[]
): IndicatorData[] {
switch (indicator.type) {
case 'sma':
return calculateSMA(candles, indicator.params.period || 20);
case 'ema':
return calculateEMA(candles, indicator.params.period || 12);
case 'boll':
return calculateBollingerBands(
candles,
indicator.params.period || 20,
indicator.params.stdDev || 2
);
case 'rsi':
return calculateRSI(candles, indicator.params.period || 14);
case 'macd':
return calculateMACD(
candles,
indicator.params.fastPeriod || 12,
indicator.params.slowPeriod || 26,
indicator.params.signalPeriod || 9
);
default:
return [];
}
}
/**
* Get display name for indicator type
*/
export function getIndicatorDisplayName(type: IndicatorType): string {
switch (type) {
case 'sma':
return 'SMA';
case 'ema':
return 'EMA';
case 'boll':
return 'Bollinger Bands';
case 'rsi':
return 'RSI';
case 'macd':
return 'MACD';
case 'volume':
return 'Volume Profile';
default:
return type.toUpperCase();
}
}
/**
* Get indicator parameter label
*/
export function getIndicatorParamLabel(indicator: ChartIndicator): string {
switch (indicator.type) {
case 'sma':
case 'ema':
return `(${indicator.params.period})`;
case 'boll':
return `(${indicator.params.period}, ${indicator.params.stdDev})`;
case 'rsi':
return `(${indicator.params.period})`;
case 'macd':
return `(${indicator.params.fastPeriod}, ${indicator.params.slowPeriod}, ${indicator.params.signalPeriod})`;
default:
return '';
}
}