Skip to content

3-class profit-maximizing gold ML pipeline (triple-barrier, $100k account, feature search) - #2

Open
ataridan7-cpu wants to merge 29 commits into
masterfrom
claude/nice-mendel-3qbdj1
Open

3-class profit-maximizing gold ML pipeline (triple-barrier, $100k account, feature search)#2
ataridan7-cpu wants to merge 29 commits into
masterfrom
claude/nice-mendel-3qbdj1

Conversation

@ataridan7-cpu

@ataridan7-cpu ataridan7-cpu commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Overview

Rewrites gold_direction_ml.ipynb into a both-directions, profit-maximizing pipeline that trades a $100,000 toy account where the model decides direction and size.

Architecture

Labels — 3-class triple-barrier (every bar)

Every bar is an event (no directional pre-filter). Each entry gets two volatility-scaled barriers and a time barrier; the first touched determines the label:

  • +1 (up) — upper barrier entry·(1 + pt_mult·σ) hit first → go long
  • −1 (down) — lower barrier entry·(1 − sl_mult·σ) hit first → go short
  • 0 (flat) — time barrier (max_holding_days) → stay flat

Model — multiclass XGBoost (multi:softprob, 3 classes)

Outputs [P(down), P(flat), P(up)]. Directional edge = P(up) − P(down).

Position sizing — confidence-proportional, compounded

frac = min(1.0, size_scale · |edge|) of current equity (no leverage). Long on edge > tau, short on edge < −tau, flat otherwise. Round-trip costs deducted per trade.

Optimization — pure profit (final account value)

Optuna, the tau/scale grid, and the feature-combo search all maximize final $ equity — no Sharpe penalty.

Feature search — exhaustive (127 subsets) + greedy forward

Searches all non-empty subsets of 7 candidates by OOF profit with quick fixed params. Greedy forward is run for comparison; the exhaustive winner is locked in when they disagree.

Purged walk-forward CV

Expanding folds with purging + embargo (de Prado-style), preventing label-overlap lookahead.

Model comparison

XGBoost vs LightGBM vs multinomial logistic vs LSTM, all scored by $ profit on the untouched holdout. SHAP summary for the XGBoost up-class.

$100k backtest

Daily equity curve, Sharpe / Sortino / max-DD / Calmar, trade stats (hit rate, avg win/loss, profit factor, long vs short counts), and per-regime breakdown (vol regime × trend).

Bug fixes applied (from high-effort code review)

# Issue Fix
1 Sample weights computed on full dataset before split — holdout event windows inflated concurrency counts for late training bars Weights now computed on events.iloc[:split] only
2 Final model trained without early stopping using Optuna's suggested n_estimators ceiling — overtrained vs CV fold models Final fit uses fit_with_es (same early-stopping logic as CV folds)
3 Logistic baseline received no sample_weight — unfair comparison Passes logisticregression__sample_weight=w_tr via pipeline kwargs
4 simulate_from_edges silently accepted entry_times outside events.index, setting last_exit=NaT and disabling the non-overlap guard Raises ValueError immediately on any NaT in the reindexed result
5 Empty trades DataFrame → groupby on zero rows returned silent empty table Guard added with explicit "no trades taken" message
6 subset_profit referenced X_tr which gets narrowed later — partial notebook reruns KeyErrored on the feature search _X_search = X_tr snapshot taken before narrowing
7 t1pos_tr clipped to split−1, causing over-purging of late-training events in early CV folds Clip removed; true label-end positions flow through
8 Greedy search called subset_profit(winner) twice — second call wasted a full OOF CV run Single max(generator) captures both winner and value
9 Sharpe/max-DD computed from uniform-spread equity curve with no caveat Comment added: approximation understates intra-trade drawdowns
10 Regime breakdown accessed Trend_Sign with no empty-trades guard Covered by fix jonathan-bower#5

Runtime

~53 s with n_trials=20, n_splits=5, exhaustive feature search over 127 subsets.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr

