Credit Stress Indicator Proposal: Composite Capitulation & Regime Detection
Credit Stress Indicator Proposal: Composite Capitulation & Regime Detection
Date: 2026-03-08
Researcher: Quant Researcher (Claude)
Status: Proposal (pending PM review)
Source research: analysis/quant-research/credit-stress-strategies-research-2026-03-08.md
Charts: See source research report
Overview
This document proposes four new indicator components based on the credit stress strategies research completed 2026-03-08. It specifies where each component lives architecturally, its exact computation, thresholds, output contract, and integration into the daily dispatch pipeline. The PM should convert this into a formal spec with acceptance criteria.
The four components are:
| # | Indicator | Purpose |
|---|---|---|
| 1 | Composite Capitulation Score (CCS) | Bottom detection (4-component signal) |
| 2 | VIX Regime Indicator | Trending vs. normal regime classification |
| 3 | Momentum Crash Classifier | Rotation vs. liquidation discrimination |
| 4 | Sector Rotation Entry/Exit Signals | Commodity/software pair trade triggers |
Architecture Recommendation
One unified model or separate indicators?
Recommendation: Extend the existing Credit Stress engine with a new "v2 layer" for CCS, and create a new standalone market-regime model for the VIX Regime Indicator and Momentum Crash Classifier. The Sector Rotation signals are advisory outputs derived from the other three and belong in the dispatch briefing, not a separate model.
Rationale:
-
CCS is a natural extension of Credit Stress. Three of its four components (HYG drawdown, XLF weakness, VIX z-score) are credit/financial stress signals. The existing Credit Stress engine already fetches HYG data via the Immune System's FRED pipeline. Adding CCS as a "Layer 4" to credit-stress keeps the domain coherent and avoids a new model for four lines of logic.
-
VIX Regime and Momentum Crash are market structure signals, not credit signals. They measure equity market behavior (VIX trend, QQQ/SPY spread) and belong in a
market-regimemodel that the Immune System and Credit Stress can both consume. The Immune System already tracks VIX for divergence detection but does not classify VIX into trending/normal regimes. -
Sector Rotation signals are compound conditions that combine CCS score, VIX regime, and HYG drawdown. They are best expressed as briefing-level logic in the dispatch pipeline (i.e., the briefing script checks: "CCS >= 3 AND VIX regime = trending? If yes, flag sector rotation entry conditions met"). This avoids creating a model for what is fundamentally a decision rule on top of other model outputs.
Interaction with existing models
data-samples/ohlcv/
|
+-------------+-------------+
| | |
Immune System Credit Stress Market Regime (NEW)
(turbulence, (corp/consumer (VIX regime,
dispersion, spreads, momentum crash
sector stress) funding, classifier)
CCS [NEW])
| | |
+------+------+------+------+
| |
Dispatch Manifest Portfolio Allocator
(briefing.py) (--credit-warning,
--immune-warning,
--regime-warning [NEW])
Key boundaries: - The Immune System does NOT change. It already tracks VIX, HYG, XLF (via financials basket), and breadth. It does not need to know about CCS or VIX regimes. - Credit Stress gains a new Layer 4 (CCS) that reads VIX and breadth data it currently does not fetch. This requires a small data.py enhancement. - Market Regime is a new lightweight model that reads VIX and QQQ/SPY OHLCV only. It produces a regime classification record. - The briefing script combines all three model outputs to surface sector rotation conditions and capitulation alerts.
Component 1: Composite Capitulation Score (CCS)
Where it lives
Extension of models/credit-stress/ as a new Layer 4. Added to engine.py
alongside the existing compute_credit_stress() function.
Inputs
| Input | Source | Frequency | Lookback |
|---|---|---|---|
| VIX close | data-samples/ohlcv/VIX.csv or ^VIX via yfinance |
Daily | 60 days (for z-score) |
| HYG close | data-samples/ohlcv/HYG.csv or yfinance |
Daily | 60 days (for drawdown) |
| SPY close | data-samples/ohlcv/SPY.csv or yfinance |
Daily | 10 days (for breadth proxy) |
| XLF close | data-samples/ohlcv/XLF.csv or yfinance |
Daily | 200 days (for SMA) |
VIX and HYG are already fetched by the Immune System. SPY and XLF are
already in the Immune System's core and financials baskets. For the Credit
Stress model to compute CCS independently (without requiring IS to run
first), it needs to fetch these four tickers itself. The simplest approach:
add a CCS_TICKERS dict to config.py and extend data.py to fetch them
alongside the existing consumer proxy basket.
Computation
def compute_capitulation_score(
vix: pd.Series, # VIX daily close
hyg: pd.Series, # HYG daily close
spy: pd.Series, # SPY daily close
xlf: pd.Series, # XLF daily close
vix_zscore_window: int = 60, # default 60, range 40-90
hyg_dd_window: int = 60, # default 60, range 40-90
hyg_dd_threshold: float = -0.03, # default -3%, range -2% to -5%
breadth_window: int = 10, # default 10, range 7-15
breadth_threshold: float = 0.7, # default 70% down days, range 0.6-0.8
vix_zscore_threshold: float = 2.0,# default 2.0, range 1.5-2.5
xlf_sma_period: int = 200, # default 200, range 150-250
) -> dict:
"""
Returns:
score: int (0-4)
components: dict with individual binary flags
component_values: dict with continuous values for each input
"""
# Component 1: VIX z-score extreme
vix_mean = vix.rolling(vix_zscore_window).mean()
vix_std = vix.rolling(vix_zscore_window).std()
vix_z = (vix - vix_mean) / vix_std
vix_extreme = 1 if vix_z.iloc[-1] > vix_zscore_threshold else 0
# Component 2: HYG drawdown from rolling high
hyg_max = hyg.rolling(hyg_dd_window).max()
hyg_dd = hyg / hyg_max - 1
hyg_stress = 1 if hyg_dd.iloc[-1] < hyg_dd_threshold else 0
# Component 3: Breadth collapse (SPY down-day ratio)
spy_returns = spy.pct_change()
down_days = (spy_returns.rolling(breadth_window).apply(
lambda x: (x < 0).sum() / len(x)
))
breadth_extreme = 1 if down_days.iloc[-1] >= breadth_threshold else 0
# Component 4: XLF below 200 DMA
xlf_sma = xlf.rolling(xlf_sma_period).mean()
xlf_weak = 1 if xlf.iloc[-1] < xlf_sma.iloc[-1] else 0
score = vix_extreme + hyg_stress + breadth_extreme + xlf_weak
return {
"ccs_score": score,
"components": {
"vix_extreme": bool(vix_extreme),
"hyg_stress": bool(hyg_stress),
"breadth_collapse": bool(breadth_extreme),
"xlf_weak": bool(xlf_weak),
},
"component_values": {
"vix_zscore": float(vix_z.iloc[-1]),
"hyg_drawdown_pct": float(hyg_dd.iloc[-1] * 100),
"spy_down_day_ratio": float(down_days.iloc[-1]),
"xlf_vs_200dma_pct": float(
(xlf.iloc[-1] / xlf_sma.iloc[-1] - 1) * 100
),
},
}
Parameters
| Parameter | Default | Range | Calibration source |
|---|---|---|---|
vix_zscore_window |
60 | 40-90 | Standard 60-day window; research used 60d |
vix_zscore_threshold |
2.0 | 1.5-2.5 | At 2.0, VIX extreme fires on ~5% of days |
hyg_dd_window |
60 | 40-90 | 60-day rolling high; matches research |
hyg_dd_threshold |
-0.03 | -0.02 to -0.05 | At -3%, fires during genuine credit events only (research: -5% fires only in 2020) |
breadth_window |
10 | 7-15 | 10 trading days (2 weeks); research used 10 |
breadth_threshold |
0.70 | 0.60-0.80 | 7+ of 10 days declining; research calibrated at 0.7 |
xlf_sma_period |
200 | 150-250 | Standard 200 DMA; research tested 200 |
7 parameters is over the 3-6 knob guideline. The PM may want to fix some
of these at defaults and expose only the most impactful (suggestion: expose
hyg_dd_threshold, vix_zscore_threshold, and breadth_threshold as
the three most impactful knobs; fix the windows at defaults).
Levels and thresholds
| CCS Score | Level Name | Meaning | Research basis |
|---|---|---|---|
| 0 | NORMAL | No stress components firing | 62.6% of days in sample |
| 1 | ELEVATED | One component firing | 21.3% of days |
| 2 | SIGNIFICANT | Two components firing | 11.6% of days |
| 3 | SEVERE | Three components firing | 3.3% of days |
| 4 | CAPITULATION | All four firing | 1.3% of days; 81% positive 1-month forward returns; median +4.9% |
The SEVERE and CAPITULATION levels are the actionable signals. SIGNIFICANT is a watch state. NORMAL and ELEVATED require no action.
Output contract
CCS extends the existing Contract 5 (Credit Stress Record). New fields are additive -- existing consumers ignore unknown fields per the contracts design principle.
Proposed Contract 5 extension:
| Field | Type | Required | Description |
|---|---|---|---|
ccs_score |
int | Yes | 0-4 composite capitulation score |
ccs_level |
string | Yes | NORMAL, ELEVATED, SIGNIFICANT, SEVERE, CAPITULATION |
ccs_vix_extreme |
bool | Yes | VIX z-score > threshold |
ccs_hyg_stress |
bool | Yes | HYG drawdown beyond threshold |
ccs_breadth_collapse |
bool | Yes | SPY decline ratio beyond threshold |
ccs_xlf_weak |
bool | Yes | XLF below 200 DMA |
ccs_vix_zscore |
float | No | Continuous VIX z-score value |
ccs_hyg_drawdown_pct |
float | No | Continuous HYG drawdown value |
Example extended Contract 5 output:
{
"credit_stress_score": 72.4,
"warning_level": "HIGH",
"corp_credit_score": 68.1,
"consumer_credit_score": 81.3,
"funding_stress_score": 55.0,
"divergence_active": true,
"divergence_direction": "consumer_leads",
"ccs_score": 3,
"ccs_level": "SEVERE",
"ccs_vix_extreme": true,
"ccs_hyg_stress": true,
"ccs_breadth_collapse": true,
"ccs_xlf_weak": false,
"ccs_vix_zscore": 2.45,
"ccs_hyg_drawdown_pct": -4.2,
"timestamp": "2026-03-08T16:00:00Z"
}
Report integration
In the daily dispatch briefing, CCS appears as a new row in the MARKET ENVIRONMENT section:
MARKET ENVIRONMENT
----------------------------------------------------------------
Immune System NORMAL (unchanged) Turbulence: 0.42 (35th %ile)
Credit Stress ELEVATED (unchanged) Composite: 58/100
Capitulation SEVERE (3/4) VIX-z HYG-dd BREADTH [xlf ok]
When CCS >= 3, the briefing also adds an ATTENTION ITEM:
ATTENTION ITEMS
----------------------------------------------------------------
[CRITICAL] Capitulation score 3/4 -- SEVERE
Components: VIX extreme, HYG stress, breadth collapse
Missing: XLF still above 200 DMA
Historical 1-mo fwd return at 3+: median +4.7%, 70% positive
Dependencies
- VIX, HYG, SPY, XLF daily OHLCV data (already fetched by dispatch
workflow via
fetch_data.py) - No dependency on Immune System or Market Regime models -- CCS reads raw price data directly
Refresh cadence
Daily, as part of the existing credit-stress model run in the dispatch workflow.
Component 2: VIX Regime Indicator
Where it lives
New model: models/market-regime/. This is a lightweight model that
classifies market regimes from VIX and equity data. It does not duplicate
Immune System turbulence detection -- it answers a different question
("Is VIX trending up structurally?" vs. "Is multi-asset turbulence
elevated?").
Inputs
| Input | Source | Frequency | Lookback |
|---|---|---|---|
| VIX close | data-samples/ohlcv/VIX.csv or ^VIX |
Daily | 50 days (for SMAs) + 20 days (for floor) |
| SPY close | data-samples/ohlcv/SPY.csv |
Daily | For forward return context only |
Computation
def compute_vix_regime(
vix: pd.Series,
sma_short: int = 20, # default 20, range 15-25
sma_long: int = 50, # default 50, range 40-60
slope_lookback: int = 5, # default 5, range 3-7
floor_window: int = 20, # default 20, range 15-25
) -> dict:
"""
Returns:
regime: str -- "trending_up", "elevated", or "normal"
regime_score: int -- 0, 1, or 2
components: dict with individual signals
duration_days: int -- consecutive days in current regime
"""
sma20 = vix.rolling(sma_short).mean()
sma50 = vix.rolling(sma_long).mean()
# SMA slopes (rate of change over slope_lookback days)
sma20_slope = sma20.diff(slope_lookback)
sma50_slope = sma50.diff(slope_lookback)
# Core trending condition
regime_trending = (
(sma20 > sma50)
& (sma20_slope > 0)
& (sma50_slope > 0)
)
# Rising floor condition
vix_floor = vix.rolling(floor_window).min()
floor_slope = vix_floor.diff(slope_lookback)
floor_rising = floor_slope > 0
# Regime score: 0 (normal), 1 (elevated), 2 (trending)
regime_score_series = regime_trending.astype(int) + floor_rising.astype(int)
# Current state
current_score = int(regime_score_series.iloc[-1])
current_trending = bool(regime_trending.iloc[-1])
current_floor_rising = bool(floor_rising.iloc[-1])
if current_score == 2:
regime = "trending_up"
elif current_score == 1:
regime = "elevated"
else:
regime = "normal"
# Duration: count consecutive days at current score or higher
# (simplified: count days regime_trending has been True)
if current_trending:
reversed_trending = regime_trending.iloc[::-1]
duration = 0
for val in reversed_trending:
if val:
duration += 1
else:
break
else:
duration = 0
return {
"vix_regime": regime,
"vix_regime_score": current_score,
"components": {
"sma_crossover": bool(sma20.iloc[-1] > sma50.iloc[-1]),
"sma20_rising": bool(sma20_slope.iloc[-1] > 0),
"sma50_rising": bool(sma50_slope.iloc[-1] > 0),
"floor_rising": current_floor_rising,
},
"duration_days": duration,
"vix_current": float(vix.iloc[-1]),
"vix_sma20": float(sma20.iloc[-1]),
"vix_sma50": float(sma50.iloc[-1]),
}
Parameters
| Parameter | Default | Range | Calibration source |
|---|---|---|---|
sma_short |
20 | 15-25 | Standard 20-day; research used 20 |
sma_long |
50 | 40-60 | Standard 50-day; research used 50 |
slope_lookback |
5 | 3-7 | 5-day rate of change; research used 5 |
floor_window |
20 | 15-25 | 20-day rolling minimum; research used 20 |
4 parameters. Within the 3-6 knob guideline.
Levels and thresholds
| Regime Score | Regime Name | Meaning | Research basis |
|---|---|---|---|
| 0 | NORMAL | VIX not trending, floor not rising | 76.3% of days; SPY ann. return +21.7% |
| 1 | ELEVATED | One of trending/floor conditions met | Transitional state |
| 2 | TRENDING_UP | Both trending and rising floor | 23.7% of days; SPY ann. return -8.0% |
The TRENDING_UP regime is the signal. Research shows a massive performance differential: -8.0% annualized during trending regimes vs. +21.7% normal.
Output contract
New Contract (propose as Contract 11: Market Regime Record):
| Field | Type | Required | Description |
|---|---|---|---|
vix_regime |
string | Yes | normal, elevated, trending_up |
vix_regime_score |
int | Yes | 0, 1, or 2 |
vix_regime_duration_days |
int | Yes | Consecutive days in current regime |
vix_current |
float | Yes | Current VIX close |
vix_sma20 |
float | Yes | 20-day SMA |
vix_sma50 |
float | Yes | 50-day SMA |
vix_floor_rising |
bool | Yes | Is the 20-day VIX floor rising? |
momentum_crash_active |
bool | Yes | See Component 3 |
momentum_crash_type |
string | Yes | none, rotation, liquidation |
timestamp |
string (ISO 8601) | Yes | Data as-of timestamp |
{
"vix_regime": "trending_up",
"vix_regime_score": 2,
"vix_regime_duration_days": 16,
"vix_current": 22.4,
"vix_sma20": 21.8,
"vix_sma50": 19.5,
"vix_floor_rising": true,
"momentum_crash_active": false,
"momentum_crash_type": "none",
"timestamp": "2026-03-08T16:00:00Z"
}
Report integration
In the daily dispatch briefing, VIX Regime appears as a new row:
MARKET ENVIRONMENT
----------------------------------------------------------------
Immune System NORMAL (unchanged) Turbulence: 0.42 (35th %ile)
Credit Stress ELEVATED (unchanged) Composite: 58/100
Capitulation NORMAL (0/4)
VIX Regime TRENDING (16 days) SMA20: 21.8 > SMA50: 19.5
Momentum No crash active
When VIX regime transitions from NORMAL to TRENDING_UP:
ATTENTION ITEMS
----------------------------------------------------------------
[HIGH] VIX regime shifted to TRENDING_UP (day 1)
Historical SPY annualized return in trending regimes: -8.0%
Action: Tighten stops, reduce position sizes
Dependencies
- VIX and SPY daily OHLCV data (already fetched by dispatch workflow)
- No dependency on Immune System or Credit Stress
Refresh cadence
Daily, as a new Tier 1 report in the dispatch workflow.
Component 3: Momentum Crash Classifier
Where it lives
Same models/market-regime/ model as the VIX Regime Indicator. These are
two functions in the same engine.py because they share data (VIX) and
the momentum crash type depends on credit data (HYG) that the market
regime model needs to fetch.
Inputs
| Input | Source | Frequency | Lookback |
|---|---|---|---|
| QQQ close | data-samples/ohlcv/QQQ.csv or yfinance |
Daily | 5 days (for spread) |
| SPY close | data-samples/ohlcv/SPY.csv or yfinance |
Daily | 5 days |
| HYG close | data-samples/ohlcv/HYG.csv or yfinance |
Daily | 20 days (for drawdown) |
Computation
def classify_momentum_crash(
qqq: pd.Series,
spy: pd.Series,
hyg: pd.Series,
spread_window: int = 5, # default 5, range 3-10
spread_threshold: float = -0.025, # default -2.5%, range -2% to -4%
hyg_dd_window: int = 20, # default 20, range 10-30
hyg_dd_threshold: float = -0.03, # default -3%, range -2% to -5%
) -> dict:
"""
Returns:
crash_active: bool
crash_type: str -- "none", "rotation", "liquidation"
spread_5d: float -- QQQ vs SPY 5-day relative return
hyg_drawdown: float -- HYG drawdown from rolling high
"""
qqq_ret = qqq.pct_change(spread_window)
spy_ret = spy.pct_change(spread_window)
spread = qqq_ret - spy_ret
hyg_max = hyg.rolling(hyg_dd_window).max()
hyg_dd = hyg / hyg_max - 1
current_spread = float(spread.iloc[-1])
current_hyg_dd = float(hyg_dd.iloc[-1])
crash_active = current_spread < spread_threshold
if not crash_active:
crash_type = "none"
elif current_hyg_dd < hyg_dd_threshold:
crash_type = "liquidation"
else:
crash_type = "rotation"
return {
"momentum_crash_active": crash_active,
"momentum_crash_type": crash_type,
"qqq_spy_spread_5d": round(current_spread * 100, 2),
"hyg_drawdown_pct": round(current_hyg_dd * 100, 2),
}
Parameters
| Parameter | Default | Range | Calibration source |
|---|---|---|---|
spread_window |
5 | 3-10 | 5 trading days (1 week); research used 5 |
spread_threshold |
-0.025 | -0.02 to -0.04 | -2.5% is between the 1st and 5th percentile of daily spreads |
hyg_dd_window |
20 | 10-30 | 20-day rolling high for HYG drawdown |
hyg_dd_threshold |
-0.03 | -0.02 to -0.05 | -3% discriminates rotation from liquidation |
4 parameters. Within the 3-6 guideline.
Levels and thresholds
| State | Meaning | Research basis |
|---|---|---|
| No crash | QQQ/SPY spread within normal range | Majority of the time |
| Rotation crash | QQQ underperforming SPY but HYG stable | 2020-03, 2021-03, 2021-02: positive 30-day fwd SPY returns |
| Liquidation crash | QQQ underperforming SPY AND HYG falling | 2022-04: -10.7% SPY 30-day fwd return |
The critical distinction: rotation crashes are bullish for SPY (rebalance toward value); liquidation crashes are bearish (reduce equity exposure).
Output contract
Included in the Contract 11 (Market Regime Record) defined in Component 2
above. The momentum_crash_active and momentum_crash_type fields are
part of the same record.
Report integration
When a momentum crash is active:
ATTENTION ITEMS
----------------------------------------------------------------
[CRITICAL] Momentum crash: LIQUIDATION
QQQ-SPY 5d spread: -3.8%
HYG drawdown: -4.1%
Historical forward SPY return: -10.7% (2022 analog)
Action: Reduce equity exposure, add hedges
Or for rotation:
ATTENTION ITEMS
----------------------------------------------------------------
[HIGH] Momentum crash: ROTATION
QQQ-SPY 5d spread: -3.2%
HYG stable (drawdown: -0.8%)
Historical forward SPY return: positive (median +3.6%)
Action: Rebalance toward value/commodity exposure
Dependencies
- QQQ, SPY, HYG daily OHLCV data (already fetched)
- No dependency on other models
Refresh cadence
Daily, same run as VIX Regime Indicator.
Component 4: Sector Rotation Entry/Exit Signals
Where it lives
Not a model. This is briefing-level logic in scripts/briefing.py
that combines outputs from Components 1-3. It reads the Credit Stress
JSON (for CCS) and Market Regime JSON (for VIX regime and momentum type)
and evaluates compound conditions.
Inputs
| Input | Source | Frequency |
|---|---|---|
| CCS output | analysis/dispatch/reports/credit-stress.json |
Daily (from dispatch) |
| Market Regime output | analysis/dispatch/reports/market-regime.json |
Daily (from dispatch) |
| XLF close | data-samples/ohlcv/XLF.csv |
Daily |
| XLF 200 DMA slope | Computed inline from XLF data | Daily |
Computation (briefing logic, not a model)
def evaluate_sector_rotation_conditions(
credit_stress_json: dict,
market_regime_json: dict,
xlf_close: float,
xlf_200_sma: float,
xlf_200_sma_slope_20d: float, # 20-day rate of change of the 200 DMA
hyg_drawdown_pct: float,
) -> dict:
"""Evaluate entry/exit conditions for commodity/software pair trade."""
# Entry conditions (all must be true)
entry_conditions = {
"xlf_below_200dma": xlf_close < xlf_200_sma,
"xlf_200dma_slope_negative": xlf_200_sma_slope_20d < 0,
"hyg_stressed": hyg_drawdown_pct < -3.0,
"vix_trending": market_regime_json["vix_regime"] == "trending_up",
}
entry_signal = all(entry_conditions.values())
# Exit conditions (any triggers exit)
# These would be tracked over time; briefing checks daily
exit_conditions = {
"xlf_reclaims_200dma_5d": False, # requires 5 consecutive days
"vix_regime_normal": market_regime_json["vix_regime"] == "normal",
}
exit_signal = any(exit_conditions.values())
return {
"pair_trade_entry": entry_signal,
"pair_trade_exit": exit_signal,
"entry_conditions": entry_conditions,
"exit_conditions": exit_conditions,
"conditions_met": sum(entry_conditions.values()),
"conditions_total": len(entry_conditions),
}
Report integration
When entry conditions are partially or fully met:
SECTOR ROTATION (Commodity/Software Pair)
----------------------------------------------------------------
Entry conditions: 3/4 met
[x] XLF below 200 DMA
[x] XLF 200 DMA slope negative
[x] VIX trending regime
[ ] HYG drawdown > -3% (current: -1.4%)
Status: WATCH -- waiting for credit confirmation
When all 4 conditions are met:
ATTENTION ITEMS
----------------------------------------------------------------
[HIGH] Sector rotation pair trade: ENTRY CONDITIONS MET
Long: XLE+XLB+XLU (equal weight)
Short: IGV
Stop loss: -15% on the pair
Risk: Fails in liquidity panics (2020 analog: -20.3%)
Dependencies
- Credit Stress model output (CCS fields)
- Market Regime model output (VIX regime)
- XLF and HYG data from OHLCV files
Refresh cadence
Daily, as part of the briefing script.
Summary: Implementation Scope
Existing model changes
| Model | Change | Effort |
|---|---|---|
models/credit-stress/config.py |
Add CCS parameters and ticker list | Small |
models/credit-stress/data.py |
Fetch VIX, HYG, SPY, XLF for CCS | Small |
models/credit-stress/engine.py |
Add compute_capitulation_score() |
Medium |
models/credit-stress/report.py |
Add CCS section to text output | Small |
models/credit-stress/credit_stress.py |
Wire CCS into CLI output | Small |
New model
| File | Purpose | Effort |
|---|---|---|
models/market-regime/config.py |
VIX regime + momentum crash parameters | Small |
models/market-regime/data.py |
Fetch VIX, QQQ, SPY, HYG from OHLCV | Small |
models/market-regime/engine.py |
compute_vix_regime() + classify_momentum_crash() |
Medium |
models/market-regime/report.py |
Text report with regime classification | Small |
models/market-regime/chart.py |
VIX regime chart with SMA overlay | Small |
models/market-regime/market_regime.py |
CLI entry point | Small |
models/market-regime/tests/ |
Unit + integration tests | Medium |
Dispatch pipeline changes
| File | Change | Effort |
|---|---|---|
.github/workflows/daily-reports.yml |
Add market-regime model run step | Small |
scripts/briefing.py |
Add CCS, VIX regime, momentum crash, sector rotation sections | Medium |
models/shared/manifest_reporter.py |
Market regime manifest key_findings schema | Small |
specs/contracts.md |
Register Contract 11 (Market Regime Record), extend Contract 5 | Small |
Contract changes
| Contract | Change |
|---|---|
| Contract 5 (Credit Stress) | Add ccs_* fields (additive, backward compatible) |
| Contract 11 (Market Regime, NEW) | VIX regime + momentum crash classification |
Risk and Limitations
-
Small sample size for CCS score 4. Only 26 observations with score 4/4 in the research sample (2018-2026). The 81% positive forward return rate is encouraging but has a wide confidence interval. The PM should document this limitation in the spec.
-
VIX regime has regime persistence risk. Once VIX enters a trending regime, the signal stays active for the entire duration (16-33 days in the research). This means entry timing is poor -- you know you are in a bad regime but not when it ends.
-
Momentum crash classifier has a binary threshold. The -2.5% QQQ/SPY spread and -3% HYG drawdown thresholds are calibrated from limited historical data (8 worst momentum weeks). A gradient approach might be more robust but adds complexity.
-
Sector rotation pair trade is fragile. It works in credit-driven events (2018, 2022) but loses -20.3% in liquidity panics (2020). The briefing must surface this risk prominently when signaling entry.
-
No CDX HY data. The research identified CDX HY as the more precise credit indicator (per Jordi), but we use HYG as a proxy. HYG may lag actual credit spread movements.
-
Breadth proxy is crude. Using SPY daily up/down count as a breadth measure is a simplification. True market breadth (NYSE advance/decline ratio) would be more precise but requires additional data sources.
Recommendation to PM
-
Prioritize CCS (Component 1) as the highest-value addition. It is the smallest implementation (extends existing credit-stress model), has the strongest research backing (monotonic forward returns by score), and directly answers the user's question: "How close are we to a tradeable bottom?"
-
Market Regime model (Components 2-3) is the second priority. The VIX regime signal has strong statistical backing (-8.0% vs. +21.7% annualized return differential) and fills a gap neither the Immune System nor Credit Stress currently addresses.
-
Sector Rotation signals (Component 4) can wait. They are compound conditions on top of Components 1-3 and are best added after those components are validated in production. The briefing integration is straightforward once the underlying models exist.
-
Consider a "Credit Stress v2" spec that covers Component 1, and a separate "Market Regime v1" spec for Components 2-3. This keeps specs focused and allows independent implementation and validation.