# ET-TRD-009: Risk-Based Position Sizer

**Versión:** 1.0.0
**Fecha:** 2026-01-25
**Epic:** OQI-003 - Trading y Charts
**Componente:** Frontend Component
**Estado:** ✅ Implementado (documentación retroactiva)
**Prioridad:** P3

---

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | ET-TRD-009 |
| **Tipo** | Especificación Técnica |
| **Epic** | OQI-003 |
| **US Relacionada** | US-TRD-009 (Calcular Position Size Basado en Riesgo) |
| **Componente Frontend** | `RiskBasedPositionSizer.tsx` |
| **Líneas de Código** | 391 |
| **Complejidad** | Baja (cálculos matemáticos simples) |

---

## 1. Descripción General

**Risk-Based Position Sizer** es una calculadora avanzada de gestión de riesgo que determina automáticamente el tamaño óptimo de una posición (lot size) basándose en:

- Saldo de cuenta
- Porcentaje de riesgo aceptable
- Distancia al Stop Loss (en pips)
- Valor del pip según el instrumento

Utiliza la fórmula estándar de position sizing:

```
Lot Size = (Account Balance × Risk %) / (Stop Loss Pips × Pip Value)
```

### Beneficios

✅ **Protección de Capital:** Limita pérdidas a un % predefinido del saldo
✅ **Consistencia:** Mismo nivel de riesgo en todas las operaciones
✅ **Psicología:** Elimina decisiones emocionales sobre tamaño de posición
✅ **Risk/Reward:** Calcula ratios automáticamente para evaluar trades
✅ **Escenarios Múltiples:** Compara 0.5%, 1%, 2%, 5% de riesgo simultáneamente

---

## 2. Arquitectura y Fórmulas

### 2.1 Diagrama de Flujo

```
┌────────────────────────────────────────────────────────────┐
│           Risk-Based Position Sizer Flow                   │
├────────────────────────────────────────────────────────────┤
│                                                             │
│  User Inputs:                                               │
│  ┌─────────────────────────────────────────────┐           │
│  │ • Account Balance: $10,000                  │           │
│  │ • Risk Percent: 1%                          │           │
│  │ • Entry Price: 1.08500                      │           │
│  │ • Stop Loss: 1.08300                        │           │
│  │ • Take Profit: 1.08900                      │           │
│  │ • Trade Type: BUY                           │           │
│  └─────────────────┬───────────────────────────┘           │
│                    │                                         │
│                    v                                         │
│  ┌─────────────────────────────────────────────┐           │
│  │  Step 1: Calculate SL Distance (pips)       │           │
│  │  slPips = |entry - sl| / pipValue           │           │
│  │  slPips = |1.08500 - 1.08300| / 0.0001      │           │
│  │  slPips = 20 pips                            │           │
│  └─────────────────┬───────────────────────────┘           │
│                    │                                         │
│                    v                                         │
│  ┌─────────────────────────────────────────────┐           │
│  │  Step 2: Calculate Risk Amount              │           │
│  │  riskAmount = balance × riskPercent / 100   │           │
│  │  riskAmount = $10,000 × 1 / 100             │           │
│  │  riskAmount = $100                           │           │
│  └─────────────────┬───────────────────────────┘           │
│                    │                                         │
│                    v                                         │
│  ┌─────────────────────────────────────────────┐           │
│  │  Step 3: Calculate Lot Size                 │           │
│  │  lots = riskAmount / (slPips × pipValueUsd) │           │
│  │  lots = $100 / (20 × $10)                   │           │
│  │  lots = 0.50                                 │           │
│  └─────────────────┬───────────────────────────┘           │
│                    │                                         │
│                    v                                         │
│  ┌─────────────────────────────────────────────┐           │
│  │  Step 4: Calculate Potential Profit         │           │
│  │  tpPips = |tp - entry| / pipValue           │           │
│  │  tpPips = |1.08900 - 1.08500| / 0.0001      │           │
│  │  tpPips = 40 pips                            │           │
│  │  profit = lots × tpPips × pipValueUsd       │           │
│  │  profit = 0.50 × 40 × $10 = $200            │           │
│  └─────────────────┬───────────────────────────┘           │
│                    │                                         │
│                    v                                         │
│  ┌─────────────────────────────────────────────┐           │
│  │  Step 5: Calculate R:R Ratio                │           │
│  │  rr = profit / potentialLoss                │           │
│  │  rr = $200 / $100 = 2.0                     │           │
│  │  Display: 1:2.0                              │           │
│  └─────────────────┬───────────────────────────┘           │
│                    │                                         │
│                    v                                         │
│  ┌─────────────────────────────────────────────┐           │
│  │  Output:                                     │           │
│  │  • Lot Size: 0.50                            │           │
│  │  • Risk: $100.00                             │           │
│  │  • Max Loss: $100.00                         │           │
│  │  • Potential Profit: $200.00                 │           │
│  │  • R:R Ratio: 1:2.0                          │           │
│  └─────────────────────────────────────────────┘           │
│                                                             │
└────────────────────────────────────────────────────────────┘
```

