Aviso. GuetaQuant es un portal de educación cuantitativa independiente. Este artículo es material pedagógico sobre implementación de software de gestión de riesgo. No es asesoría financiera, ni recomendación de inversión, ni captación de fondos. Operar en mercados apalancados conlleva riesgo de pérdida total del capital.
Casi todo el material sobre dimensionamiento de posición se detiene en la misma ecuación:
lotes = (balance × riesgo%) / (distancia_stop × valor_por_lote)
Es correcta, y es la parte fácil. El problema no está en la fórmula: está en las tres líneas que vienen después, cuando hay que ajustar ese número al paso de lote y al mínimo que exige el broker.
Ahí es donde encontramos un bug en nuestro propio código. Lo publicamos porque el patrón está en todas partes, y porque el error se concentra exactamente en las cuentas más pequeñas.
1. El bug: MathMax(minLot, lotes)
Este es el cierre habitual de una función de lotaje. Aparece en foros de MQL5, en repositorios de GitHub y estaba en el fragmento que nosotros mismos publicábamos:
double lots = MathFloor(rawLot / lotStep) * lotStep;
return MathMax(minLot, MathMin(maxLot, lots)); // <-- aquí
MathFloor está bien: redondear hacia abajo garantiza no pasarse del presupuesto. El problema es MathMax(minLot, ...).
Cuando el lote presupuestado queda por debajo del mínimo del broker, MathFloor devuelve 0.00. MathMax entonces lo sube a minLot. La función devuelve un lote operable و no dice nada.
Ese lote ya no respeta el presupuesto. Y la función sigue reportando el presupuesto original como si fuera el riesgo asumido.
2. La medición
Cuenta de 500 USD. Riesgo 0.5%. Stop de 500 ticks, valor del tick 1.00 USD.
| Concepto | Valor |
|---|---|
| Presupuesto de riesgo (0.5% de 500) | 2.50 USD |
| Lotes brutos | 0.005 |
floor al paso de 0.01 |
0.00 |
Devuelto tras MathMax(minLot, …)
|
0.01 |
| Riesgo real de esos 0.01 lotes | 5.00 USD |
| Riesgo reportado por la función | 2.50 USD |
El riesgo efectivo es el doble del presupuestado, y nada en la respuesta lo indica.
No es un caso de laboratorio: es una cuenta de 500 USD operando con riesgo conservador. Es el perfil de quien más necesita que el límite se respete.
El mismo problema con round() en lugar de floor()
Una variante frecuente es usar round() en vez de floor(). Buscamos por fuerza bruta el peor caso sobre una malla de balances (500–20.000 USD), stops (5–120 pips) y riesgos (0.5%, 1%, 2%):
| Paso de lote | Peor sobrepaso medido | Caso |
|---|---|---|
| 0.01 | +99.15% | balance 1.175 USD, riesgo 0.5%, stop 117 pips |
| 0.1 | +99.58% | balance 12.025 USD, riesgo 0.5%, stop 120 pips |
En ambos casos los lotes brutos caen justo por encima de la mitad del paso (0.0050 y 0.0501), round() sube al siguiente escalón y el riesgo casi se duplica. El sobrepaso no está acotado a "unos centavos": está acotado a medio paso de lote, que cerca del mínimo equivale a un 100% del presupuesto.
Y un tercer caso: el valor del pip quemado en el código
Muchas implementaciones asumen valor_pip = 10 USD, que solo es cierto para pares con USD como divisa cotizada y lote estándar. En XAUUSD el valor real ronda 1 USD por pip.
Con balance de 10.000 USD, riesgo 1% (presupuesto 100 USD) y stop de 320 pips:
- Asumiendo 10 USD/pip: lotes brutos
0.0312→ riesgo real 9.60 USD. Se arriesga una décima parte de lo previsto, y la estrategia queda infra-dimensionada sin que nadie lo note. - Con el valor real de 1 USD/pip: lotes brutos
0.3125→ riesgo real 99.20 USD. Correcto.
Este falla en silencio y hacia abajo, que es peor de detectar: nadie revisa una función de riesgo porque esté arriesgando de menos.
3. El arreglo
La regla es simple: si el lote presupuestado no llega al mínimo del broker, la operación no cabe dentro del riesgo definido. Subirla a minLot no es un ajuste, es cambiar el presupuesto sin avisar.
La función debe rechazar la entrada y devolver el dato para que decida la persona:
import math
def tamano_posicion(balance, riesgo_pct, sl_ticks, valor_tick,
paso_lote=0.01, lote_min=0.01, lote_max=100.0):
presupuesto = balance * riesgo_pct / 100
perdida_por_lote = sl_ticks * valor_tick
lotes_brutos = presupuesto / perdida_por_lote
lotes = math.floor(lotes_brutos / paso_lote) * paso_lote
if lotes < lote_min:
return {
"operable": False,
"motivo": "el lote presupuestado esta por debajo del minimo del broker",
"lotes_brutos": round(lotes_brutos, 6),
"presupuesto": round(presupuesto, 2),
"riesgo_si_usas_el_minimo": round(lote_min * perdida_por_lote, 2),
}
lotes = min(lotes, lote_max)
riesgo_real = lotes * perdida_por_lote
return {
"operable": True,
"lotes": round(lotes, 2),
"presupuesto": round(presupuesto, 2),
"riesgo_real": round(riesgo_real, 2),
}
Salida real de las dos llamadas:
>>> tamano_posicion(500, 0.5, 500, 1.0)
{'operable': False,
'motivo': 'el lote presupuestado esta por debajo del minimo del broker',
'lotes_brutos': 0.005, 'presupuesto': 2.5, 'riesgo_si_usas_el_minimo': 5.0}
>>> tamano_posicion(10_000, 1.0, 500, 1.0)
{'operable': True, 'lotes': 0.2, 'presupuesto': 100.0, 'riesgo_real': 100.0}
El primer caso no devuelve un lote. Devuelve el motivo y el número que hay que mirar: operar el mínimo cuesta 5.00 USD contra un presupuesto de 2.50. La decisión de aceptar ese riesgo es del trader, no de una línea de código que se lo oculta.
Y en MQL5:
double lots = MathFloor(rawLot / lotStep) * lotStep;
// Si el lote presupuestado no alcanza el minimo del broker, la operacion NO cabe
// dentro del riesgo definido. Subirlo a minLot arriesgaria mas de lo presupuestado.
if(lots < minLot) return 0.0;
return MathMin(maxLot, lots);
4. Tres reglas para revisar tu propia implementación
-
Nunca
round(), siemprefloor(). El sobrepaso deround()llega a medio paso de lote, que cerca del mínimo es ~100% del presupuesto. -
Nunca subir a
minLoten silencio. O se rechaza la entrada, o se devuelve el riesgo efectivo junto con una bandera de que hubo ajuste. Devolver solo el presupuesto teórico es reportar un número falso. -
Nunca quemar el valor del pip. Léelo del símbolo (
SYMBOL_TRADE_TICK_VALUEySYMBOL_TRADE_TICK_SIZEen MQL5). Un10.0fijo rompe en oro, índices y cripto.
5. Nota de transparencia
Este bug estaba en nuestro propio código, en el endpoint público /api/position-size/, que devolvía roundedLots: 0.01 junto a riskAmount: 2.5 cuando el riesgo real era 5.00 USD. Ya está corregido: la respuesta ahora incluye effectiveRiskAmount, effectiveRiskPercent, clampedToMin y clampedToMax, y hay dos pruebas de regresión que fallan si alguien vuelve a ocultar el ajuste.
La calculadora web sí mostraba la advertencia correctamente desde el principio; el fallo estaba en la librería que alimenta la API y en el fragmento MQL5 publicado. Publicamos el error porque un portal que enseña gestión de riesgo y esconde sus propios defectos no sirve para nada.
La calculadora, la fórmula completa y el indicador en MQL5 bajo licencia AGPLv3 están acá:
Calculadora de lotaje MT5 — GuetaQuant
¿Cómo maneja tu implementación el caso de lote por debajo del mínimo? Me interesa saber si alguien lo rechaza, lo clampa o lo reporta.
Top comments (0)