Backtest de cartera (2020-07-27 → 2022-12-30)
Estrategia: sma_cross · Selector: fuerza de tendencia (cierre / SMA200 − 1)
Parámetros: {'fast': 20, 'slow': 50, 'trend': 200, 'atr_period': 14, 'stop_atr_mult': 2.0, 'take_profit_atr_mult': 3.0, 'max_hold_days': 40, 'calendar_days': False, 'volume_filter_ratio': 0.8, 'entry_window': 5, 'exit_mode': 'take_profit'}
Universo (10): SPY QQQ AAPL MSFT NVDA AMZN GOOGL META JPM XOM · Benchmark: SPY
Métricas por modo de salida
| Símbolo | Periodo | Retorno | CAGR | Max DD | Sharpe | Trades | Win % | Expectancy | Exposición | Buy & Hold |
|---|---|---|---|---|---|---|---|---|---|---|
| PORTFOLIO-REAL2000-IS (take_profit) | 2020-07-27 → 2022-12-30 | +2.4% | +1.0% | -1.1% | 0.91 | 10 | 80% | +3.57% | 23% | +18.3% |
La columna Buy & Hold es SPY en el mismo periodo.
Detalle take_profit
Operaciones cerradas: 10 · Máx. posiciones simultáneas: 2/5 · Señales descartadas por falta de huecos: 0 · por régimen: 0 · por hueco de apertura: 0 · Reglas de riesgo: 0 kills diarios, 0 días vetados por drawdown, 0 señales descartadas, 0 vetadas por anti-PDT (0 day trades) · Precios nominales: sí (0 posiciones atravesaron un split)
Riesgo comparable: SPY a la misma volatilidad (mezcla con efectivo, vol móvil 60 d): +0.3 % · IC 90 % del retorno anualizado (bootstrap por bloques): [-0.1 %, +2.0 %]
| Símbolo | Trades | Win % | P&L | Ret. medio |
|---|---|---|---|---|
| AAPL | 3 | 67% | +4.94 | +1.46% |
| JPM | 2 | 100% | +16.78 | +5.98% |
| XOM | 5 | 80% | +26.54 | +3.87% |
Por año y régimen take_profit
| Grupo | Ops | Win rate | P&L medio | P&L total | Ret. medio |
|---|---|---|---|---|---|
| 2021 | 5 | 100% | +8.42 | +42.11 | +5.15% |
| régimen bull | 7 | 86% | +6.17 | +43.17 | +3.91% |
| 2022 | 5 | 60% | +1.23 | +6.15 | +1.99% |
| régimen bear | 3 | 67% | +1.70 | +5.09 | +2.76% |
Últimas operaciones take_profit
| Entrada | Salida | Precio in | Precio out | Qty | P&L | Ret. | Motivo salida |
|---|---|---|---|---|---|---|---|
| 2021-05-14 | 2021-06-16 | 60.04 | 64.25 | 3 | +12.62 | +7.01% | take-profit: close 64.37 >= 64.10; SMA20=60.60 SMA50=59.05 SMA200=47.50 ATR=1.35 close=64.37 |
| 2021-06-29 | 2021-07-06 | 134.77 | 140.56 | 1 | +5.79 | +4.30% | bracket take-profit |
| 2021-08-18 | 2021-09-27 | 156.27 | 164.75 | 1 | +8.48 | +5.43% | bracket take-profit |
| 2021-10-05 | 2021-11-10 | 62.46 | 65.29 | 3 | +8.48 | +4.52% | take-profit: close 66.36 >= 66.33; SMA20=63.86 SMA50=59.77 SMA200=58.01 ATR=1.29 close=66.36 |
| 2021-11-10 | 2021-11-18 | 150.11 | 156.84 | 1 | +6.74 | +4.49% | bracket take-profit |
| 2022-01-12 | 2022-01-26 | 71.46 | 75.78 | 2 | +8.64 | +6.05% | bracket take-profit |
| 2022-04-06 | 2022-04-18 | 172.37 | 164.78 | 1 | -7.59 | -4.40% | bracket stop-loss |
| 2022-08-19 | 2022-09-23 | 94.19 | 88.55 | 2 | -11.27 | -5.98% | bracket stop-loss |
| 2022-10-20 | 2022-11-07 | 104.26 | 112.32 | 1 | +8.07 | +7.74% | take-profit: close 112.30 >= 112.06; SMA20=105.27 SMA50=98.42 SMA200=89.64 ATR=2.60 close=112.30 |
| 2022-11-01 | 2022-11-11 | 127.06 | 135.36 | 1 | +8.30 | +6.53% | take-profit: close 135.13 >= 134.71; SMA20=124.39 SMA50=116.70 SMA200=124.82 ATR=2.55 close=135.13 |
Lectura honesta
- Sesgo de supervivencia. El universo son símbolos que hoy sabemos que sobrevivieron y subieron; sobre datos de 2019 en adelante eso infla el resultado. Compara siempre contra el benchmark y trata estas cifras como techo, no como expectativa.
- Este backtest de cartera es el que se compara con el paper: mismo capital compartido, mismo límite de posiciones, mismo sizing y mismo selector.
- Paridad con el bot: hueco de apertura > MAX_GAP_PCT descarta la entrada y stop/take-profit se desplazan con el precio de apertura, como en
engine/cycle.py. Lo que el backtest NO reproduce: la cancelación de entradas sin fill (aquí toda entrada se llena en la apertura), los fills parciales y la latencia real del cron. La regla anti-PDT SÍ se simula con--risk-rules(solo veta entradas, como en vivo). - Reglas de pérdida diaria y drawdown solo con
--risk-rules(el workflow automático las activa): con el histórico gratuito (desde 2020-07-27) nunca se disparan; se prueban con un crash sintético entests/test_kill_switch_system.py. - El objetivo de take-profit de la regla al cierre se recalcula cada día con el ATR actual (entrada + 3×ATR), mientras la pata bracket queda fija en la entrada: cuando la volatilidad baja, la regla al cierre vence antes. El bot hace exactamente lo mismo (docs/STRATEGY.md).
- El número de operaciones no es un umbral universal de validez: mira el intervalo de confianza del retorno anualizado (bootstrap por bloques, abajo) y recuerda que esta ventana participó en la selección de variantes.
Cómo se calcula
- Sharpe: retornos diarios de la equity de la cartera, tipo libre de riesgo 0, media ÷ desviación × √252. Sin remuneración del efectivo (la cuenta paper no paga intereses).
- Costes: comisión 0.00 % por lado + 0.00 USD por orden; slippage 0.050 % por lado sobre apertura, stop y objetivo.
- Drawdown máximo: caída desde el máximo previo de la equity diaria al cierre.
- Exposición: % de días con alguna posición abierta. Es tiempo invertido, no capital ni riesgo: no se usa como denominador de nada.
- Benchmark a la misma volatilidad: mezcla benchmark + efectivo cuyo peso sigue la volatilidad móvil de 60 días de la estrategia (solo datos pasados). Descriptivo.
- IC 90 % del retorno anualizado: bootstrap por bloques de 10 días sobre los retornos diarios (1 000 remuestreos). Conserva la dependencia dentro del bloque; NO corrige que la ventana fuera de muestra participó en la selección del modo de salida.
- Precios: indicadores sobre barras ajustadas por splits (IEX); dinero, tamaños enteros, stop y objetivo con precios nominales del día: sí.
- Anti-PDT (con reglas de riesgo): veta entradas nuevas tras 3 operaciones abiertas y cerradas el mismo día en 5 días hábiles si la equity está bajo 25,000 USD; nunca bloquea una salida. Como en vivo.
- Evidencia técnica vs económica: este reporte mide si el sistema hace lo que dice; no demuestra ventaja económica. Esa se plantea aparte, con la observación en paper y estos intervalos, nunca con el retorno acumulado solo.
Reproducir
- Commit del código:
957a040 - Comando:
bot backtest-portfolio --start 2019-01-01 --split 2023-01-01 --cash 2000 --exit-mode take_profit --risk-rules --tag REAL2000 --no-png - Hash de los datos (cachés CSV usadas):
8bfaba63e4ea - Datos conservados en:
backtest-data/2026-09-12 - Ajuste y feed:
split (indicadores) + raw (dinero), feed IEX - alpaca-py:
0.44.0 - Generado:
2026-09-12 04:37 UTC - Mismo commit + mismos archivos de caché + mismo comando → mismo resultado (
tests/test_backtest_reproducible.py). Sin el archivo de datos el hash no sirve.