claude added 2 commits June 13, 2026 17:50
Fixes applied during review:
- Enable XGBoost early stopping (early_stopping_rounds on constructor) so the
  validation set actually halts training and populates best_iteration.
- Replace the non-stationary raw gold/silver ratio with a rolling z-score that
  trees can use out-of-sample.
- Rework the backtest to use non-overlapping 5-day periods (the prior daily
  compounding of overlapping 5-day returns overstated performance ~5x), and
  add transaction costs.
- Add a majority-class baseline alongside accuracy/ROC-AUC.
- Forward-fill only when fetching data to avoid back-fill lookahead.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Replaces the basic fixed-horizon XGBoost notebook with a full research workflow
(renamed gold_direction_xgboost.ipynb -> gold_direction_ml.ipynb):

- Expanded feature set (~26): momentum, vol/trend regimes, cross-asset
  correlations and momentum spreads, VIX/ratio z-scores, calendar effects.
- Triple-barrier + meta-labeling with volatility-scaled barriers and
  average-uniqueness sample weights (Lopez de Prado).
- Purged, embargoed walk-forward cross-validation.
- Optuna hyperparameter search optimizing out-of-fold strategy Sharpe.
- Probability calibration (isotonic) + leak-free threshold tuning on OOF preds.
- Model comparison (XGBoost vs LightGBM vs logistic) + SHAP explainability.
- Event-driven, cost-aware backtest with Sharpe/Sortino/maxDD/Calmar,
  trade-level stats, and a per-regime breakdown.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
@ataridan7-cpu ataridan7-cpu changed the title Add gold direction XGBoost notebook with leakage and backtest fixes Research-grade gold ML pipeline (triple-barrier, walk-forward CV, Optuna, SHAP) Jun 13, 2026
claude added 12 commits June 13, 2026 19:03
- ARIMA: rolling, leak-free h-step cumulative expected-return forecast fed in as
  a single model feature (refit on a trailing window every N days, forward-filled).
- LSTM: sequence-model benchmark added to the model-comparison section, ingesting
  the last lstm_seq_len days of train-scaled features per event; skipped
  gracefully if TensorFlow is unavailable.
- Deps: add statsmodels and tensorflow-cpu; new CONFIG knobs for both.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Split the large multi-step cells so each cell does one job (a single function
definition, one transformation, or one plot). Adds markdown sub-headers and
section dividers. No behavioral change to the pipeline.
New section 4b (train split only, to respect the holdout):
- feature-feature correlation heatmap
- each feature's correlation with the meta-label target (sorted bar + table)
- redundancy check listing feature pairs with |corr| > 0.8

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Based on the correlation analysis: drop Mom_10 (|corr| 0.86 with MACD) and
SMA_Slope_200 (|corr| 0.83 with Dist_SMA_200, 0.74 with ARIMA_Forecast) to
reduce multicollinearity. Kept Gold_RSI_5 and ARIMA_Forecast.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
- Restore Mom_10 and SMA_Slope_200 to FEATURES (keep full set; let the
  regularized ensemble handle collinearity).
- Fix Sharpe scaling in the model comparison: report non-annualized
  PerTrade_Sharpe (events are multi-day/overlapping, so sqrt(252) was wrong);
  the annualized Sharpe remains in the backtest.
- Document that the comparison uses a neutral 0.5 threshold.
- Avoid redundant refit of xgb_best via a prefit flag in evaluate().
- Cosmetics: drop unused t1_all; simplify benchmark daily returns to pct_change.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Replace ordinal DayOfWeek/Month integers with sin/cos pairs (DOW_sin/cos over a
5-day trading week, Month_sin/cos over 12 months) so the tree no longer treats
Dec/Jan or Fri/Mon as maximally distant. Addresses review item jonathan-bower#4.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Runtime:
- Add make_xgb() helper (tree_method=hist, consistent settings; removes
  boilerplate in 4 places).