### 2.2 Fórmulas Matemáticas

#### Formula 1: Pip Distance Calculation

```typescript
const pipValue = symbol.includes('JPY') ? 0.01 : 0.0001;

// SL distance in pips
const slPips = Math.abs(entryPrice - stopLoss) / pipValue;

// TP distance in pips
const tpPips = takeProfit ? Math.abs(takeProfit - entryPrice) / pipValue : 0;
```

**Ejemplos:**
- EURUSD (entry: 1.08500, SL: 1.08300) → 20 pips
- USDJPY (entry: 149.50, SL: 149.00) → 50 pips
- GBPUSD (entry: 1.25800, SL: 1.25600) → 20 pips

#### Formula 2: Risk Amount

```typescript
const riskAmount = (accountBalance * riskPercent) / 100;
```

**Ejemplos:**
- Balance: $10,000 @ 1% → $100 risk
- Balance: $50,000 @ 2% → $1,000 risk
- Balance: $5,000 @ 0.5% → $25 risk

#### Formula 3: Lot Size (Position Size)

```typescript
const pipValueUsd = 10; // $10 per pip per standard lot for majors

const lots = riskAmount / (slPips * pipValueUsd);
const roundedLots = Math.floor(lots * 100) / 100; // Round down to 0.01
```

**Ejemplos:**
- Risk: $100 / (20 pips × $10) → 0.50 lots
- Risk: $1,000 / (50 pips × $10) → 2.00 lots
- Risk: $25 / (10 pips × $10) → 0.25 lots

**Important:** Always round DOWN to prevent exceeding risk tolerance

#### Formula 4: Potential Loss (Verification)

```typescript
const potentialLoss = roundedLots * slPips * pipValueUsd;
```

This should equal `riskAmount` (or slightly less due to rounding)

#### Formula 5: Potential Profit

```typescript
const potentialProfit = roundedLots * tpPips * pipValueUsd;
```

#### Formula 6: Risk/Reward Ratio

```typescript
const riskRewardRatio = potentialProfit / potentialLoss;
```

**Interpretation:**
- R:R = 2.0 → For every $1 risked, gain $2 (1:2 ratio) ✅ Good
- R:R = 1.0 → Equal risk/reward (1:1 ratio) ⚠️ Borderline
- R:R = 0.5 → Risk $2 to gain $1 (1:0.5 ratio) ❌ Bad trade

---

## 3. Component Interface

### 3.1 Props

```typescript
interface RiskBasedPositionSizerProps {
  accountBalance?: number;           // Default: 10000
  defaultSymbol?: string;             // Default: 'EURUSD'
  defaultRiskPercent?: number;        // Default: 1
  onCalculate?: (result: CalculationResult) => void;  // Callback on calc
  onApplyToOrder?: (lots: number) => void;  // Apply to order form
  compact?: boolean;                  // Default: false
}

interface CalculationResult {
  lots: number;                       // Calculated position size
  riskAmount: number;                 // Dollar amount at risk
  potentialLoss: number;              // Max loss (≈ riskAmount)
  potentialProfit: number;            // Potential gain (if TP hit)
  riskRewardRatio: number;            // R:R ratio
  pipValue: number;                   // Dollar value per pip for this lot size
  slPips: number;                     // Distance to SL in pips
  tpPips: number;                     // Distance to TP in pips
}
```

### 3.2 State Variables

```typescript
const [balance, setBalance] = useState<string>('10000');
const [riskPercent, setRiskPercent] = useState<string>('1');
const [symbol, setSymbol] = useState('EURUSD');
const [entryPrice, setEntryPrice] = useState<string>('');
const [stopLoss, setStopLoss] = useState<string>('');
const [takeProfit, setTakeProfit] = useState<string>('');
const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY');
const [copied, setCopied] = useState(false);
```

