Alphanume

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.