# ET-TRD-011: Market Bias Indicator

**Versión:** 1.0.0
**Fecha:** 2026-01-25
**Epic:** OQI-003 - Trading y Charts
**Componente:** Frontend Component + Backend Calculation
**Estado:** 🔴 No Implementado (especificación nueva)
**Prioridad:** P3

---

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | ET-TRD-011 |
| **Tipo** | Especificación Técnica |
| **Epic** | OQI-003 |
| **US Relacionada** | US-TRD-011 (Ver Sesgo de Mercado Multi-Timeframe) |
| **Componente Frontend** | `MarketBiasIndicator.tsx` (a crear) |
| **Componente Backend** | `/api/market-bias/:symbol` (a crear) |
| **Complejidad** | Media (análisis multi-timeframe + cálculos técnicos) |
| **Esfuerzo Estimado** | 2 horas |

---

## 1. Descripción General

**Market Bias Indicator** es un indicador de sesgo de mercado que muestra la dirección predominante del precio a través de múltiples timeframes, utilizando una combinación de:

- **Moving Averages (MA):** Tendencia basada en EMAs (20, 50, 200)
- **RSI:** Momentum (sobrecompra/sobreventa)
- **MACD:** Divergencia y señales de cruce
- **Price Action:** Higher Highs/Lower Lows
- **Volume Trend:** Confirmación de volumen

### Objetivo

Proporcionar una visión rápida y clara del sesgo del mercado en múltiples timeframes (1m, 5m, 15m, 1h, 4h, 1d) para ayudar a los traders a:

✅ Identificar la tendencia dominante
✅ Alinear trades con el sesgo del mercado
✅ Detectar divergencias entre timeframes
✅ Confirmar setups de entrada con bias

---

## 2. Arquitectura del Indicador

### 2.1 Diagrama de Flujo

```
┌──────────────────────────────────────────────────────────┐
│          Market Bias Indicator Architecture              │
├──────────────────────────────────────────────────────────┤
│                                                           │
│  Market Data (BTCUSD, Multiple Timeframes)               │
│         │                                                 │
│         v                                                 │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Fetch Candles for 6 Timeframes                    │  │
│  │  • 1m (last 100 candles)                           │  │
│  │  • 5m (last 100 candles)                           │  │
│  │  • 15m (last 100 candles)                          │  │
│  │  • 1h (last 100 candles)                           │  │
│  │  • 4h (last 100 candles)                           │  │
│  │  • 1d (last 100 candles)                           │  │
│  └──────────────┬─────────────────────────────────────┘  │
│                 │                                         │
│         ┌───────┼───────┬───────┬───────┬───────┐        │
│         │       │       │       │       │       │        │
│         v       v       v       v       v       v        │
│       1m      5m     15m      1h      4h      1d        │
│         │       │       │       │       │       │        │
│         v       v       v       v       v       v        │
│  ┌─────────────────────────────────────────────────┐    │
│  │  Calculate Technical Indicators (per TF)        │    │
│  │  • EMA 20, 50, 200                              │    │
│  │  • RSI (14 period)                              │    │
│  │  • MACD (12, 26, 9)                             │    │
│  │  • Price trend (HH/HL vs LH/LL)                 │    │
│  │  • Volume trend (avg volume vs current)         │    │
│  └──────────────┬──────────────────────────────────┘    │
│                 │                                         │
│                 v                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │  Calculate Bias Score (per TF)                  │    │
│  │  • MA Bias: +1 if price > all MAs, -1 if <     │    │
│  │  • RSI Bias: +1 if > 50, -1 if < 50            │    │
│  │  • MACD Bias: +1 if histogram > 0, -1 if < 0   │    │
│  │  • Trend Bias: +1 if HH/HL, -1 if LH/LL        │    │
│  │  Total Score: Sum / 4 = [-1.0, +1.0]           │    │
│  └──────────────┬──────────────────────────────────┘    │
│                 │                                         │
│                 v                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │  Aggregate Multi-Timeframe Bias                │    │
│  │  • 1m:  +0.75 (BULLISH)                         │    │
│  │  • 5m:  +0.50 (BULLISH)                         │    │
│  │  • 15m: +0.25 (NEUTRAL)                         │    │
│  │  • 1h:  -0.25 (NEUTRAL)                         │    │
│  │  • 4h:  -0.50 (BEARISH)                         │    │
│  │  • 1d:  -0.75 (BEARISH)                         │    │
│  │  Overall: +0.00 (NEUTRAL/CONFLICTED)            │    │
│  └──────────────┬──────────────────────────────────┘    │
│                 │                                         │
│                 v                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │  Frontend Display                               │    │
│  │  • Visual grid (6 timeframes)                   │    │
│  │  • Color coding (green/red/yellow)              │    │
│  │  • Alignment indicator                          │    │
│  │  • Recommended action                           │    │
│  └─────────────────────────────────────────────────┘    │
│                                                           │
└──────────────────────────────────────────────────────────┘
```

