Changes include: - Updated architecture documentation - Enhanced module definitions (OQI-001 to OQI-008) - ML integration documentation updates - Trading strategies documentation - Orchestration and inventory updates - Docker configuration updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
864 lines
23 KiB
Markdown
864 lines
23 KiB
Markdown
---
|
||
id: "ET-TRD-006"
|
||
title: "Especificación Técnica - Technical Indicators"
|
||
type: "Technical Specification"
|
||
status: "Done"
|
||
priority: "Alta"
|
||
epic: "OQI-003"
|
||
project: "trading-platform"
|
||
version: "1.0.0"
|
||
created_date: "2025-12-05"
|
||
updated_date: "2026-01-04"
|
||
---
|
||
|
||
# ET-TRD-006: Especificación Técnica - Technical Indicators
|
||
|
||
**Version:** 1.0.0
|
||
**Fecha:** 2025-12-05
|
||
**Estado:** Pendiente
|
||
**Épica:** [OQI-003](../_MAP.md)
|
||
**Requerimiento:** RF-TRD-006
|
||
|
||
---
|
||
|
||
## Resumen
|
||
|
||
Esta especificación detalla la implementación de indicadores técnicos para análisis de mercado: SMA, EMA, RSI, MACD y Bollinger Bands, incluyendo fórmulas matemáticas, optimización de cálculos y integración con Lightweight Charts.
|
||
|
||
---
|
||
|
||
## Arquitectura
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ FRONTEND CHART │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ Lightweight Charts │ │
|
||
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
|
||
│ │ │ Candlestick│ │ Indicators │ │ Volume │ │ │
|
||
│ │ │ Series │ │ Overlays │ │ Series │ │ │
|
||
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
└────────────────────────────────┬────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ INDICATOR CALCULATION SERVICE │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ Web Worker (Optional) │ │
|
||
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||
│ │ │ SMA │ │ EMA │ │ RSI │ │ MACD │ │ │
|
||
│ │ │Calculator│ │Calculator│ │Calculator│ │Calculator│ │ │
|
||
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
|
||
│ │ │ │ │
|
||
│ │ ▼ │ │
|
||
│ │ ┌──────────────┐ │ │
|
||
│ │ │ Bollinger │ │ │
|
||
│ │ │ Bands │ │ │
|
||
│ │ └──────────────┘ │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ DATA INPUT │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ OHLCV Data (Klines/Candles) │ │
|
||
│ │ - Open, High, Low, Close, Volume │ │
|
||
│ │ - Timestamp array │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Interfaces TypeScript
|
||
|
||
```typescript
|
||
// types/indicators.types.ts
|
||
|
||
export interface OHLCV {
|
||
time: number; // Unix timestamp
|
||
open: number;
|
||
high: number;
|
||
low: number;
|
||
close: number;
|
||
volume: number;
|
||
}
|
||
|
||
export interface IndicatorValue {
|
||
time: number;
|
||
value: number;
|
||
}
|
||
|
||
export interface MACDValue {
|
||
time: number;
|
||
macd: number;
|
||
signal: number;
|
||
histogram: number;
|
||
}
|
||
|
||
export interface BollingerBandsValue {
|
||
time: number;
|
||
upper: number;
|
||
middle: number;
|
||
lower: number;
|
||
}
|
||
|
||
export interface IndicatorParams {
|
||
period?: number;
|
||
stdDev?: number; // For Bollinger Bands
|
||
fastPeriod?: number; // For MACD
|
||
slowPeriod?: number; // For MACD
|
||
signalPeriod?: number; // For MACD
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Implementación de Indicadores
|
||
|
||
### 1. Simple Moving Average (SMA)
|
||
|
||
**Descripción:** Promedio simple de precios durante N períodos.
|
||
|
||
**Fórmula:**
|
||
```
|
||
SMA = (P1 + P2 + ... + Pn) / n
|
||
|
||
Donde:
|
||
Pi = Precio en el período i
|
||
n = Número de períodos
|
||
```
|
||
|
||
**Implementación:**
|
||
|
||
```typescript
|
||
// services/indicators/sma.ts
|
||
|
||
export function calculateSMA(
|
||
data: OHLCV[],
|
||
period: number = 20,
|
||
source: 'close' | 'open' | 'high' | 'low' = 'close'
|
||
): IndicatorValue[] {
|
||
if (data.length < period) {
|
||
return [];
|
||
}
|
||
|
||
const result: IndicatorValue[] = [];
|
||
|
||
for (let i = period - 1; i < data.length; i++) {
|
||
let sum = 0;
|
||
|
||
for (let j = 0; j < period; j++) {
|
||
sum += data[i - j][source];
|
||
}
|
||
|
||
const sma = sum / period;
|
||
|
||
result.push({
|
||
time: data[i].time,
|
||
value: sma,
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
// Optimized version with sliding window
|
||
export function calculateSMAOptimized(
|
||
data: OHLCV[],
|
||
period: number = 20,
|
||
source: 'close' | 'open' | 'high' | 'low' = 'close'
|
||
): IndicatorValue[] {
|
||
if (data.length < period) {
|
||
return [];
|
||
}
|
||
|
||
const result: IndicatorValue[] = [];
|
||
let sum = 0;
|
||
|
||
// Calculate initial sum
|
||
for (let i = 0; i < period; i++) {
|
||
sum += data[i][source];
|
||
}
|
||
|
||
result.push({
|
||
time: data[period - 1].time,
|
||
value: sum / period,
|
||
});
|
||
|
||
// Sliding window
|
||
for (let i = period; i < data.length; i++) {
|
||
sum = sum - data[i - period][source] + data[i][source];
|
||
result.push({
|
||
time: data[i].time,
|
||
value: sum / period,
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. Exponential Moving Average (EMA)
|
||
|
||
**Descripción:** Promedio móvil que da más peso a los precios recientes.
|
||
|
||
**Fórmula:**
|
||
```
|
||
EMA = (Close - EMA_prev) × Multiplier + EMA_prev
|
||
|
||
Donde:
|
||
Multiplier = 2 / (period + 1)
|
||
EMA_prev = EMA del período anterior
|
||
Para el primer valor: EMA = SMA
|
||
```
|
||
|
||
**Implementación:**
|
||
|
||
```typescript
|
||
// services/indicators/ema.ts
|
||
|
||
export function calculateEMA(
|
||
data: OHLCV[],
|
||
period: number = 20,
|
||
source: 'close' | 'open' | 'high' | 'low' = 'close'
|
||
): IndicatorValue[] {
|
||
if (data.length < period) {
|
||
return [];
|
||
}
|
||
|
||
const result: IndicatorValue[] = [];
|
||
const multiplier = 2 / (period + 1);
|
||
|
||
// Calculate initial SMA as first EMA value
|
||
let ema = 0;
|
||
for (let i = 0; i < period; i++) {
|
||
ema += data[i][source];
|
||
}
|
||
ema = ema / period;
|
||
|
||
result.push({
|
||
time: data[period - 1].time,
|
||
value: ema,
|
||
});
|
||
|
||
// Calculate EMA for remaining data
|
||
for (let i = period; i < data.length; i++) {
|
||
ema = (data[i][source] - ema) * multiplier + ema;
|
||
result.push({
|
||
time: data[i].time,
|
||
value: ema,
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. Relative Strength Index (RSI)
|
||
|
||
**Descripción:** Indicador de momentum que mide la velocidad y cambio de movimientos de precio.
|
||
|
||
**Fórmula:**
|
||
```
|
||
RSI = 100 - (100 / (1 + RS))
|
||
|
||
Donde:
|
||
RS = Average Gain / Average Loss
|
||
Average Gain = Sum of gains over period / period
|
||
Average Loss = Sum of losses over period / period
|
||
```
|
||
|
||
**Implementación:**
|
||
|
||
```typescript
|
||
// services/indicators/rsi.ts
|
||
|
||
export function calculateRSI(
|
||
data: OHLCV[],
|
||
period: number = 14
|
||
): IndicatorValue[] {
|
||
if (data.length < period + 1) {
|
||
return [];
|
||
}
|
||
|
||
const result: IndicatorValue[] = [];
|
||
const gains: number[] = [];
|
||
const losses: number[] = [];
|
||
|
||
// Calculate price changes
|
||
for (let i = 1; i < data.length; i++) {
|
||
const change = data[i].close - data[i - 1].close;
|
||
gains.push(change > 0 ? change : 0);
|
||
losses.push(change < 0 ? Math.abs(change) : 0);
|
||
}
|
||
|
||
// Calculate initial average gain and loss
|
||
let avgGain = 0;
|
||
let avgLoss = 0;
|
||
|
||
for (let i = 0; i < period; i++) {
|
||
avgGain += gains[i];
|
||
avgLoss += losses[i];
|
||
}
|
||
|
||
avgGain /= period;
|
||
avgLoss /= period;
|
||
|
||
// Calculate first RSI
|
||
let rs = avgGain / (avgLoss || 1);
|
||
let rsi = 100 - 100 / (1 + rs);
|
||
|
||
result.push({
|
||
time: data[period].time,
|
||
value: rsi,
|
||
});
|
||
|
||
// Calculate RSI for remaining data using Wilder's smoothing
|
||
for (let i = period; i < gains.length; i++) {
|
||
avgGain = (avgGain * (period - 1) + gains[i]) / period;
|
||
avgLoss = (avgLoss * (period - 1) + losses[i]) / period;
|
||
|
||
rs = avgGain / (avgLoss || 1);
|
||
rsi = 100 - 100 / (1 + rs);
|
||
|
||
result.push({
|
||
time: data[i + 1].time,
|
||
value: rsi,
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. Moving Average Convergence Divergence (MACD)
|
||
|
||
**Descripción:** Indicador de tendencia que muestra la relación entre dos EMAs.
|
||
|
||
**Fórmula:**
|
||
```
|
||
MACD Line = EMA(12) - EMA(26)
|
||
Signal Line = EMA(9) of MACD Line
|
||
Histogram = MACD Line - Signal Line
|
||
```
|
||
|
||
**Implementación:**
|
||
|
||
```typescript
|
||
// services/indicators/macd.ts
|
||
|
||
export function calculateMACD(
|
||
data: OHLCV[],
|
||
fastPeriod: number = 12,
|
||
slowPeriod: number = 26,
|
||
signalPeriod: number = 9
|
||
): MACDValue[] {
|
||
if (data.length < slowPeriod) {
|
||
return [];
|
||
}
|
||
|
||
// Calculate fast and slow EMAs
|
||
const fastEMA = calculateEMA(data, fastPeriod);
|
||
const slowEMA = calculateEMA(data, slowPeriod);
|
||
|
||
// Calculate MACD line
|
||
const macdLine: IndicatorValue[] = [];
|
||
const startIndex = slowPeriod - 1;
|
||
|
||
for (let i = 0; i < slowEMA.length; i++) {
|
||
const fastIndex = i + (slowPeriod - fastPeriod);
|
||
macdLine.push({
|
||
time: slowEMA[i].time,
|
||
value: fastEMA[fastIndex].value - slowEMA[i].value,
|
||
});
|
||
}
|
||
|
||
// Calculate signal line (EMA of MACD line)
|
||
const signalLine = calculateEMAFromValues(macdLine, signalPeriod);
|
||
|
||
// Calculate histogram and combine results
|
||
const result: MACDValue[] = [];
|
||
|
||
for (let i = 0; i < signalLine.length; i++) {
|
||
const macdIndex = i + signalPeriod - 1;
|
||
result.push({
|
||
time: signalLine[i].time,
|
||
macd: macdLine[macdIndex].value,
|
||
signal: signalLine[i].value,
|
||
histogram: macdLine[macdIndex].value - signalLine[i].value,
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function calculateEMAFromValues(
|
||
values: IndicatorValue[],
|
||
period: number
|
||
): IndicatorValue[] {
|
||
if (values.length < period) {
|
||
return [];
|
||
}
|
||
|
||
const result: IndicatorValue[] = [];
|
||
const multiplier = 2 / (period + 1);
|
||
|
||
// Initial SMA
|
||
let ema = 0;
|
||
for (let i = 0; i < period; i++) {
|
||
ema += values[i].value;
|
||
}
|
||
ema = ema / period;
|
||
|
||
result.push({
|
||
time: values[period - 1].time,
|
||
value: ema,
|
||
});
|
||
|
||
// EMA calculation
|
||
for (let i = period; i < values.length; i++) {
|
||
ema = (values[i].value - ema) * multiplier + ema;
|
||
result.push({
|
||
time: values[i].time,
|
||
value: ema,
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5. Bollinger Bands
|
||
|
||
**Descripción:** Bandas de volatilidad basadas en desviación estándar.
|
||
|
||
**Fórmula:**
|
||
```
|
||
Middle Band = SMA(20)
|
||
Upper Band = SMA(20) + (2 × Standard Deviation)
|
||
Lower Band = SMA(20) - (2 × Standard Deviation)
|
||
|
||
Standard Deviation = sqrt(sum((Close - SMA)²) / period)
|
||
```
|
||
|
||
**Implementación:**
|
||
|
||
```typescript
|
||
// services/indicators/bollinger-bands.ts
|
||
|
||
export function calculateBollingerBands(
|
||
data: OHLCV[],
|
||
period: number = 20,
|
||
stdDev: number = 2
|
||
): BollingerBandsValue[] {
|
||
if (data.length < period) {
|
||
return [];
|
||
}
|
||
|
||
const result: BollingerBandsValue[] = [];
|
||
const sma = calculateSMAOptimized(data, period);
|
||
|
||
for (let i = 0; i < sma.length; i++) {
|
||
const dataIndex = i + period - 1;
|
||
|
||
// Calculate standard deviation
|
||
let sumSquaredDiff = 0;
|
||
for (let j = 0; j < period; j++) {
|
||
const diff = data[dataIndex - j].close - sma[i].value;
|
||
sumSquaredDiff += diff * diff;
|
||
}
|
||
|
||
const standardDeviation = Math.sqrt(sumSquaredDiff / period);
|
||
const deviation = standardDeviation * stdDev;
|
||
|
||
result.push({
|
||
time: data[dataIndex].time,
|
||
upper: sma[i].value + deviation,
|
||
middle: sma[i].value,
|
||
lower: sma[i].value - deviation,
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Servicio de Indicadores
|
||
|
||
```typescript
|
||
// services/indicator.service.ts
|
||
|
||
import { calculateSMAOptimized } from './indicators/sma';
|
||
import { calculateEMA } from './indicators/ema';
|
||
import { calculateRSI } from './indicators/rsi';
|
||
import { calculateMACD } from './indicators/macd';
|
||
import { calculateBollingerBands } from './indicators/bollinger-bands';
|
||
|
||
export class IndicatorService {
|
||
static calculate(
|
||
type: string,
|
||
data: OHLCV[],
|
||
params: IndicatorParams
|
||
): any {
|
||
switch (type) {
|
||
case 'SMA':
|
||
return calculateSMAOptimized(data, params.period || 20);
|
||
|
||
case 'EMA':
|
||
return calculateEMA(data, params.period || 20);
|
||
|
||
case 'RSI':
|
||
return calculateRSI(data, params.period || 14);
|
||
|
||
case 'MACD':
|
||
return calculateMACD(
|
||
data,
|
||
params.fastPeriod || 12,
|
||
params.slowPeriod || 26,
|
||
params.signalPeriod || 9
|
||
);
|
||
|
||
case 'BB':
|
||
return calculateBollingerBands(
|
||
data,
|
||
params.period || 20,
|
||
params.stdDev || 2
|
||
);
|
||
|
||
default:
|
||
throw new Error(`Unknown indicator type: ${type}`);
|
||
}
|
||
}
|
||
|
||
static getDefaultParams(type: string): IndicatorParams {
|
||
const defaults: Record<string, IndicatorParams> = {
|
||
SMA: { period: 20 },
|
||
EMA: { period: 20 },
|
||
RSI: { period: 14 },
|
||
MACD: { fastPeriod: 12, slowPeriod: 26, signalPeriod: 9 },
|
||
BB: { period: 20, stdDev: 2 },
|
||
};
|
||
|
||
return defaults[type] || {};
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Integración con Lightweight Charts
|
||
|
||
```typescript
|
||
// hooks/useIndicators.ts
|
||
|
||
import { useEffect, useRef } from 'react';
|
||
import { IChartApi, ISeriesApi } from 'lightweight-charts';
|
||
import { useTradingStore } from '../stores/trading.store';
|
||
import { useChartStore } from '../stores/chart.store';
|
||
import { IndicatorService } from '../services/indicator.service';
|
||
|
||
export function useIndicators(chart: IChartApi | null) {
|
||
const seriesRefs = useRef<Map<string, ISeriesApi<any>>>(new Map());
|
||
const { klines } = useTradingStore();
|
||
const { indicators } = useChartStore();
|
||
|
||
useEffect(() => {
|
||
if (!chart || !klines.length) return;
|
||
|
||
// Remove old series
|
||
seriesRefs.current.forEach((series, id) => {
|
||
if (!indicators.find((ind) => ind.id === id)) {
|
||
chart.removeSeries(series);
|
||
seriesRefs.current.delete(id);
|
||
}
|
||
});
|
||
|
||
// Add/Update indicators
|
||
indicators.forEach((indicator) => {
|
||
if (!indicator.visible) {
|
||
const series = seriesRefs.current.get(indicator.id);
|
||
if (series) {
|
||
chart.removeSeries(series);
|
||
seriesRefs.current.delete(indicator.id);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Calculate indicator data
|
||
const data = IndicatorService.calculate(
|
||
indicator.type,
|
||
klines,
|
||
indicator.params
|
||
);
|
||
|
||
// Create or update series
|
||
let series = seriesRefs.current.get(indicator.id);
|
||
|
||
if (!series) {
|
||
series = createIndicatorSeries(chart, indicator);
|
||
seriesRefs.current.set(indicator.id, series);
|
||
}
|
||
|
||
// Set data based on indicator type
|
||
if (indicator.type === 'MACD') {
|
||
// MACD requires multiple series (line + histogram)
|
||
updateMACDSeries(chart, indicator.id, data);
|
||
} else if (indicator.type === 'BB') {
|
||
// Bollinger Bands requires 3 lines
|
||
updateBollingerBandsSeries(chart, indicator.id, data);
|
||
} else {
|
||
// Simple line series (SMA, EMA, RSI)
|
||
series.setData(data);
|
||
}
|
||
});
|
||
}, [chart, klines, indicators]);
|
||
|
||
return seriesRefs.current;
|
||
}
|
||
|
||
function createIndicatorSeries(
|
||
chart: IChartApi,
|
||
indicator: ChartIndicator
|
||
): ISeriesApi<any> {
|
||
const colors = {
|
||
SMA: '#2962FF',
|
||
EMA: '#FF6D00',
|
||
RSI: '#9C27B0',
|
||
MACD: '#00BCD4',
|
||
BB: '#4CAF50',
|
||
};
|
||
|
||
const color = indicator.color || colors[indicator.type];
|
||
|
||
if (indicator.type === 'RSI') {
|
||
// RSI uses a separate pane
|
||
return chart.addLineSeries({
|
||
color,
|
||
lineWidth: 2,
|
||
priceScaleId: 'rsi',
|
||
priceFormat: {
|
||
type: 'custom',
|
||
formatter: (price: number) => price.toFixed(2),
|
||
},
|
||
});
|
||
}
|
||
|
||
return chart.addLineSeries({
|
||
color,
|
||
lineWidth: 2,
|
||
priceLineVisible: false,
|
||
});
|
||
}
|
||
|
||
function updateMACDSeries(
|
||
chart: IChartApi,
|
||
indicatorId: string,
|
||
data: MACDValue[]
|
||
) {
|
||
// Implementation for MACD with histogram
|
||
// Requires creating macdLine, signalLine, and histogram series
|
||
}
|
||
|
||
function updateBollingerBandsSeries(
|
||
chart: IChartApi,
|
||
indicatorId: string,
|
||
data: BollingerBandsValue[]
|
||
) {
|
||
// Implementation for Bollinger Bands
|
||
// Requires creating upper, middle, and lower band series
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Web Worker para Cálculos
|
||
|
||
Para optimizar el rendimiento con grandes volúmenes de datos:
|
||
|
||
```typescript
|
||
// workers/indicator.worker.ts
|
||
|
||
import { IndicatorService } from '../services/indicator.service';
|
||
|
||
self.onmessage = (e: MessageEvent) => {
|
||
const { type, data, params, requestId } = e.data;
|
||
|
||
try {
|
||
const result = IndicatorService.calculate(type, data, params);
|
||
|
||
self.postMessage({
|
||
requestId,
|
||
success: true,
|
||
data: result,
|
||
});
|
||
} catch (error: any) {
|
||
self.postMessage({
|
||
requestId,
|
||
success: false,
|
||
error: error.message,
|
||
});
|
||
}
|
||
};
|
||
```
|
||
|
||
**Uso del Worker:**
|
||
|
||
```typescript
|
||
// hooks/useIndicatorWorker.ts
|
||
|
||
import { useRef, useCallback } from 'react';
|
||
|
||
export function useIndicatorWorker() {
|
||
const workerRef = useRef<Worker | null>(null);
|
||
const requestIdRef = useRef(0);
|
||
const callbacksRef = useRef<Map<number, (result: any) => void>>(new Map());
|
||
|
||
useEffect(() => {
|
||
workerRef.current = new Worker(
|
||
new URL('../workers/indicator.worker.ts', import.meta.url)
|
||
);
|
||
|
||
workerRef.current.onmessage = (e) => {
|
||
const { requestId, success, data, error } = e.data;
|
||
const callback = callbacksRef.current.get(requestId);
|
||
|
||
if (callback) {
|
||
if (success) {
|
||
callback(data);
|
||
} else {
|
||
console.error('Worker error:', error);
|
||
}
|
||
callbacksRef.current.delete(requestId);
|
||
}
|
||
};
|
||
|
||
return () => {
|
||
workerRef.current?.terminate();
|
||
};
|
||
}, []);
|
||
|
||
const calculate = useCallback(
|
||
(type: string, data: any[], params: any): Promise<any> => {
|
||
return new Promise((resolve) => {
|
||
const requestId = ++requestIdRef.current;
|
||
callbacksRef.current.set(requestId, resolve);
|
||
|
||
workerRef.current?.postMessage({
|
||
requestId,
|
||
type,
|
||
data,
|
||
params,
|
||
});
|
||
});
|
||
},
|
||
[]
|
||
);
|
||
|
||
return { calculate };
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Testing
|
||
|
||
```typescript
|
||
// __tests__/indicators.test.ts
|
||
|
||
import { calculateSMAOptimized } from '../services/indicators/sma';
|
||
import { calculateEMA } from '../services/indicators/ema';
|
||
import { calculateRSI } from '../services/indicators/rsi';
|
||
|
||
describe('Indicators', () => {
|
||
const mockData: OHLCV[] = [
|
||
{ time: 1, open: 100, high: 105, low: 95, close: 102, volume: 1000 },
|
||
{ time: 2, open: 102, high: 108, low: 100, close: 106, volume: 1200 },
|
||
{ time: 3, open: 106, high: 110, low: 104, close: 108, volume: 1100 },
|
||
// ... more data
|
||
];
|
||
|
||
describe('SMA', () => {
|
||
it('should calculate SMA correctly', () => {
|
||
const result = calculateSMAOptimized(mockData, 2);
|
||
expect(result.length).toBe(mockData.length - 1);
|
||
expect(result[0].value).toBe((102 + 106) / 2);
|
||
});
|
||
|
||
it('should return empty array if insufficient data', () => {
|
||
const result = calculateSMAOptimized(mockData.slice(0, 1), 2);
|
||
expect(result).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('EMA', () => {
|
||
it('should calculate EMA correctly', () => {
|
||
const result = calculateEMA(mockData, 3);
|
||
expect(result.length).toBeGreaterThan(0);
|
||
expect(result[0].value).toBeCloseTo((102 + 106 + 108) / 3, 2);
|
||
});
|
||
});
|
||
|
||
describe('RSI', () => {
|
||
it('should calculate RSI between 0 and 100', () => {
|
||
const result = calculateRSI(mockData, 14);
|
||
result.forEach((value) => {
|
||
expect(value.value).toBeGreaterThanOrEqual(0);
|
||
expect(value.value).toBeLessThanOrEqual(100);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Performance Optimizations
|
||
|
||
1. **Memoización de Cálculos:**
|
||
```typescript
|
||
const memoizedSMA = useMemo(
|
||
() => calculateSMAOptimized(klines, period),
|
||
[klines, period]
|
||
);
|
||
```
|
||
|
||
2. **Incremental Updates:**
|
||
```typescript
|
||
// Solo recalcular último valor cuando llega nueva vela
|
||
function updateIndicatorIncremental(
|
||
previousValues: IndicatorValue[],
|
||
newCandle: OHLCV
|
||
): IndicatorValue[] {
|
||
// Implementation...
|
||
}
|
||
```
|
||
|
||
3. **Debouncing:**
|
||
```typescript
|
||
const debouncedCalculate = useMemo(
|
||
() => debounce((data) => calculateIndicators(data), 100),
|
||
[]
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## Referencias
|
||
|
||
- [Technical Analysis Library](https://github.com/anandanand84/technicalindicators)
|
||
- [Investopedia - Technical Indicators](https://www.investopedia.com/terms/t/technicalindicator.asp)
|
||
- [Lightweight Charts - Custom Studies](https://tradingview.github.io/lightweight-charts/docs/series-types)
|