Insights
Market-Regime Indicator for Backtesting Strategies
Alphanume Team · August 4, 2026
Use a market-regime indicator in backtests to segment volatility, drawdown, and strategy behavior by state. The S&P 500 Risk Regime is a fixed daily risk-on or risk-off classification, not a directional market forecast.
Alphanume's GET /v1/sp500-risk-regime endpoint returns one row per covered trading date with risk_regime equal to 0 for risk-on or 1 for risk-off. The label is derived from forward-looking implied-volatility metrics, published daily at 10:10 AM New York time, and stored without later revisions.
The natural research question is how a strategy's risk changes between those states. Split returns, realized volatility, drawdown, hit rate, and tail loss by the flag. A second test can apply a predeclared sizing multiplier. Reading 1 as a market-short signal asks the data to make a directional claim it does not make.
The clock controls the join
Strategy decision | Regime label available | Safe rule |
|---|---|---|
Market open | Today's 10:10 AM label is unavailable | Use the prior session's published label |
After 10:10 AM | Same-day label is available | Trade only after a defined delay |
Daily close | Same-day label is already known | Join on the same date |
Overnight position | Depends on entry clock | State the exact observation carried into the holding period |
A date-only merge can look correct while using information published after the strategy entered. Store the strategy decision timestamp and the regime publication rule, then derive regime_known_at_decision. This is especially important for open-to-close and overnight tests.
The risk-regime API guide covers the basic request. The backtest layer adds this observation-time alignment.
Pull the fixed state history
The endpoint supports exact date and standard date ranges, returns rows newest first, and does not paginate. Exact date cannot be combined with range filters. Save the raw response before shifting or joining labels because the shift rule belongs to the strategy, not the source dataset.
import os
import requests
import pandas as pd
response = requests.get(
"https://api.alphanume.com/v1/sp500-risk-regime",
headers={"X-API-Key": os.environ["ALPHANUME_API_KEY"]},
params={"date_gte": "2018-01-01", "date_lte": "2025-12-31"},
timeout=30,
)
response.raise_for_status()
regime = pd.DataFrame(response.json()["data"])
regime["date"] = pd.to_datetime(regime["date"])
regime = regime.sort_values("date")
assert regime["date"].is_unique
assert set(regime["risk_regime"].dropna().unique()).issubset({0, 1})Do not forward-fill unexplained gaps until you know whether the date was a non-trading day, a coverage boundary, or a missing observation. Report the join rate between strategy dates and regime dates before evaluating performance.
Split behavior before changing exposure
Metric | Why report it by state | Common mistake |
|---|---|---|
Observation count | Shows state imbalance | Comparing noisy means without the denominator |
Mean and median return | Checks direction honestly | Assuming risk-off must be negative |
Realized volatility | Tests the intended state distinction | Reporting return alone |
Maximum drawdown | Shows path risk | Computing it from unordered state subsets |
Tail loss | Measures stress exposure | Choosing the percentile after viewing results |
The published Alphanume proof page reports the market-level evidence openly: risk-off days realized 24.7% annualized volatility versus 12.8% in risk-on, while annualized returns were 13.0% versus 12.7%. The forward 10-day volatility split was 19.0% versus 12.3%. Those results support a volatility-state interpretation and reject a directional reading.
Your strategy can interact differently with the same states. A trend strategy, short-volatility book, and market-neutral event strategy have different exposures, so the published market test does not answer the strategy-specific join.
Test one sizing rule
After the descriptive split, test a fixed multiplier such as 1.0 in risk-on and 0.5 in risk-off. Apply it at the first execution time when the label is known, then compare return, volatility, drawdown, turnover, and tail loss with the unsized baseline. Include financing and transaction costs created by changes in exposure.
joined["size_multiplier"] = joined["risk_regime"].map({0: 1.0, 1: 0.5})
joined["sized_return"] = joined["strategy_return"] * joined["size_multiplier"]
summary = joined.groupby("risk_regime").agg(
observations=("strategy_return", "size"),
mean_return=("strategy_return", "mean"),
median_return=("strategy_return", "median"),
return_std=("strategy_return", "std"),
)
assert joined["size_multiplier"].notna().all()This simple overlay is a hypothesis, not the endpoint's recommended allocation. Testing many multipliers and keeping the best one creates the usual tuning problem. Predeclare the rule or validate it on a later period.
Know what the binary state omits
- Risk-off does not say how severe stress is, so a threshold crossing and a crisis share the value 1.
- The response exposes the label rather than its underlying continuous score or distance from the boundary.
- Most risk-off days are ordinary volatile sessions and do not imply an imminent crash.
- A state filter can reduce exposure to profitable volatile periods as well as losing ones.
- Full historical testing requires more than the free trailing 20-session delayed window.
Run the unsized split first
Join one strategy's daily returns to the correctly known regime label and export the raw state history, join-coverage table, and by-state statistics. Verify the open-versus-10:10 timing rule. Then run one predeclared sizing multiplier and publish the directional result even if it is flat.
Explore the state contract on the S&P 500 Risk Regime page and use the Risk Regime guide before extending the analysis across intraday strategies.