---

## 3. Cálculo de Bias Score

### 3.1 Componentes del Score (per Timeframe)

#### Component 1: Moving Average Bias

```typescript
function calculateMABias(close: number, ema20: number, ema50: number, ema200: number): number {
  let score = 0;

  // Price above all MAs = strong bullish
  if (close > ema20 && close > ema50 && close > ema200) {
    score = 1.0;
  }
  // Price below all MAs = strong bearish
  else if (close < ema20 && close < ema50 && close < ema200) {
    score = -1.0;
  }
  // Price between MAs = mixed/neutral
  else if (close > ema20 && close > ema50) {
    score = 0.5;  // Above short-term MAs
  }
  else if (close < ema20 && close < ema50) {
    score = -0.5;  // Below short-term MAs
  }
  else {
    score = 0.0;  // Choppy/neutral
  }

  return score;
}
```

**Interpretation:**
- +1.0 → Price above ALL MAs (strong uptrend)
- +0.5 → Price above short-term MAs (moderate uptrend)
- 0.0 → Price mixed between MAs (neutral/range)
- -0.5 → Price below short-term MAs (moderate downtrend)
- -1.0 → Price below ALL MAs (strong downtrend)

#### Component 2: RSI Bias

```typescript
function calculateRSIBias(rsi: number): number {
  if (rsi > 70) return 1.0;      // Overbought = strong bullish momentum
  if (rsi > 60) return 0.75;     // Bullish momentum
  if (rsi > 50) return 0.5;      // Slight bullish bias
  if (rsi === 50) return 0.0;    // Neutral
  if (rsi > 40) return -0.5;     // Slight bearish bias
  if (rsi > 30) return -0.75;    // Bearish momentum
  return -1.0;                    // Oversold = strong bearish momentum
}
```

#### Component 3: MACD Bias

```typescript
function calculateMACDBias(macdLine: number, signalLine: number, histogram: number): number {
  let score = 0;

  // MACD above signal and histogram positive = bullish
  if (macdLine > signalLine && histogram > 0) {
    score = 1.0;
  }
  // MACD below signal and histogram negative = bearish
  else if (macdLine < signalLine && histogram < 0) {
    score = -1.0;
  }
  // MACD above signal but histogram negative = weakening bullish
  else if (macdLine > signalLine && histogram < 0) {
    score = 0.25;
  }
  // MACD below signal but histogram positive = weakening bearish
  else if (macdLine < signalLine && histogram > 0) {
    score = -0.25;
  }

  return score;
}
```

#### Component 4: Price Action Bias (Higher Highs / Lower Lows)

```typescript
function calculatePriceActionBias(candles: Candle[]): number {
  const last10 = candles.slice(-10);

  const highs = last10.map(c => c.high);
  const lows = last10.map(c => c.low);

  // Count higher highs (HH)
  let HH = 0;
  for (let i = 1; i < highs.length; i++) {
    if (highs[i] > highs[i - 1]) HH++;
  }

  // Count lower lows (LL)
  let LL = 0;
  for (let i = 1; i < lows.length; i++) {
    if (lows[i] < lows[i - 1]) LL++;
  }

  // HH dominate = uptrend
  if (HH > LL + 2) return 1.0;
  if (HH > LL) return 0.5;

  // LL dominate = downtrend
  if (LL > HH + 2) return -1.0;
  if (LL > HH) return -0.5;

  // Equal = neutral/range
  return 0.0;
}
```