- Early stopping inside CV folds via fit_with_es() (validation tail precedes the
  test fold, so no look-ahead) - used in both the Optuna objective and the OOF
  threshold-tuning loop.
- Tighten search: n_estimators 200-600 (was 200-1000), default n_trials 30->20.
- ARIMA arima_refit_every 21->63 (forecasts are forward-filled anyway).

Metric:
- PerTrade_Sharpe in the model comparison now scores executed trades only
  (sig==1), excluding no-trade zeros.

Verified: full notebook runs end-to-end in ~18s.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
…ration CV

- P1: Split proba_ho into proba_ho_raw (xgb_best, consistent scale with best_t)
  and proba_ho_cal (calibrated, for reliability curve only). Threshold tuning
  and backtest now both operate on the raw probability scale, eliminating the
  mismatch where best_t was tuned on uncalibrated OOF scores but applied to
  isotonic-shifted holdout scores.
- P2: Add exit transaction cost (span[-1] -= cost_per_trade) so round-trip costs
  are fully modeled in the backtest.
- P3: Replace CalibratedClassifierCV(cv=3) with TimeSeriesSplit(n_splits=3) so
  calibration folds stay time-ordered within the training set.
- P4: Add clarifying comment to fold_sharpe that non-traded zeros make it a
  ranking metric for Optuna, not a realized per-trade Sharpe.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Gold_RSI dropped in favour of Gold_RSI_5 (shorter window captures same signal
more responsively). DOW_cos and ARIMA_Forecast showed near-zero correlation with
the meta-label and added no model value; removing ARIMA also cuts ~6 s of
rolling-refit compute (12 s vs 19 s full run). ARIMA CONFIG params removed too.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Keeps only the 7 features with highest absolute correlation with the meta-label
(Gold_Vol, High_Vol_Regime, Month_cos, Vol_Ratio, VIX_Z, Gold_SPX_Mom_Spread,
Gold_RSI_5). Removing noise features improved AUC above 0.50 for both tree
models, lifted backtest Sharpe to 2.03, and cut runtime to ~9 s.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Major rewrite per Option C:
- 3-class triple-barrier labels (down/flat/up) on every bar via two-sided
  volatility barriers; model trades long on +1, short on -1, flat on 0.
- Multiclass XGBoost (multi:softprob); edge = P(up) - P(down) drives direction.
- Confidence-proportional position sizing on a compounding $100,000 account
  with no leverage (frac = min(1, size_scale*|edge|) of current equity), one
  position at a time via a shared simulate_from_edges engine.
- Every objective (Optuna, tau/size_scale tuning, feature search) maximizes
  total profit (final account value) rather than Sharpe.
- Feature-combo search over the 7 candidates: exhaustive 127-subset sweep
  (shipped) plus a greedy-forward comparison. Exhaustive wins
  [Gold_Vol, Month_cos, Gold_SPX_Mom_Spread, Gold_RSI_5] by ~$10.7k OOF over
  greedy, which gets stuck on a marginal pick — documented in the notebook.
- Dropped probability calibration (binary-oriented, no profit benefit).
- Multiclass evaluation (3x3 confusion, macro OVR AUC), profit-based model
  comparison, up-class SHAP, and a $100k backtest reporting final account value.

Fixes a datetime64 unit-mismatch bug in the non-overlap guard that had
suppressed all trades. Holdout: $100k -> ~$183k (+83%), Sharpe 1.96 vs 1.13
buy & hold.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Correctness:
- sample weights now computed on training events only (events.iloc[:split]
  over featured.index) so holdout event holding windows cannot inflate
  concurrent-event counts for late training bars (leakage fix)
- final xgb_best now uses fit_with_es (early stopping) to match the tree
  depth of the CV fold models that produced best_params / best_tau / best_scale
- Logistic baseline now receives sample_weight=w_tr via pipeline kwarg,
  making the model-comparison table fair
- simulate_from_edges raises ValueError when entry_times are not in
  events.index, catching the NaT last_exit bug that would silently disable
  the non-overlap guard
