Backtest por API: POST /v1/backtest¶
Qué vas a aprender¶
- Cómo se referencia una estrategia: por
strategy_ido en línea contemplate_id+params. - Cómo crear y derivar instancias (
POST /v1/strategies,POST /v1/strategies/{id}/derive) antes de lanzarlas. - Cada bloque del
payloadde un backtest:strategy,data(ventana y modo),execution(lane, costes,target_lots),engine,cache,output,profile,engine_options,budget. - Cómo leer la respuesta:
signature,window_resolved,results[]conrun_metrics,stage_timings,tables; y los bloquesparity,speedupycache. - Un ejemplo real completo: P01 20/50 sobre NVDA 5min en 2024 con los dos motores.
1. La idea: una «orden de trabajo» completa¶
Un backtest por API es como encargar un estudio a un laboratorio. No basta con decir «prueba la estrategia de medias»: el laboratorio necesita saber qué estrategia exacta, sobre qué datos, con qué reglas de ejecución y costes, con qué máquina (motor), qué quieres que te devuelvan y cuánto estás dispuesto a esperar. Cada una de esas preguntas es un bloque del payload:
flowchart LR
P[payload] --> S[strategy<br/>¿qué estrategia?]
P --> D[data<br/>¿qué activo, frecuencia y ventana?]
P --> E[execution<br/>¿qué contrato y costes?]
P --> G[engine<br/>vectorta / nautilus / both]
P --> C[cache<br/>¿qué se da por calculado?]
P --> O[output<br/>¿cuánto detalle?]
P --> F[profile<br/>¿cuántos núcleos y repeticiones?]
P --> X[engine_options<br/>ajustes propios de cada motor]
P --> B[budget<br/>¿cuánto tiempo/memoria? D-33]
Solo strategy, data y execution son obligatorios; engine vale vectorta por defecto y el resto tiene valores por defecto razonables.
2. Antes de lanzar: identidad de la estrategia¶
2.1 Id determinista, nombre libre¶
Una instancia se identifica por un hash de (template_id, formula_version, params completos y ordenados):
Consecuencia práctica: si tú y un compañero creáis por separado «EMA 20/50», obtenéis el mismo id sin coordinaros. El nombre (name) es para personas y se puede cambiar; el id nunca. Ids reales verificados por los tests del repositorio:
| Plantilla + params | strategy_id |
|---|---|
P01 {fast:20, slow:50} |
P01-dc9260 |
P01 {fast:20, slow:100} |
P01-09dd21 |
P02 {period:14, lower:30, upper:70} |
P02-be06fa |
P02 {period:14, lower:25, upper:75} |
P02-bc0dd2 |
Los params se completan antes del hash
{"fast":20} y {"fast":20,"slow":50} son la misma estrategia (se rellena con los defaults) y tienen el mismo id.
2.2 Crear y derivar¶
curl -sS -X POST "$BASE/v1/strategies" -H 'Content-Type: application/json' \
-d @examples/api/06a_create_p01_20_50.request.json | jq '.payload | {strategy_id, name, params}'
Payload real de la petición:
{ "template_id": "P01", "params": { "fast": 20, "slow": 50 },
"name": "EMA cross 20/50 base", "slug": "ema-cross-20-50",
"description": "Punto de partida de la fase A sobre NVDA.", "tags": ["fase-A"] }
Respuesta real (recortada): {"strategy_id": "P01-dc9260", "name": "EMA cross 20/50 base", "params": {"fast": 20, "slow": 50}, "already_existed": false}. Repetirla devolvería already_existed: true, no un duplicado.
curl -sS -X POST "$BASE/v1/strategies/P01-dc9260/derive" -H 'Content-Type: application/json' \
-d @examples/api/06b_derive_p01_20_100.request.json | jq '.payload'
La petición cambia una sola variable: "param_overrides": {"slow": 100}. Respuesta real (recortada):
{
"strategy_id": "P01-09dd21",
"derived_from": "P01-dc9260",
"param_diff_vs_parent": { "slow": { "from": 50, "to": 100 } },
"shared_features_with_parent": ["ema_fast"],
"invalidated_features_vs_parent": ["ema_slow"],
"cache_forecast": {
"expected_feature_hits": ["ema_fast"],
"expected_feature_misses": ["ema_slow"],
"expected_result_hit": false,
"explanation": "Cambiar ['slow'] invalida ['ema_slow']; ['ema_fast'] se reutiliza. Política y ledger se rehacen siempre."
}
}
La Parte V cuenta el lado conceptual en Cómo se define una estrategia y un recorrido completo en Ejemplo completo.
3. La petición, campo a campo¶
Usamos la petición real examples/api/01_backtest_p01_both.request.json.
3.1 strategy — ¿qué estrategia?¶
Dos formas excluyentes (dar ambas ⇒ 422 STRATEGY_SELECTOR_AMBIGUOUS):
| Forma | Ejemplo | Cuándo |
|---|---|---|
| Por id (canónica) | {"strategy_id": "P01-dc9260"} |
La instancia ya existe. |
| En línea | {"template_id": "P01", "params": {"fast": 20, "slow": 50}, "name": "…", "register": true} |
Pruebas rápidas; la API resuelve el id y, si register=true, la registra. |
Con register: false no se persiste la instancia, pero la respuesta sigue llevando el strategy_id: el id se calcula, no se asigna.
3.2 data — ¿qué datos?¶
"data": {
"asset_id": "NVDA", "assets": null, "timeframe": "5min",
"window": { "start": "2024-01-01T00:00:00Z", "end": "2025-01-01T00:00:00Z",
"mode": "reset_flat", "checkpoint_id": null }
}
| Campo | Valores | Significado |
|---|---|---|
asset_id / assets |
uno u otro | Un activo, o un universo multiactivo (Q01–Q09). Ambos ⇒ 422 DATA_SELECTOR_AMBIGUOUS. |
timeframe |
1min, 2min, 5min, 15min, 30min, 1h, 1d |
Frecuencia de barra. |
window.start / end |
RFC 3339 UTC o null |
Intervalo semiabierto [start, end) sobre el cierre de barra. null = límite del dataset. |
window.mode |
reset_flat | continue |
Arranque plano o continuación desde checkpoint. |
window.checkpoint_id |
string | Obligatorio con continue. |
Los tres significados de «cambiar el periodo»
En la conversación de trading «cambiar el periodo» puede ser tres cosas muy distintas, y la API las separa en tres campos:
| Significado | Campo | Qué se invalida |
|---|---|---|
| Ventana de fechas | data.window.start/end |
Con reset_flat, se reutilizan curvas; se rehacen política y contabilidad. |
| Frecuencia de barra (1min → 5min → 1d) | data.timeframe |
Todo salvo los datos base: re-agregación, features, política, ledger. |
| Lookback del indicador (EMA20 → EMA50) | strategy.params |
Solo la feature cuyo parámetro cambió. |
reset_flat frente a continue:
reset_flat |
continue |
|
|---|---|---|
| Estado inicial | Plano: sin posiciones ni órdenes | Recuperado del checkpoint |
| Qué se resetea | Caja a initial_cash, posiciones a 0 y todo el estado de política |
Nada |
| Requisito | Ninguno | Checkpoint compatible (mismo data_hash, formula_version y contrato) o 409 CHECKPOINT_INCOMPATIBLE |
3.3 execution — el contrato SIM-S¶
"execution": {
"lane": "SIM-S", "contract_version": "1", "initial_cash": 100000,
"target_lots": [-1, 0, 1], "cost_scenario": "hypothetical_5bps", "costs": null
}
| Campo | Defecto | Significado |
|---|---|---|
lane |
— | Solo SIM-S en esta versión del contrato. SIM-R ⇒ 501 LANE_UNSUPPORTED. |
contract_version |
— | "1": señal con la barra cerrada, fill a la apertura siguiente, posición inicial plana, sin liquidación final, redondeos explícitos. |
initial_cash |
100000 | Capital inicial. |
target_lots |
[-1, 0, 1] |
Objetivos discretos: corto, plano, largo. Un paso de +1 a −1 es una orden delta de −2. |
cost_scenario |
— | Escenario con nombre: zero o hypothetical_5bps. |
costs |
— | Costes explícitos. Dar ambos ⇒ 422 COSTS_AMBIGUOUS. |
Costes explícitos y escenarios con nombre:
Campo de costs |
Significado |
|---|---|
fee_rate |
Fracción del nocional por fill (0.0005 = 5 pb), redondeada al céntimo con ROUND_HALF_EVEN. |
half_spread_ticks |
Medio spread adverso, en ticks, aplicado al precio. |
slippage_ticks |
Deslizamiento adverso adicional, en ticks. |
| Escenario | fee_rate |
half_spread_ticks |
slippage_ticks |
|---|---|---|---|
zero |
0 | 0 | 0 |
hypothetical_5bps |
0.0005 | 0 | 0 |
Ejemplo de trading
Tu EMA rápida cruza por encima de la lenta al cierre de las 15:30. La señal es +1. No compras a ese cierre (sería ver el futuro de tu propio fill): compras a la apertura de la barra siguiente. Si venías corto (−1), la orden es de +2 lotes. Con hypothetical_5bps pagas 0,05 % del nocional de ese fill.
3.4 engine — ¿qué motor?¶
| Valor | Adaptador por defecto | Para qué |
|---|---|---|
vectorta |
VTA_CPU_LEDGER |
Consultas rápidas y barridos. |
nautilus |
NT_FEATURES (configurable) |
Realismo de órdenes y ejecución. |
both |
los dos, en procesos aislados | Paridad y speedup. Único modo que produce el bloque parity. |
Con both, el orden de ejecución se aleatoriza por parejas AB/BA con semilla 1729 y queda registrado en speedup.ab_order.
3.5 cache, output, profile¶
"cache": { "cache_mode": "WARM_FEATURES", "write_through": true, "checkpoint_out": null },
"output": { "output_mode": "metrics", "include_timings": true, "include_parity": true, "tail_rows": 5 },
"profile": { "profile": "one_core", "repetitions": { "warmup": 3, "measured": 20 } }
| Bloque | Campo clave | Resumen |
|---|---|---|
cache |
cache_mode |
Qué etapas entran en el reloj: COLD_PROCESS_FULL, WARM_DATA, WARM_FEATURES, INTENT_REPLAY, OPT_FULL, APPEND_CONTINUE. |
output |
output_mode |
metrics (métricas + checksums), fills (+ intents y fills completos), full (+ posiciones, equity, modelos a Parquet). |
output |
tail_rows |
0..500 filas finales de equity inline para inspección humana. |
profile |
profile |
one_core, outer_parallel o inner_parallel. Nunca workers>1 e internal_threads>1 a la vez (422 PROFILE_OVERSUBSCRIBED). |
profile |
repetitions |
{"warmup": 3, "measured": 20} para casos cortos; null = una sola ejecución, sin P95. |
Estado de la caché de features (DEV-06b-01)
En los ejemplos regenerados, la caché de features no está cableada al runner. Por eso cache.features_hits sale vacío aunque pidas WARM_FEATURES: declarar un hit que no se sirvió sería una cifra falsa. result_hit es siempre false, que es la promesa que sí se cumple.
3.6 engine_options — ajustes que no cambian el «qué»¶
"engine_options": {
"vectorta": { "kernel": "scalar", "use_batch": true, "streaming_provider": "vectorta_stream" },
"nautilus": { "adapter": "NT_FEATURES", "account_type": "MARGIN", "indicator_provider": "native",
"fill_model": null, "latency_model": null, "use_message_queue": false, "log_level": "ERROR" }
}
| Opción | Nota |
|---|---|
vectorta.kernel |
scalar siempre disponible; avx2/avx512 ⇒ 422 ENGINE_OPTION_UNAVAILABLE con el wheel PyPI; auto cae en escalar sin avisar. La respuesta siempre dice el kernel realmente ejecutado. |
nautilus.adapter |
NT_ONLINE (flujo online completo), NT_FEATURES (defecto: comparte features), NT_INTENT_REPLAY (solo reproducir decisiones). |
nautilus.account_type |
CASH no admite cortos: con -1 en target_lots ⇒ 422 ENGINE_OPTION_ALTERS_CONTRACT. |
nautilus.latency_model, fill_model |
Latencia ≠ 0 o fills probabilísticos alteran SIM-S ⇒ 422 ENGINE_OPTION_ALTERS_CONTRACT. |
nautilus.log_level |
Por encima de ERROR la medida deja de ser comparable (LOGGING_AFFECTS_TIMING). |
3.7 budget — y la decisión D-33¶
Si el caso cabe: 200. Si no cabe pero está dentro de la política de recursos: 202 con job_id. Si supera la política: 507 RESOURCE_LIMIT (ver capítulo 1, sección 5).
4. Lanzarlo¶
import requests
BASE = "http://localhost:8000"
body = {
"schema_version": "v1",
"payload": {
"strategy": {"template_id": "P01", "params": {"fast": 20, "slow": 50}},
"data": {"asset_id": "NVDA", "timeframe": "5min",
"window": {"start": "2024-01-01T00:00:00Z",
"end": "2025-01-01T00:00:00Z", "mode": "reset_flat"}},
"execution": {"lane": "SIM-S", "contract_version": "1",
"initial_cash": 100000, "target_lots": [-1, 0, 1],
"cost_scenario": "hypothetical_5bps"},
"engine": "both",
"engine_options": {"vectorta": {"kernel": "scalar"},
"nautilus": {"adapter": "NT_FEATURES"}},
},
}
r = requests.post(f"{BASE}/v1/backtest", json=body, timeout=900)
env = r.json()
if r.status_code == 200:
p = env["payload"]
for res in p["results"]:
m = res["run_metrics"]
print(res["adapter"], m["n_fills"], m["final_equity"])
elif r.status_code == 202:
print("en cola:", env["payload"]["poll"])
else:
print(r.status_code, env["error"]["code"], env["error"]["details"])
5. La respuesta, bloque a bloque (ejemplo real)¶
Todo lo que sigue sale de examples/api/01_backtest_p01_both.response.json.
5.1 status, signature, window_resolved¶
{
"status": "PASS",
"warnings": [],
"signature": {
"strategy_id": "P01-dc9260", "template_id": "P01", "strategy_name": "EMA cross 20/50 base",
"params": { "fast": 20, "slow": 50 },
"data_hash": "e8b5fc3612fd944a692af21807706f665fb5bec4f1251d1652fbeaabea6595b0",
"formula_version": "1.0.0",
"execution_contract": {
"lane": "SIM-S", "contract_version": "1", "initial_cash": 100000.0,
"target_lots": [-1, 0, 1], "auto_liquidate_at_end": false,
"costs": { "fee_rate": 0.0005, "half_spread_ticks": 0, "slippage_ticks": 0 },
"engine_binding": { "nautilus": { "bar_execution": false,
"next_open_mechanism": "synthetic_open_quote_at_close_ts_plus_1ns", "replay_ts_shift_ns": 1 } }
},
"cache_mode": "WARM_FEATURES", "output_mode": "metrics",
"host_fingerprint_hash": "x86_64|Linux|py3.12.14",
"…": "…"
},
"window_resolved": { "start": "2024-01-02T14:30:00Z", "end": "2024-12-31T21:00:00Z",
"bars": 19550, "warmup_bars": 49, "first_decision_index": 49 }
}
- La firma es el DNI del resultado. Dos resultados solo son comparables si sus firmas coinciden en lo que importa.
window_resolvedtraduce tu ventana a barras reales: pediste desde el 1 de enero, pero la primera barra de 5min de 2024 es la de2024-01-02T14:30:00Z. Hay 19 550 barras y las 49 primeras son calentamiento (la EMA de 50 aún no es válida): la primera decisión se toma en el índice 49.engine_bindingdeclara el truco de Nautilus para el fill a la apertura siguiente: una quote sintética 1 ns después del cierre. Es un artificio de reproducción, no latencia de mercado.
5.2 results[]: un resultado por motor¶
Comparación real de los dos elementos de results:
| Métrica | VectorTA (VTA_CPU_LEDGER) |
Nautilus (NT_FEATURES) |
|---|---|---|
n_bars |
19 550 | 19 550 |
n_intents / n_fills |
328 / 328 | 328 / 328 |
final_equity |
100 009,38 | 100 009,38 |
final_cash |
100 143,67 | 100 143,67 |
final_position |
−1 | −1 |
costs_total |
35,05 | 35,05 |
max_drawdown |
0,0002946 | 0,0002946 |
sharpe_daily |
"<medido>" |
"<medido>" |
end_to_end_wall_ns (una ejecución) |
8 384 965 (≈ 8,4 ms) | 743 774 161 (≈ 744 ms) |
median_wall_ns (20 repeticiones) |
8 330 236 | 986 363 096 |
p95_wall_ns |
21 743 223 | 1 334 579 996 |
Lectura de trader: con 1 lote de NVDA, 328 fills en un año de barras de 5 minutos y 35,05 USD de comisiones, la estrategia acaba prácticamente plana (+9,38 USD sobre 100 000) y corta (−1). Los dos motores dan exactamente la misma contabilidad.
\"<medido>\" no es un número
sharpe_daily no tiene definición en el contrato todavía, así que sale como "<medido>". Si alguna vez ves ese literal convertido en número en un informe, es un bug.
Nautilus añade campos propios:
"native_account_balance": "100006.07", "canonical_cash": 100143.67,
"economic_bars": 19550, "effective_events": 39099, "events_per_second": 58857.459
effective_events ≈ el doble de economic_bars porque Nautilus procesa además la quote de apertura sintética. Por eso comparar «eventos/s» de Nautilus con «barras/s» de VectorTA sería un error de lectura.
stage_timings desglosa el reloj en 18 etapas con nombre (read_decode_ns, feature_build_ns, weights_and_policy_ns, convert_native_ns, simulate_ns, metrics_ns, end_to_end_wall_ns…). Un 0 significa «esta etapa no aplica». Extracto real de Nautilus:
{ "convert_native_ns": 30750226, "load_engine_ns": 23926612, "reset_engine_ns": 1279575,
"simulate_ns": 664299831, "metrics_ns": 20362532, "end_to_end_wall_ns": 743774161, "…": "…" }
La simulación orientada a eventos (simulate_ns ≈ 664 ms) es la que se lleva el tiempo en Nautilus, frente a ≈ 0,46 ms en el ledger vectorial.
tables da, por cada tabla, filas, checksum y ruta Parquet dentro de results/runs/<run_id>/:
"fills": { "rows": 328, "checksum": "3d8554f9d550490a", "path": "results/runs/NVDA_5min_P01-dc9260_VTA_CPU_LEDGER-…/fills.parquet" },
"equity": { "rows": 19550, "checksum": "9e70399f11e376d1", "path": "results/runs/…/equity.parquet" }
equity_tail son las últimas tail_rows filas (real, última fila de VectorTA):
{ "timestamp": "2024-12-31T21:00:00Z", "cash_base": 100143.67, "equity_base": 100009.38,
"gross_exposure": 134.29, "net_exposure": -134.29, "costs_total": 35.05 }
5.3 parity, speedup, cache¶
"parity": {
"verdict": "PASS",
"compared": { "a": "VTA_CPU_LEDGER", "b": "NT_FEATURES" },
"comparisons": [
{ "what": "features", "mode": "tolerance", "from_index": 49, "verdict": "PASS", "n_compared": 39032, "max_abs_diff": 0.0 },
{ "what": "signals", "mode": "exact", "from_index": 49, "verdict": "PASS", "n_compared": 19501 },
{ "what": "fills", "mode": "exact", "verdict": "PASS", "n_compared": 328 },
{ "what": "equity", "mode": "quantized", "verdict": "PASS", "n_compared": 19550, "max_abs_diff_minor": 0 }
],
"first_divergence": null
},
"speedup": {
"published": true,
"value": 118.40757420272521,
"definition": "median_wall_nautilus / median_wall_vta_pipeline",
"gates": { "parity": "PASS", "same_data_hash": true, "…": "9 puertas en verde" },
"ab_order": "AB",
"interpretation": "Un valor > 1 favorece al pipeline VectorTA + ledger para este caso concreto."
},
"cache": { "features_hits": [], "features_misses": ["ema:20", "ema:50"], "result_hit": false, "…": "…" },
"run_id": "NVDA_5min_P01-dc9260_VTA_CPU_LEDGER-4a894cc067c94483-20260917T151218071527091"
El speedup es la división de medianas: 986 363 096 / 8 330 236 ≈ 118,4. Solo existe porque parity.verdict = PASS; si la paridad fallara, published sería false y value null. La paridad y las nueve puertas se estudian en el capítulo 4 y el capítulo 6.
Ese 118× es de este caso
Es la mediana de un caso concreto (P01, NVDA 5min, 2024, WARM_FEATURES, one_core, en la máquina de la regeneración). En otro servidor o con otro caso medirá distinto. No lo cites como «VectorTA es 118 veces más rápido» sin sus condiciones.
6. Variaciones útiles (ejemplos reales)¶
| Ejemplo | Qué cambia | Resultado real |
|---|---|---|
02a |
P02-be06fa (RSI 14, 30/70), NVDA 5min, both |
PASS, 550 fills, equity final 99 921,49 en ambos motores |
02c |
P02-bc0dd2 (umbrales 25/75) | PASS, 342 fills, equity 99 942,12; features_misses: ["rsi:14"] (caché no cableada) |
04 |
Mismo P01-dc9260 pero asset_id: "KO", COLD_PROCESS_FULL |
PASS, 19 551 barras, 389 fills, equity 99 965,88 |
06d |
P01-09dd21 (20/100) | PASS, warmup_bars: 99, 197 fills, equity 100 064,26 |
El data_hash de 04 (KO) es el mismo que el de NVDA
examples/api/README.md (paseo 4) dice que al cambiar de NVDA a KO «el data_hash cambia». En los ficheros regenerados no cambia: 01 (NVDA, 19 550 barras) y 04 (KO, 19 551 barras) llevan el mismo e8b5fc36…95b0. El motivo está en el código: finazbench/runner/from_api.py (data_hash()) firma con el data_hash del manifiesto canónico del dataset completo, no con un hash por activo y timeframe. Hoy, por tanto, data_hash identifica la versión del dataset descargado, y lo que distingue NVDA de KO en la firma es asset_id. Es una incoherencia entre el README de ejemplos (y la intención del contrato) y la implementación; no la uses para detectar un cambio de activo.
Cambiar solo los umbrales del RSI
Entre 02a y 02c solo cambian lower/upper, que son parámetros de política, no de feature. El RSI14 es el mismo; cambian las señales y, por tanto, los fills (550 → 342). Un hit del resultado completo sería un bug: compartir indicadores no implica compartir posiciones ni costes.
Errores típicos de POST /v1/backtest
404 STRATEGY_NOT_FOUND, 404 ASSET_NOT_FOUND, 409 STRATEGY_FORMULA_STALE, 409 INSUFFICIENT_HISTORY, 409 CHECKPOINT_INCOMPATIBLE, 422 ENGINE_OPTION_ALTERS_CONTRACT, 422 ENGINE_OPTION_UNAVAILABLE, 422 PROFILE_OVERSUBSCRIBED, 501 STRATEGY_UNSUPPORTED_BY_ADAPTER, 504 TIME_BUDGET_EXCEEDED, 507 RESOURCE_LIMIT.
7. Checklist de lectura de una respuesta¶
flowchart TD
A[Respuesta 200] --> B{status == PASS?}
B -- no --> B1[Lee status y warnings<br/>no mires tiempos]
B -- sí --> C{engine == both?}
C -- no --> F[run_metrics + stage_timings]
C -- sí --> D{parity.verdict == PASS?}
D -- no --> D1[first_divergence<br/>capítulo 4]
D -- sí --> E[speedup.published y gates]
E --> F
F --> G[run_id → results/runs/…]
Resumen¶
POST /v1/backtestrecibe una orden de trabajo completa:strategy,data,execution(obligatorios) yengine,cache,output,profile,engine_options,budget.- La estrategia se referencia por
strategy_iddeterminista o en línea (template_id+params); crear y derivar son unPOSTcada uno. - SIM-S (versión actual del contrato): señal al cierre, fill a la apertura siguiente, sin liquidación final, lotes discretos
[-1, 0, 1]. - La respuesta trae
signature,window_resolved, unresults[]por motor, y conbothlos bloquesparityyspeedup. - En el ejemplo real P01 20/50 NVDA 5min 2024 los dos motores coinciden al céntimo (328 fills, equity 100 009,38) y el speedup publicado es ≈ 118,4 para ese caso.
Para practicar¶
- Lanza
01_backtest_p01_both.request.jsoncon"engine": "vectorta". ¿Qué bloques desaparecen de la respuesta? - Cambia
cost_scenarioa"zero". ¿Cambian_fills? ¿Yfinal_equity? Razona por qué. - Pide P01 en línea con
{"fast": 50, "slow": 20}. ¿Qué error recibes y qué campo señala? - Añade
"account_type": "CASH"a las opciones de Nautilus manteniendotarget_lots: [-1, 0, 1]. Explica el error. - Compara
window_resolved.warmup_barsentre P01 20/50 y P01 20/100. ¿De qué depende?