### 3.2 Final Bias Score (per Timeframe)

```typescript
function calculateBiasScore(
  candles: Candle[],
  ema20: number,
  ema50: number,
  ema200: number,
  rsi: number,
  macd: MACD
): BiasResult {
  const close = candles[candles.length - 1].close;

  const maBias = calculateMABias(close, ema20, ema50, ema200);
  const rsiBias = calculateRSIBias(rsi);
  const macdBias = calculateMACDBias(macd.macd, macd.signal, macd.histogram);
  const paBias = calculatePriceActionBias(candles);

  // Weighted average (all equal weight for simplicity)
  const totalScore = (maBias + rsiBias + macdBias + paBias) / 4;

  return {
    score: totalScore,  // -1.0 to +1.0
    direction: totalScore > 0.3 ? 'BULLISH' : totalScore < -0.3 ? 'BEARISH' : 'NEUTRAL',
    strength: Math.abs(totalScore) * 100,  // 0-100%
    components: {
      ma: maBias,
      rsi: rsiBias,
      macd: macdBias,
      priceAction: paBias
    }
  };
}
```

---

## 4. API Endpoint

### 4.1 Request

```http
GET /api/market-bias/:symbol
Query params: ?timeframes=1m,5m,15m,1h,4h,1d
```

### 4.2 Response

```json
{
  "symbol": "BTCUSD",
  "timestamp": "2026-01-25T10:30:15Z",
  "biases": [
    {
      "timeframe": "1m",
      "score": 0.75,
      "direction": "BULLISH",
      "strength": 75,
      "components": {
        "ma": 1.0,
        "rsi": 0.75,
        "macd": 1.0,
        "priceAction": 0.25
      },
      "currentPrice": 43250.50,
      "indicators": {
        "ema20": 43100.00,
        "ema50": 42950.00,
        "ema200": 42500.00,
        "rsi": 68.5,
        "macd": {
          "macd": 125.3,
          "signal": 98.7,
          "histogram": 26.6
        }
      }
    },
    {
      "timeframe": "5m",
      "score": 0.50,
      "direction": "BULLISH",
      "strength": 50,
      "components": { /* ... */ }
    },
    {
      "timeframe": "15m",
      "score": 0.25,
      "direction": "NEUTRAL",
      "strength": 25,
      "components": { /* ... */ }
    },
    {
      "timeframe": "1h",
      "score": -0.25,
      "direction": "NEUTRAL",
      "strength": 25,
      "components": { /* ... */ }
    },
    {
      "timeframe": "4h",
      "score": -0.50,
      "direction": "BEARISH",
      "strength": 50,
      "components": { /* ... */ }
    },
    {
      "timeframe": "1d",
      "score": -0.75,
      "direction": "BEARISH",
      "strength": 75,
      "components": { /* ... */ }
    }
  ],
  "overall": {
    "score": 0.00,
    "direction": "CONFLICTED",
    "alignment": "LOW",  // ALIGNED / MODERATE / LOW / CONFLICTED
    "recommendation": "WAIT - Higher timeframes bearish, lower bullish. Wait for alignment."
  }
}
```

---

## 5. Frontend Component

### 5.1 Component Code

