--- 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 = { 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>>(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 { 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(null); const requestIdRef = useRef(0); const callbacksRef = useRef 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 => { 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)