---

## 4. Features Implementadas

### 4.1 Input Controls

| Campo | Tipo | Validación | Descripción |
|-------|------|------------|-------------|
| **Account Balance** | Number | > 0 | Saldo de cuenta en USD |
| **Risk Percent** | Number | 0.1 - 10% | Porcentaje de riesgo por trade |
| **Trade Direction** | BUY/SELL | Required | Tipo de operación |
| **Entry Price** | Number | > 0 | Precio de entrada |
| **Stop Loss** | Number | Validate logic | SL debe estar "contra" la dirección |
| **Take Profit** | Number | Optional | TP (opcional) |

**Validación de Stop Loss:**
- BUY trades: SL < Entry Price
- SELL trades: SL > Entry Price
- Si inválido → Muestra warning rojo

### 4.2 Quick Risk Presets

Botones rápidos para seleccionar niveles de riesgo comunes:

```typescript
{[0.5, 1, 2, 3].map((pct) => (
  <button onClick={() => setRiskPercent(pct.toString())}>
    {pct}%
  </button>
))}
```

- 0.5% → Conservative (profesionales)
- 1% → Standard (recomendado)
- 2% → Aggressive
- 3% → Very aggressive (no recomendado)

### 4.3 Calculation Result Panel

Muestra en tiempo real:

```
┌─────────────────────────────────────────┐
│ Recommended Position Size               │
│                                         │
│         0.50 lots      📋 [Copy]       │
│                                         │
│ ────────────────────────────────────── │
│ Risk Amount       Max Loss              │
│ $100.00          $100.00                │
│                                         │
│ Potential Profit  R:R Ratio             │
│ $200.00          1:2.00                 │
│ ────────────────────────────────────── │
│ ℹ SL: 20.0 pips • Pip value: $5.00    │
│                                         │
│ [Apply to Order]                        │
└─────────────────────────────────────────┘
```

### 4.4 Risk Scenarios Matrix

Muestra 4 escenarios simultáneos (0.5%, 1%, 2%, 5%):

```
┌─────┬─────┬─────┬─────┐
│ 0.5%│ 1%  │ 2%  │ 5%  │
│0.25 │0.50 │1.00 │2.50 │
│ $50 │$100 │$200 │$500 │
└─────┴─────┴─────┴─────┘
```

Click en cualquier escenario → cambia risk% automáticamente

### 4.5 Copy to Clipboard

Botón "Copy" copia lot size al portapapeles:

```typescript
const handleCopyLots = async () => {
  await navigator.clipboard.writeText(calculation.lots.toFixed(2));
  setCopied(true);
  setTimeout(() => setCopied(false), 2000);
};
```

Muestra checkmark verde por 2 segundos

### 4.6 Apply to Order

Callback para integrar con formularios de orden:

```typescript
const handleApply = () => {
  if (calculation && onApplyToOrder) {
    onApplyToOrder(calculation.lots);
  }
};
```

---

## 5. Cálculos Avanzados

### 5.1 Pip Value Detection

Detecta automáticamente el valor del pip según el par:

```typescript
const pipValue = symbol.includes('JPY') ? 0.01 : 0.0001;
```

- **Major pairs (EURUSD, GBPUSD):** 0.0001 (4 decimales)
- **JPY pairs (USDJPY, EURJPY):** 0.01 (2 decimales)

### 5.2 Pip Value USD (Approximation)

Asume $10 USD per pip per lot para pares mayores:

```typescript
const pipValueUsd = 10; // Approximate for majors
```

**Real pip values (1 standard lot):**
- EURUSD: $10/pip
- GBPUSD: $10/pip
- USDJPY: ~$9.09/pip (variable)
- AUDUSD: $10/pip

**Mejora futura:** API para obtener pip values reales dinámicamente

### 5.3 Lot Size Rounding

Redondea HACIA ABAJO para no exceder riesgo:

```typescript
const roundedLots = Math.floor(lots * 100) / 100;
```

**Ejemplos:**
- 0.5678 → 0.56 lots
- 1.2345 → 1.23 lots
- 0.0199 → 0.01 lots (minimum tradeable)

**Micro lots (0.01) son el tamaño mínimo**

---

## 6. Validaciones

### 6.1 Input Validation