```tsx
// apps/frontend/src/modules/trading/components/MarketBiasIndicator.tsx

import React, { useEffect, useState } from 'react';
import { TrendingUp, TrendingDown, Minus, AlertTriangle, CheckCircle2 } from 'lucide-react';
import { apiClient } from '@/lib/apiClient';

interface BiasData {
  timeframe: string;
  score: number;
  direction: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
  strength: number;
  components: {
    ma: number;
    rsi: number;
    macd: number;
    priceAction: number;
  };
}

interface MarketBiasResponse {
  symbol: string;
  biases: BiasData[];
  overall: {
    score: number;
    direction: string;
    alignment: 'ALIGNED' | 'MODERATE' | 'LOW' | 'CONFLICTED';
    recommendation: string;
  };
}

interface MarketBiasIndicatorProps {
  symbol: string;
  timeframes?: string[];
  onBiasChange?: (overall: any) => void;
  refreshInterval?: number;  // Refresh every N seconds
}

export const MarketBiasIndicator: React.FC<MarketBiasIndicatorProps> = ({
  symbol,
  timeframes = ['1m', '5m', '15m', '1h', '4h', '1d'],
  onBiasChange,
  refreshInterval = 60  // Default 60 seconds
}) => {
  const [biasData, setBiasData] = useState<MarketBiasResponse | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchBias();

    // Auto-refresh
    const interval = setInterval(() => {
      fetchBias();
    }, refreshInterval * 1000);

    return () => clearInterval(interval);
  }, [symbol, refreshInterval]);

  const fetchBias = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await apiClient.get(`/api/market-bias/${symbol}`, {
        params: { timeframes: timeframes.join(',') }
      });

      setBiasData(response.data);
      onBiasChange?.(response.data.overall);
    } catch (err) {
      console.error('Error fetching market bias:', err);
      setError('Failed to load market bias');
    } finally {
      setLoading(false);
    }
  };

  const getBiasColor = (direction: string) => {
    switch (direction) {
      case 'BULLISH': return 'text-green-400 bg-green-500/10 border-green-500/30';
      case 'BEARISH': return 'text-red-400 bg-red-500/10 border-red-500/30';
      case 'NEUTRAL': return 'text-yellow-400 bg-yellow-500/10 border-yellow-500/30';
      default: return 'text-gray-400 bg-gray-500/10 border-gray-500/30';
    }
  };

  const getBiasIcon = (direction: string) => {
    switch (direction) {
      case 'BULLISH': return <TrendingUp className="w-4 h-4" />;
      case 'BEARISH': return <TrendingDown className="w-4 h-4" />;
      default: return <Minus className="w-4 h-4" />;
    }
  };

  const getAlignmentIcon = (alignment: string) => {
    if (alignment === 'ALIGNED') return <CheckCircle2 className="w-5 h-5 text-green-400" />;
    if (alignment === 'CONFLICTED') return <AlertTriangle className="w-5 h-5 text-red-400" />;
    return <Minus className="w-5 h-5 text-yellow-400" />;
  };

  if (loading && !biasData) {
    return (
      <div className="bg-gray-800/50 rounded-xl border border-gray-700 p-4">
        <div className="animate-pulse">Loading market bias...</div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="bg-gray-800/50 rounded-xl border border-red-500/30 p-4">
        <span className="text-red-400">{error}</span>
      </div>
    );
  }

  if (!biasData) return null;

  return (
    <div className="bg-gray-800/50 rounded-xl border border-gray-700 p-4 space-y-4">
      {/* Header */}
      <div className="flex items-center justify-between">
        <div>
          <h3 className="font-semibold text-white">Market Bias</h3>
          <p className="text-xs text-gray-500">{symbol} - Multi-Timeframe Analysis</p>
        </div>
        {getAlignmentIcon(biasData.overall.alignment)}
      </div>

      {/* Overall Bias */}
      <div className={`p-3 rounded-lg border ${getBiasColor(biasData.overall.direction)}`}>
        <div className="flex items-center justify-between mb-2">
          <div className="flex items-center gap-2">
            {getBiasIcon(biasData.overall.direction)}
            <span className="font-bold">{biasData.overall.direction}</span>
          </div>
          <span className="text-sm">{biasData.overall.alignment} Alignment</span>
        </div>
        <p className="text-xs opacity-80">{biasData.overall.recommendation}</p>
      </div>

      {/* Timeframe Grid */}
      <div className="grid grid-cols-3 gap-2">
        {biasData.biases.map((bias) => (
          <div
            key={bias.timeframe}
            className={`p-2 rounded-lg border transition-all ${getBiasColor(bias.direction)}`}
          >
            <div className="flex items-center justify-between mb-1">
              <span className="text-xs font-bold">{bias.timeframe}</span>
              {getBiasIcon(bias.direction)}
            </div>

            {/* Strength Bar */}
            <div className="w-full h-1.5 bg-gray-800 rounded-full overflow-hidden mb-1">
              <div
                className={`h-full ${
                  bias.direction === 'BULLISH' ? 'bg-green-500' :
                  bias.direction === 'BEARISH' ? 'bg-red-500' :
                  'bg-yellow-500'
                }`}
                style={{ width: `${bias.strength}%` }}
              />
            </div>

            <div className="text-xs opacity-70">{bias.strength.toFixed(0)}% strength</div>
          </div>
        ))}
      </div>

      {/* Component Breakdown (Expandable) */}
      <details className="text-xs">
        <summary className="cursor-pointer text-gray-500 hover:text-gray-400">
          View Component Breakdown
        </summary>
        <div className="mt-2 space-y-2">
          {biasData.biases.map((bias) => (
            <div key={bias.timeframe} className="p-2 bg-gray-900/50 rounded">
              <div className="font-bold mb-1">{bias.timeframe}</div>
              <div className="grid grid-cols-4 gap-2 text-xs">
                <div>
                  <span className="text-gray-500">MA:</span>
                  <span className={bias.components.ma > 0 ? 'text-green-400' : 'text-red-400'}>
                    {' '}{bias.components.ma.toFixed(2)}
                  </span>
                </div>
                <div>
                  <span className="text-gray-500">RSI:</span>
                  <span className={bias.components.rsi > 0 ? 'text-green-400' : 'text-red-400'}>
                    {' '}{bias.components.rsi.toFixed(2)}
                  </span>
                </div>
                <div>
                  <span className="text-gray-500">MACD:</span>
                  <span className={bias.components.macd > 0 ? 'text-green-400' : 'text-red-400'}>
                    {' '}{bias.components.macd.toFixed(2)}
                  </span>
                </div>
                <div>
                  <span className="text-gray-500">PA:</span>
                  <span className={bias.components.priceAction > 0 ? 'text-green-400' : 'text-red-400'}>
                    {' '}{bias.components.priceAction.toFixed(2)}
                  </span>
                </div>
              </div>
            </div>
          ))}
        </div>
      </details>
    </div>
  );
};

export default MarketBiasIndicator;
```

