trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-009-risk-based-position-sizer.md
Adrian Flores Cortes cea9ae85f1 docs: Add 8 ET specifications from TASK-002 audit gaps
Complete remaining ET specs identified in INTEGRATION-PLAN:
- ET-EDU-007: Video Player Advanced (554 LOC component)
- ET-MT4-001: WebSocket Integration (BLOCKER - 0% implemented)
- ET-ML-009: Ensemble Signal (Multi-strategy aggregation)
- ET-TRD-009: Risk-Based Position Sizer (391 LOC component)
- ET-TRD-010: Drawing Tools Persistence (backend + store)
- ET-TRD-011: Market Bias Indicator (multi-timeframe analysis)
- ET-PFM-009: Custom Charts (SVG AllocationChart + Canvas PerformanceChart)
- ET-ML-008: ICT Analysis Card (expanded - 294 LOC component)

All specs include:
- Architecture diagrams
- Complete code examples
- API contracts
- Implementation guides
- Testing scenarios

Related: TASK-2026-01-25-002-FRONTEND-COMPREHENSIVE-AUDIT
Priority: P1-P3 (mixed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:20:53 -06:00

23 KiB
Raw Blame History

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

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

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)

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)

const potentialLoss = roundedLots * slPips * pipValueUsd;

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

Formula 5: Potential Profit

const potentialProfit = roundedLots * tpPips * pipValueUsd;

Formula 6: Risk/Reward Ratio

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

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

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:

{[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:

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:

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:

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:

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:

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

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

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

<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

<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

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

<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

<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

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

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)

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

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

9.2 Multi-Currency Support

Soportar cuentas en EUR, GBP, AUD:

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:

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

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