```typescript
const calculation = useMemo(() => {
  const balanceNum = parseFloat(balance) || 0;
  const riskPercentNum = parseFloat(riskPercent) || 0;
  const entry = parseFloat(entryPrice) || 0;
  const sl = parseFloat(stopLoss) || 0;
  const tp = parseFloat(takeProfit) || 0;

  // Required fields
  if (!balanceNum || !riskPercentNum || !entry || !sl) {
    return null;
  }

  // ... calculations
}, [balance, riskPercent, entryPrice, stopLoss, takeProfit, symbol]);
```

### 6.2 Stop Loss Logic Validation

```typescript
const isValidSL = () => {
  const entry = parseFloat(entryPrice) || 0;
  const sl = parseFloat(stopLoss) || 0;
  if (!entry || !sl) return true;

  if (tradeType === 'BUY') {
    return sl < entry;  // SL must be below entry for BUY
  } else {
    return sl > entry;  // SL must be above entry for SELL
  }
};
```

**UI Feedback:**
- Invalid SL → Red border + warning message
- Blocks calculation until fixed

---

## 7. UI Components

### 7.1 Account Balance Input

```tsx
<div className="relative">
  <DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
  <input
    type="number"
    value={balance}
    onChange={(e) => setBalance(e.target.value)}
    className="w-full pl-10 pr-4 py-2.5 bg-gray-900 border border-gray-700 rounded-lg"
  />
</div>
```

### 7.2 Risk Percent Slider + Buttons

```tsx
<input
  type="number"
  value={riskPercent}
  step="0.5"
  min="0.1"
  max="10"
  className="w-full pl-10 pr-4 py-2.5 bg-gray-900 border border-gray-700 rounded-lg"
/>

<div className="flex gap-2 mt-2">
  {[0.5, 1, 2, 3].map((pct) => (
    <button
      onClick={() => setRiskPercent(pct.toString())}
      className={parseFloat(riskPercent) === pct ? 'bg-blue-600' : 'bg-gray-700'}
    >
      {pct}%
    </button>
  ))}
</div>
```

### 7.3 BUY/SELL Toggle

```tsx
<div className="grid grid-cols-2 gap-2">
  <button
    onClick={() => setTradeType('BUY')}
    className={tradeType === 'BUY' ? 'bg-green-600' : 'bg-gray-700'}
  >
    <TrendingUp className="w-4 h-4" />
    BUY
  </button>
  <button
    onClick={() => setTradeType('SELL')}
    className={tradeType === 'SELL' ? 'bg-red-600' : 'bg-gray-700'}
  >
    <TrendingDown className="w-4 h-4" />
    SELL
  </button>
</div>
```

### 7.4 Price Inputs (Entry, SL, TP)

```tsx
<div className="grid grid-cols-3 gap-3">
  <div>
    <label>Entry Price</label>
    <input
      type="number"
      step="0.00001"
      placeholder="1.08500"
      className="font-mono"
    />
  </div>
  <div>
    <label>
      <Shield className="text-red-400" />
      Stop Loss
    </label>
    <input className={!isValidSL() ? 'border-red-500' : ''} />
  </div>
  <div>
    <label>
      <Target className="text-green-400" />
      Take Profit
    </label>
    <input />
  </div>
</div>
```

### 7.5 Result Card

```tsx
<div className="p-4 bg-blue-500/10 border border-blue-500/30 rounded-xl">
  <div className="flex items-center justify-between">
    <span>Recommended Position Size</span>
    <div className="flex items-center gap-2">
      <span className="text-2xl font-bold">{calculation.lots.toFixed(2)}</span>
      <span>lots</span>
      <button onClick={handleCopyLots}>
        {copied ? <Check className="text-green-400" /> : <Copy />}
      </button>
    </div>
  </div>

  <div className="grid grid-cols-2 gap-3 pt-3 border-t">
    <div>
      <div className="text-xs text-gray-500">Risk Amount</div>
      <div className="text-red-400">${calculation.riskAmount.toFixed(2)}</div>
    </div>
    <div>
      <div className="text-xs text-gray-500">Max Loss</div>
      <div className="text-red-400">${calculation.potentialLoss.toFixed(2)}</div>
    </div>
    <div>
      <div className="text-xs text-gray-500">Potential Profit</div>
      <div className="text-green-400">${calculation.potentialProfit.toFixed(2)}</div>
    </div>
    <div>
      <div className="text-xs text-gray-500">R:R Ratio</div>
      <div className="text-white">1:{calculation.riskRewardRatio.toFixed(2)}</div>
    </div>
  </div>

  <button onClick={handleApply} className="w-full py-2.5 bg-blue-600 rounded-lg">
    Apply to Order
  </button>
</div>
```

