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