- regime breakdown wrapped in trades.empty guard with clear message

Robustness:
- subset_profit now references _X_search (snapshot before X_tr narrowing)
  so partial notebook reruns no longer KeyError on the feature search
- t1pos_tr clip removed; true label-end positions flow through to
  PurgedWalkForwardCV unchanged

Efficiency / clarity:
- greedy search evaluates each candidate once (max with generator) instead
  of calling subset_profit twice per winner
- equity curve comment warns that M**(1/n) spreading is an approximation;
  intra-trade drawdowns may be deeper than Sharpe/max-DD suggest

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
@ataridan7-cpu ataridan7-cpu changed the title Research-grade gold ML pipeline (triple-barrier, walk-forward CV, Optuna, SHAP) 3-class profit-maximizing gold ML pipeline (triple-barrier, $100k account, feature search) Jun 13, 2026
claude added 14 commits June 13, 2026 22:06
n_trials 20→50 in CONFIG. In practice Optuna converged to the same
optimum with seed=42; raising this gives more exploration in production
where the random seed or data range may differ.

Investigated class-frequency reweighting (full and sqrt-inverse) to
address the always-predicts-up collapse, but both approaches forced
shorts in a gold bull market and reduced holdout profit significantly.
The current feature set (|corr| < 0.07 with direction) does not contain
enough short signal to profitably short the holdout period; the long-biased
model with Sharpe 1.63 / max-DD -16% is the genuine optimum for these
features.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Four new macro factor groups replace the weakest original technicals:
- TIP_Z: TIPS ETF return z-score (real yield proxy — gold's strongest
  documented macro driver; gold rallies when real yields fall)
- GDX_Gold_Ratio_Z / GDX_Gold_Mom_Spread: miner/gold ratio z-score and
  21-day momentum spread (miners lead physical gold by days-to-weeks)
- Oil_Mom_21 / Gold_Oil_Ratio_Z: crude oil momentum and gold/oil ratio
  z-score (inflation and commodity-complex demand)
- EURUSD_Mom_21 / EURUSD_Z: EUR/USD 21-day momentum (dollar weakness)

New tickers: TIP, GDX, CL=F, EURUSD=X (GDX available from 2006 so
events start ~2008, giving ~4862 labeled events vs prior 6323).

CANDIDATE_FEATURES updated to 8 features (255 exhaustive subsets):
4 retained technicals + 4 macro factors.

Results on holdout vs prior:
  Final equity:  $157,553 (+57.6%)  vs $140,234 (+40.2%)
  Profit:        +$17,319 improvement
  Sharpe:        1.89  vs 1.63
  Sortino:       2.17  vs 1.57
  Max drawdown: -16.1% unchanged
  Trades:        130 (105 long, 25 short) vs 91 (all long)

TIP_Z (real yield proxy) is now the highest-correlation feature (0.078)
and the exhaustive winner is ['Gold_Vol', 'TIP_Z'] — adding real rates
unlocked genuine short signals (25 shorts, previously 0).

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Replace static 8-feature CANDIDATE_FEATURES with a dynamic selection:
compute each feature's Pearson correlation with the signed direction target
(-1/0/+1) on the training split, take the top-7 by absolute correlation,
and narrow X_tr/X_ho to those columns. This drops Gold_SPX_Mom_Spread
(|corr|=0.0018, lowest of 8) and reduces exhaustive search from 255
to 127 subsets. FEATURE_POOL retains all 8 for the valid-event mask and
X_all construction; CANDIDATE_FEATURES is assigned dynamically in §4b.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
- tau grid: 0.02-0.30/16pts → 0.08-0.50/22pts (wider range, tau floor at 0.08
  prevents overfitting to low-confidence noise trades)
- scale options: add 12, 20, 30 (9 values total, 198 combos vs 80)
- nthread=1 in make_xgb: forces single-threaded execution for full
  reproducibility across runs (parallel float-point ordering was nondeterministic)

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
- Add trades_to_sharpe(): builds daily equity curve from trade ledger,
  returns annualized Sharpe (0.0 for < 2 trades)
