Golden SkyGolden SkyGuía Técnica

Referencia técnica · Paso 9 · Walk-forward

Folds y permutación

La primera validación: partir la serie en folds consecutivos, agregar correctamente con el PF pooleado (no el promedio de razones), y testear contra un null que preserva la estructura temporal mediante un test de permutación con corrección de Phipson-Smyth.

§0Notación

gp / glgross profit / gross loss
n_validpermutaciones válidas
CVcoef. de variación (std/mean)
roll(·)rotación circular del array
realestadístico observado (PF pooled)
nullestadístico bajo permutación

§1Folds y validez

La serie se parte en sub-períodos consecutivos y el combo se backtestea en cada uno. Dos gates definen cuándo un fold "cuenta". Los tres números son parámetros de entrada — abajo van sus valores por defecto, ajustables por el usuario.

calculate_folds rolling: n_folds ventanas consecutivas no solapadas  // fold_size = n // n_folds
full_test = True → cada fold testea el 100% (train vacío)  // WFV puro, no re-entrena
expanding: el train crece, el test avanza  ·  gap_periods: embargo entre folds
fold válido  ⟺  trades ≥ min_trades_per_fold
se exigen ≥ min_valid_folds folds válidos
n_foldscantidad de folds — default 5 (rango 2–20)
min_trades
_per_fold
trades mínimos para que un fold cuente — default 15
min_valid
_folds
folds válidos mínimos exigidos — default 3

§2PF pooleado

La agregación cross-fold suma las ganancias y pérdidas reales de todos los folds antes de calcular el ratio — nunca promedia los PF por fold.

pool_fold_metrics gp = Σ ( pnl > 0 )  ·  gl = | Σ ( pnl ≤ 0 ) |  // sobre TODOS los folds válidos
pooled_PF = min( gpgl , cap = 10 )
Por qué NO el promedio de los PF por fold. El PF de un fold es una razón (gp/gl). Promediar razones deja que un fold chico distorsione el resultado: si un fold tiene pocos trades y ninguno perdedor, su gl = 0 y su PF se capea en 10 — ese 10 pesa igual que cualquier otro fold en el promedio, aunque lo sostengan 3 trades. El pooled, en cambio, suma las ganancias y pérdidas reales de todos los folds antes de dividir, así que refleja lo que de verdad pasó.
Fold A — 3 trades, todos ganan → gp = 150, gl = 0  ⟹  PF capeado = 10
Fold B — 60 trades → gp = 250, gl = 450  ⟹  PF = 0.56
promedio de PF = (10 + 0.56) / 2 = 5.3 → parece ganador
pooled = (150 + 250) / 450 = 0.89 → pierde plata
El promedio dice 5.3, sostenido por 3 trades; el pooled dice 0.89 sobre los 63 trades reales — invierte el signo de la conclusión. avg/std/min/max quedan como diagnóstico secundario; la decisión usa el pooled. Sharpe y expectancy también se poolean.

§3Test de permutación

¿El PF real supera al de barajar el timing de la señal manteniendo su estructura? El null correcto rota la señal — preservando las rachas — en vez de redibujarla al azar.

Null model · circular_shift

offset ~ Uniforme( 1, n_bars )
señal_nula = roll( señal_real, offset )  // preserva la autocorrelación; rompe el alineamiento señal↔precio
estadístico = PF pooleado( señal_nula )

El iid Bernoulli del legacy redibuja señales independientes → destruye el clustering real y da p-values optimistas. El circular_shift es el null correcto para series autocorrelacionadas.

p-value · Phipson-Smyth (2010)

p = 1 + #{ null ≥ real }1 + n_valid  ·  p ≥ 1n_valid + 1  // nunca 0.0 exacto
significativo si p < ( 1 − confidence )  // confidence 0.95 → p < 0.05

El +1 trata al estadístico real como una realización más del null (no una certeza): sin él, p puede ser 0.0 exacto y propagar FDR p_adj = 0. Solo el top-N por robustness recibe el test (es caro).

§4Corrección FDR

Al testear varios combos, algunos "ganan" por azar. Se aplica Benjamini-Hochberg sobre los p-values del Monte Carlo.

survivors = benjamini_hochberg( p-values del MC del top-N , fdr_alpha = 0.10 )

Limitación conocida: la familia del FDR es el top-N que recibió MC, no el set completo evaluado → la multiplicidad real es mayor que la corregida. Un FDR p al límite (~0.08-0.10) es candidato, no veredicto — el gate final real sigue siendo OOS y transferencia.

§5Robustness score

Un heurístico de ranking que combina rentabilidad pooleada y consistencia entre folds. No es un p-value ni una probabilidad de confianza.

robustness_score si pooled_PF < 1 ∨ Σreturns < 0 ∨ < 2 folds → 0
profitability = min( 1, (pooled_PF − 1) / 2 )  // PF=1→0, PF=2→0.5, PF≥3→1
consistency = 0.25·(1−CVPF) + 0.15·(1−2·stdWR) + 0.40·prof_folds_ratio + 0.20·(1−CVSharpe)
robustness = profitability · consistency  // ∈ [0,1]

Es un score de ranking, no de significancia — la UI no debe leerlo como "0.85 → 85% de confianza". Ordena candidatos; el gate estadístico es el FDR (§4), y los gates finales son OOS y transferencia.

Qué responde: ¿el edge es consistente entre sub-períodos y sobrevive un null que preserva la estructura temporal?   Qué no: comportamiento fuera de muestra (todo el WFV es in-sample) ni en otros mercados. Es la primera de varias herramientas de validación, no la definitiva.

Fuentes:
· domain/walk_forward/folds.py — calculate_folds (rolling/expanding, full_test, gap)
· domain/validation_common/pooled_metrics.py — pool_fold_metrics (Σgp/Σgl)
· domain/walk_forward/monte_carlo.py — permutation_test (circular_shift, Phipson-Smyth)
· domain/walk_forward/robustness.py — robustness_score