---

## 6. Uso e Integración

### 6.1 Standalone

```tsx
import MarketBiasIndicator from '@/modules/trading/components/MarketBiasIndicator';

function TradingDashboard() {
  return (
    <div>
      <MarketBiasIndicator
        symbol="BTCUSD"
        timeframes={['1m', '5m', '15m', '1h', '4h', '1d']}
        refreshInterval={60}  // Refresh every 60 seconds
        onBiasChange={(overall) => {
          console.log('Overall bias changed:', overall.direction);
        }}
      />
    </div>
  );
}
```

### 6.2 Integration with Trading Alerts

```tsx
function TradingPage() {
  const handleBiasChange = (overall: any) => {
    if (overall.alignment === 'ALIGNED') {
      showNotification(`${overall.direction} bias aligned across timeframes!`);
    }
  };

  return (
    <MarketBiasIndicator
      symbol="BTCUSD"
      onBiasChange={handleBiasChange}
    />
  );
}
```

---

## 7. Roadmap de Implementación

### Fase 1: Backend API (1h)
1. Crear endpoint `/api/market-bias/:symbol`
2. Implementar cálculo de indicadores (EMA, RSI, MACD)
3. Implementar scoring logic
4. Test con múltiples símbolos

### Fase 2: Frontend Component (1h)
1. Crear `MarketBiasIndicator.tsx`
2. Grid de timeframes con color coding
3. Overall bias display
4. Component breakdown (expandable)
5. Auto-refresh logic

---

## 8. Referencias

- **Componente:** `apps/frontend/src/modules/trading/components/MarketBiasIndicator.tsx` (a crear)
- **Backend:** `apps/backend/src/services/marketBias.service.ts` (a crear)
- **US:** `US-TRD-011-market-bias-indicator.md`

---

**Última actualización:** 2026-01-25
**Responsable:** Frontend Lead + ML Engineer