- Add simulate_composite(): profit_ratio * max(Sharpe, 0) — rewards strategies
  that are both profitable and risk-adjusted; both must be positive to score well
- Add cv_objective(): OOF composite used by Optuna and tau/scale grid
- Feature search keeps pure profit objective (QUICK_PARAMS are too weak to give
  reliable OOF Sharpe estimates; composite adds noise not signal at that stage)
- Fix empty-trades crash in trade-stats cell (.dt.days replaced with list comprehension)

Result: $158,510 (+58.5%), Sharpe 1.84, Max-DD -11.8%, Calmar 1.08

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
1. CV purge assertion: drop off-by-one (+1) so it matches the strict-< purge mask
2. Fix stale "255 subsets" comment -> top-7 pool yields 127 exhaustive subsets
3. Feature-search prints now label OOF dollars ($) vs Optuna composite (ratio)
4. Title cell + CONFIG comment: "pure profit" -> "profit x Sharpe composite"
5. simulate_composite floors Sharpe at 0.1 (was 0.0) so sparse early folds don't
   zero the composite and bias Optuna toward low tau
6. average_uniqueness_weights asserts get_indexer found all dates (no silent
   inf/NaN weights on calendar misalignment)
7. fit_with_es early-stop val tail = max(50, 15%) via min(0.85*n, n-50), keeping
   the usual 15% on large folds while guaranteeing >=50 samples on small ones
8. simulate_from_edges floors equity at 0 (no negative account on a losing short)

Verified end-to-end: final $154,934 (+54.9%), Sharpe 1.91, Sortino 2.06,
Max-DD -10.6%, Calmar 1.14; feature winner ['TIP_Z', 'Gold_Vol'].

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Holdout trades bucketed by confidence (|edge|): annualized Sharpe per
confidence quintile (left) and per-trade confidence vs realized return
(right). Shows a non-monotonic (inverted-U) relationship — best
risk-adjusted trades sit in the mid-confidence band (|edge|~0.16,
Sharpe ~2.0), with near-zero per-trade correlation (Pearson r=0.00).

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Tests linear vs flat vs capped-edge vs band sizing, each tuned on OOF
edges and evaluated on the holdout. Conclusion: capped/sweet-spot sizing
collapses back to linear under OOF tuning, flat ~= linear, and dropping
the high-confidence tail (band) is robustly worse. Current linear sizing
is retained.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Decomposes confidence into its two roles: entry gate (|edge|>tau) vs
proportional sizing. Finding: the gate is essential (+43% / Sharpe 1.49
with it vs +10% / Sharpe 0.51 without), while proportional sizing is a
no-op (tuned scale saturates every trade to the 100% cap, so it equals
flat-at-max). Confidence is needed as a threshold filter, not a sizing
signal.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
…gate

Ablation showed the entry gate (|edge|>tau) drives nearly all performance
while proportional sizing is a no-op (tuned scale saturated every trade to
the 100% cap). So:

- simulate_from_edges: flat position_frac instead of min(maxfrac, scale*|edge|);
  asymmetric gate with separate tau_long / tau_short thresholds
- CONFIG: drop edge_threshold/size_scale, add tau_long, tau_short, position_frac
- Section 7 now grid-searches (tau_long, tau_short, position_frac) on the OOF
  profit x Sharpe composite (was tau x size_scale)
- All downstream call sites (Optuna, feature search, model comparison, LSTM,
  final backtest) updated to the new 5-arg engine signature
- Markdown/title updated to describe the gate + flat sizing

Verified end-to-end: gate tau_long=0.16 / tau_short=0.10 / frac=1.0,
final $148,460 (+48.5%), Sharpe 2.01 (best of session), Sortino 2.06,
profit factor 1.77, 83 trades.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
…uning, vectorized Sharpe

