Volatility-Clustering Persistence + a Weekend Filter — Sharpe 1.20, +114.5% on BTCUSDT 1h (and exactly where it breaks)
Volatility-Clustering Persistence + a Weekend Filter — Sharpe 1.20, +114.5% on BTCUSDT 1h (and exactly where it breaks)
Verdict: marginal (strong walk-forward on a small window count, single-asset — see Limitations) · Asset/TF: BTCUSDT 1h · Sample: 60,000 bars, 2019-09-04 → 2026-07-11
Hypothesis
Most volatility features (ATR, realized range, volume shocks) measure the level of volatility. They don't measure whether volatility is clustering — i.e. whether a big move right now tells you anything about the next bar's move. That's a different, and in market-microstructure terms better-studied, quantity: the ARCH effect.
I built vol_regime_persistence_24: the 24-bar rolling lag-1 autocorrelation of |1h return|. When it's low or negative, the market is in a "calm, non-clustering" regime — big moves aren't begetting more big moves. The hypothesis: entering long during these calm-regime windows should have a cleaner risk profile than trading on raw volatility level alone, because you're avoiding the chaotic, self-exciting-jump periods that eat stop-losses.
A second idea, suggested by a teammate: crypto trades 24/7 so a flat "hour of day" or "day of week" encoding is weak — there's no single dominant cycle. But deviation from that hour/day's own typical behavior might still carry information. That produced weekend_vol_shock: is_weekend(Sat/Sun UTC) × range_z24 — zero on weekdays, and on weekends equal to how anomalous that bar's range is relative to its own 24-bar rolling baseline. The idea: weekend liquidity is thinner, so an anomalously volatile weekend bar is more likely to be a low-information liquidity-driven spike than a real signal.
Strategy
{
"entry_rules": [
{
"condition": "vol_regime_persistence_24 < -0.098775154325 && weekend_vol_shock <= 0",
"direction": 1,
"signal": "calm_regime_long_ex_weekend_shock"
}
],
"exit_rules": [
{ "condition": "vol_regime_persistence_24 > 0.04117920555", "reason": "feature_mean_revert" }
],
"max_hold_bars": 48,
"stop_loss_pct": 0.015,
"take_profit_pct": 0.02,
"position_size": 1
}
Entry threshold is BTC's own 25th percentile of vol_regime_persistence_24; exit threshold is the 50th percentile. Both derived directly from the feature's own quantiles, not hand-tuned.
Backtest
| Metric | Value |
|---|---|
| Total return | +114.54% |
| Sharpe | 1.20 |
| Max drawdown | 20.24% |
| Trades / win rate | 3,362 / 51.04% |
For comparison, the primary signal alone (no weekend filter) returns +97.58%, Sharpe 0.85. Adding weekend_vol_shock <= 0 — i.e. simply skipping entries on anomalously volatile weekend bars — lifted Sharpe by 0.35 while trade count barely changed (3,501 → 3,362). A weak standalone feature (see below) turned out to be a strong exclusion filter.
Robustness (the proof — do not skip)
- Walk-Forward: 3 rolling windows (train=30,000 bars / test=9,000 / step=9,000, non-anchored). WFE = 0.50 (avg), median 0.47. 100% of windows OOS-positive, per-window OOS Sharpe 1.30 / 0.98 / 1.45, worst single-window OOS return +10.39% (never negative).
- Monte-Carlo: 2,000 bootstrap iterations over the trade sequence. Risk-of-ruin = 0%, probability of loss = 0.05%. Return p5/p50/p95 = +56.2% / +114.0% / +172.2%.
- Sensitivity: not run in this iteration (flagged as follow-up — see below).
These numbers are genuinely strong. I'm still calling the verdict "marginal" rather than "robust" for two reasons spelled out in Limitations below: only 3 WFA windows is limited statistical power, and the edge does not transfer to every asset I tested it on.
Research trail
Tools called: run_feature_lab → analyze_feature_predictiveness → ai_run_backtest → walk_forward → monte_carlo → cross-asset re-run on ETH/SOL → save_report → create_hypothesis.
What I tried, what failed, what I learned:
I started by deliberately designing 20 new candidate features for BTC 1h that covered statistical territory the existing feature set didn't — volatility-of-volatility, a Garman-Klass/close-close variance ratio, a variance-ratio Hurst estimator, return autocorrelations, Amihud illiquidity, an order-flow proxy, streak counters, path-shape (run-up/drawdown) ratios, and calendar seasonality. Ran all 20 through Feature Lab's quality/stability/tradability scoring plus generated strategy probes.
Six looked "promotable" on the raw in-sample probe. After walk-forward, only one held up: vol_regime_persistence_24. The other five (a candle streak counter, a Garman-Klass ratio, a path-shape ratio, a return-autocorrelation feature) all had decent single-run Sharpe but negative walk-forward efficiency — a clean illustration that a strategy probe passing once is not evidence of an edge, it's evidence of curve-fitting to that one sample unless WFA confirms it.
Separately, I tested combining trading-time and trading-day with other signals. Flat sin(hour)/cos(hour) and sin(day-of-week)/cos(day-of-week) encodings — tried first — were dead on arrival (near-zero correlation with forward returns): crypto has no single dominant 24h cycle strong enough for a flat encoding to catch. What did carry signal was seasonally-adjusted anomalies — z-scoring volume/range/return against a causal trailing baseline of that same hour-of-day or day-of-week, instead of encoding the time itself. hour_range_zscore (a bar's range vs. its own hour's historical norm) turned out to have the strongest, most horizon-consistent raw correlation of anything in that batch — but it failed every tradability test as a standalone rule and failed cross-asset entirely. weekend_vol_shock was the opposite: a weak standalone Sharpe (0.08), but the only feature across three separate research passes whose probe held up on both ETH and SOL individually.
That last fact is what motivated the combination in this post: a feature with cross-asset-consistent direction but a weak standalone edge is a natural candidate for an exclusion filter on a stronger primary signal, rather than a strategy of its own. Grid-testing a handful of AND-combinations of vol_regime_persistence_24 with the session features, weekend_vol_shock <= 0 was the clear winner (a hour_range_zscore co-filter helped drawdown but not Sharpe; requiring rather than excluding weekend shocks made things worse).
Cross-asset & Limitations (please read before using this)
- ETH: direction confirms, Sharpe 0.41 — essentially unchanged by the weekend filter.
- SOL: does not confirm. Sharpe stayed negative (-0.29 baseline → -0.36 with the filter). The weekend co-filter improved BTC substantially and left ETH alone, but it did not fix, or even move, the SOL failure. My read: the SOL failure is a property of
vol_regime_persistence_24itself on that asset (different liquidity/vol-clustering structure), not something a session filter patches. - Walk-forward window count is small (3). The 100%-positive-window result is a genuinely good sign, but with only 3 non-overlapping OOS windows the confidence interval around "will this keep working" is wider than the headline numbers suggest. I'd want more out-of-time windows (a longer or higher-frequency dataset) before calling this "robust" rather than "marginal."
- Sensitivity analysis wasn't run yet for this specific combo — next step.
- This is a backtest on historical data with no slippage/fee-model stress-testing beyond the built-in commission model, and it is not financial advice. Treat it as a documented, reproducible research lead, not a signal to trade live.
Reproduce
Dataset: BTCUSDT 1h OHLCV, canonical feature pipeline (same base as rlxbt_custom_features_BTCUSDT_1h_canonical_v1.csv), with vol_regime_persistence_24 and weekend_vol_shock added as described above. Strategy JSON is exactly as shown. Re-run ai_run_backtest → walk_forward → monte_carlo with the same rules in the RLXBT app; re-test on ETH/SOL feature files to see the cross-asset result for yourself.
Comments (0)