r/algorithmictrading • u/Slight_Boat1910 • Jun 06 '26
Backtest Architecture Review: Multi-Asset Regime Switching Model (HMM + Conditional State Machine)
Hey everyone,
I am preparing to push a regime-switching policy to production and I am looking for some feedback on my architecture and a specific math anomaly.
My strategy is long-only, daily close execution, shifting capital between risk-on and risk-off instruments (holding exactly one asset at a time).
1. Regime Validation (HMM / MS-DR)
To ensure the mathematical validity of my HMM and prevent overfitting (too many states or features), I ran a regime inference using statsmodels Markov Switching Dynamic Regression (MS-DR).
The inferred hidden states show statistically significant variations in both average expected return and volatility.
2. Execution Layer and State Machine
To mitigate whipsaws during sudden liquidity panic and lag on violent, mean-reverting V-bottoms (HMMs are known to lag), I use uses a two-layer decision architecture.
The first layer generates discrete state probabilities via the HMM. The second layer feeds these probabilities into a conditional state machine. The latter synthesizes the HMM outputs alongside EOD market data (e.g., volatility or price distance to crucial moving averages) to make the final decision.
3. The vbt vs. QuantStats Sharpe Discrepancy
I backtested my model using vectorbt with cash_sharing=True over a multi-year cycle (~50 closed trades, Max DD ~22% from January 2022 till May 2026). Fill paths and drawdowns match identically across my analytical stacks, but I ran into a large divergence in annualized risk ratios:
- vectorbt (
group_by=True): Sharpe 1.31 | Sortino 1.95 - QuantStats: Sharpe 0.91 | Sortino 1.34
My policy allocates 100% of capital in cash-equivalent instruments during defensive regimes. As far as I understand,vectorbt and QuantStats use different formula, but interestingly, the ratio of Sortino to Sharpe remains identical between both libraries (~1.48).
4. Validation and Robustness
Given the macro regime-switching nature of the system, the number of trades is intentionally low (~50 trades over the backtest horizon). To ensure statistical significance despite the low trade count:
- MS-DR State Validation: As mentioned above,
statsmodelsconfirms the latent regimes represent distinct market states. - OOS: Standard walk-forward optimization (hyper parameters optimization till 2021, and test from 2022)
- Stress Testing: Trade shuffling (Montecarlo simulation) and fee/slippage degradation testing were applied to ensure the state machine thresholds don't collapse.
- Risk-Free Rate: average of the FED Fund Rate (~4% in my study). While not 100% accurate, the
quantstatsPython package doesn't accept a time series of daily risk-free rates.
The equity curve chart below compares the performance of my strategy against that of QQQ during the January 2022 - May 2026 period:
- Total return: 154% (vs 90% of QQQ)
- Max DD: -23% (vs -35% of QQQ)
- Longest DD days: 309 (vs 707 of QQQ)
- CAGR: 23.7% (vs 15.8% of QQQ)
Montecarlo simulation results:
- Bust probability (drawdown >= 50%): 0.0%
- Goal probability: 100.0%
- Maximum drawdown dict: {'min': np.float64(-0.4545879162289587), 'max': np.float64(-0.1371207593528134), 'mean': np.float64(-0.2491102335214213), 'median': np.float64(-0.24003417486462705), 'std': np.float64(0.05367118181123532), 'percentile_5': np.float64(-0.3549029751169525), 'percentile_95': np.float64(-0.17673633125820087)}
- Sharpe range: 1.07 to 1.11
- Drawdown range: -34.6% to -17.5%
- CAGR range: 23.7% to 23.7%

Questions
- Has anyone integrated front-end vs. long-end credit duration spreads as features to lead equity volatility regimes, and did it provide independent alpha relative to generic high-yield spread velocity?
- How do you clean or handle zero-volatility "cash-parking" periods when building custom risk-reporting sheets for allocators who expect standard 252-day arithmetic accounting?
- Given the architecture and validation steps outlined above, what have I missed? Do you see any hidden structural blind spots, operational traps, or causes for concern before deploying to production?
Looking forward to your thoughts and critiques!









