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>
23 KiB
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
-
Basic Calculation:
- Balance: $10,000
- Risk: 1%
- Entry: 1.08500
- SL: 1.08300 (20 pips)
- Expected: 0.50 lots, $100 risk
-
High Risk:
- Balance: $50,000
- Risk: 5%
- Entry: 1.25800
- SL: 1.25300 (50 pips)
- Expected: 5.00 lots, $2,500 risk
-
Invalid SL (BUY):
- Entry: 1.08500
- SL: 1.08700 (above entry)
- Expected: Red warning, no calculation
-
Invalid SL (SELL):
- Entry: 1.08500
- SL: 1.08300 (below entry)
- Expected: Red warning, no calculation
-
No Take Profit:
- Entry: 1.08500
- SL: 1.08300
- TP: (empty)
- Expected: Calculation works, R:R = 0
-
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