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>
23 KiB
| id | title | type | status | priority | epic | project | version | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|---|
| ET-TRD-006 | Especificación Técnica - Technical Indicators | Technical Specification | Done | Alta | OQI-003 | trading-platform | 1.0.0 | 2025-12-05 | 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 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
// 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:
// 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:
// 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:
// 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:
// 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:
// 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
// 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
// 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:
// 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:
// 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
// __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
- Memoización de Cálculos:
const memoizedSMA = useMemo(
() => calculateSMAOptimized(klines, period),
[klines, period]
);
- Incremental Updates:
// Solo recalcular último valor cuando llega nueva vela
function updateIndicatorIncremental(
previousValues: IndicatorValue[],
newCandle: OHLCV
): IndicatorValue[] {
// Implementation...
}
- Debouncing:
const debouncedCalculate = useMemo(
() => debounce((data) => calculateIndicators(data), 100),
[]
);