3-class profit-maximizing gold ML pipeline (triple-barrier, $100k account, feature search) - #2
Open
ataridan7-cpu wants to merge 29 commits into
Open
3-class profit-maximizing gold ML pipeline (triple-barrier, $100k account, feature search)#2ataridan7-cpu wants to merge 29 commits into
ataridan7-cpu wants to merge 29 commits into
Conversation
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
- 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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Rewrites
gold_direction_ml.ipynbinto 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:
entry·(1 + pt_mult·σ)hit first → go longentry·(1 − sl_mult·σ)hit first → go shortmax_holding_days) → stay flatModel — 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 onedge > tau, short onedge < −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)
events.iloc[:split]onlyn_estimatorsceiling — overtrained vs CV fold modelsfit_with_es(same early-stopping logic as CV folds)sample_weight— unfair comparisonlogisticregression__sample_weight=w_trvia pipeline kwargssimulate_from_edgessilently acceptedentry_timesoutsideevents.index, settinglast_exit=NaTand disabling the non-overlap guardValueErrorimmediately on any NaT in the reindexed resulttradesDataFrame →groupbyon zero rows returned silent empty tablesubset_profitreferencedX_trwhich gets narrowed later — partial notebook reruns KeyErrored on the feature search_X_search = X_trsnapshot taken before narrowingt1pos_trclipped tosplit−1, causing over-purging of late-training events in early CV foldssubset_profit(winner)twice — second call wasted a full OOF CV runmax(generator)captures both winner and valueTrend_Signwith no empty-trades guardRuntime
~53 s with
n_trials=20,n_splits=5, exhaustive feature search over 127 subsets.https://claude.ai/code/session_01SbRD1crgrw43xnCTpzRWZr