272 lines
7.4 KiB
TypeScript
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 '';
|
|
}
|
|
}
|