Part 1 efficiency fixes (runtime 392s -> 94s, 4.2x faster):

- Feature search: cap exhaustive subsets to size <=3 (127->63; winners are
  small) and evaluate them with joblib.Parallel(n_jobs=-1). Each XGBoost fit
  stays nthread=1 so per-fit determinism is preserved. Greedy rounds parallelized
  the same way (round-to-round dependency stays sequential).
- Optuna: inline the fold loop, report the running OOF composite per fold, and add
  a MedianPruner(n_startup_trials=5, n_warmup_steps=1) to abort hopeless trials.
- trades_to_sharpe: vectorize via np.searchsorted positional slicing + cumulative
  log-growth instead of per-trade boolean masks over featured.index (it is called
  1083x in the gate grid).

Verified end-to-end: no exceptions, 46 cells parse, Sharpe 2.63 / +73.9%, results
within the established range.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Root cause of run-to-run instability was the DATA, not the model: yfinance
returns slightly different historical values on repeated calls (same shape and
dates, different values), so every downstream stage drifted. Verified XGBoost
itself is bit-identical across processes.

Fixes (two cached runs are now byte-identical end-to-end):
- fetch_macro_data caches the download to macro_data_cache.pkl (gitignored;
  each environment fetches one snapshot then reuses it; delete to refresh)
- make_xgb uses n_jobs=1 (the deprecated nthread alias was ignored, so fits ran
  multi-threaded with nondeterministic float-summation order); pin OMP/BLAS
  thread env vars to 1 before heavy imports in cell 1
- Seed-bagging (model improvement 2.1): BaggedModel averages directional_edge
  over CONFIG["n_bag"]=5 seed-perturbed fits; oof_edges, the feature search, the
  gate-tuning OOF, and the final model are bagged (Optuna's inner CV stays
  single-seed for speed). SHAP explains bag member 0. Reduces edge variance and
  stabilized the feature winner.

Runtime unchanged (~95-100s; bagged QUICK fits absorbed by the parallel search).

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
…tion

Implements three flagged, A/B-tested improvements and ships the winner:

- 2.3 calibrate_edge (SHIPPED, default ON): isotonic map fit on the leak-free OOF
  edge -> P(up move), re-expressed as 2*P-1 on the same scale; applied to the
  holdout edge so the OOF-tuned gate transfers better. Best robust result:
  $149,229 (+49.2%) vs $138,924 baseline, Sharpe 1.50 vs 1.40, 145 trades.
- 2.4 label_mode="binary" (available, default OFF): up-vs-down head, flat dropped
  from the FIT only (no look-ahead; predicts on all events). Highest composite but
  on ~23 trades with lower profit -> fragile, not shipped.
- 2.5 feature_select="stability" (available, default OFF): keep features whose XGB
  gain exceeds the per-fold mean in >=60% of folds. Marginal alone; degenerate in
  combination, so not shipped.

A/B matrix on the holdout profit×Sharpe composite picked calibration; all flag
combinations overfit the gate to 0 holdout trades, so a single lever ships. Two
consecutive runs are byte-identical (reproducibility preserved; isotonic is
deterministic). Binary/stability branches added to make_xgb, fit_with_es,
oof_edges, directional_edge, §8 metrics, SHAP, and the feature lock.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Merge the daily equity-curve construction into the §10 headline cell so the
summary prints Max DD alongside Final/Profit/Trades. It reuses the same daily
curve as the perf table, so the headline value (-15.3%) matches Max Drawdown
exactly — one consistent number, surfaced on every run.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Add a Max DD column to the §9 model comparison (XGBoost, LightGBM, Logistic,
LSTM), so drawdown shows for every run/model — not just the deployed backtest.
Extract a shared holdout_equity_curve()/max_drawdown() helper used by both §9
and §10, so the XGBoost row's Max DD (-0.1533) is identical to the §10 headline
(-15.3%). The A/B runner (run_cfg.py) also reports maxDD now.

https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants