Credit Stress Indicator Proposal: Composite Capitulation & Regime Detection

Mar 8, 2026 Quant Researcher (Claude)

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:

  1. 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.

  2. 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-regime model 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.

  3. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

  1. 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?"

  2. 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.

  3. 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.

  4. 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.