---

## 8. Uso e Integración

### 8.1 Standalone Mode

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

function TradingPage() {
  return (
    <div>
      <RiskBasedPositionSizer
        accountBalance={15000}
        defaultRiskPercent={1}
        onCalculate={(result) => {
          console.log('Calculated lot size:', result.lots);
        }}
      />
    </div>
  );
}
```

### 8.2 Integration with Order Form

```tsx
import RiskBasedPositionSizer from '@/modules/trading/components/RiskBasedPositionSizer';
import OrderForm from '@/modules/trading/components/OrderForm';

function TradingDashboard() {
  const [calculatedLots, setCalculatedLots] = useState<number | null>(null);

  return (
    <div className="grid grid-cols-2 gap-4">
      {/* Position Sizer */}
      <RiskBasedPositionSizer
        accountBalance={userBalance}
        onApplyToOrder={(lots) => {
          setCalculatedLots(lots);
          // Automatically fill order form
        }}
      />

      {/* Order Form */}
      <OrderForm
        initialLotSize={calculatedLots}
      />
    </div>
  );
}
```

### 8.3 Compact Mode (Sidebar)

```tsx
<RiskBasedPositionSizer
  compact={true}  // Hides scenarios and tips
  accountBalance={userBalance}
/>
```

---

## 9. Mejoras Futuras

### 9.1 Dynamic Pip Values (API Integration)

Actualmente usa $10 aproximado. Mejorar con API real:

```typescript
// Fetch real-time pip values
const pipValueUsd = await tradingService.getPipValue(symbol, accountCurrency);
```

### 9.2 Multi-Currency Support

Soportar cuentas en EUR, GBP, AUD:

```typescript
interface Props {
  accountCurrency?: 'USD' | 'EUR' | 'GBP';
}

const convertedRisk = riskAmount * conversionRate;
```

### 9.3 Advanced Risk Models

- **Kelly Criterion:** Optimal position sizing basado en win rate
- **Fixed Fractional:** Basado en equity drawdown
- **Volatility-Based:** Ajustar según ATR/volatilidad

### 9.4 Presets Persistence

Guardar configuraciones favoritas:

```typescript
const savePreset = async (name: string) => {
  await apiClient.post('/api/risk-presets', {
    name,
    risk_percent: riskPercent,
    default_symbol: symbol
  });
};
```

### 9.5 Risk Analytics

Dashboard de gestión de riesgo:
- Total risk exposure (todas las posiciones abiertas)
- Daily risk limit tracker
- Risk heatmap por símbolo
- Historical risk performance

---

## 10. Testing Scenarios

### Manual Test Cases

1. **Basic Calculation:**
   - Balance: $10,000
   - Risk: 1%
   - Entry: 1.08500
   - SL: 1.08300 (20 pips)
   - **Expected:** 0.50 lots, $100 risk

2. **High Risk:**
   - Balance: $50,000
   - Risk: 5%
   - Entry: 1.25800
   - SL: 1.25300 (50 pips)
   - **Expected:** 5.00 lots, $2,500 risk

3. **Invalid SL (BUY):**
   - Entry: 1.08500
   - SL: 1.08700 (above entry)
   - **Expected:** Red warning, no calculation

4. **Invalid SL (SELL):**
   - Entry: 1.08500
   - SL: 1.08300 (below entry)
   - **Expected:** Red warning, no calculation

5. **No Take Profit:**
   - Entry: 1.08500
   - SL: 1.08300
   - TP: (empty)
   - **Expected:** Calculation works, R:R = 0

6. **JPY Pair:**
   - Symbol: USDJPY
   - Entry: 149.50
   - SL: 149.00 (50 pips)
   - **Expected:** Correct pip value (0.01)

---

## 11. Referencias

- **Componente:** `apps/frontend/src/modules/trading/components/RiskBasedPositionSizer.tsx` (391 líneas)
- **Integración:** `AdvancedOrderEntry.tsx`, `QuickOrderPanel.tsx`
- **US:** `US-TRD-009-risk-based-position-sizer.md`
- **Formula Reference:** [Babypips Position Size Calculator](https://www.babypips.com/tools/position-size-calculator)

---

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

