trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-006-indicadores.md
rckrdmrd a7cca885f0 feat: Major platform documentation and architecture updates
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>
2026-01-07 05:33:35 -06:00

864 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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)