KB / framework
Signal Alignment (12 categories vs price)
Last verified
Compares SPY’s recent price direction against the equity implications of twelve signal categories. Eleven are surfaced by default; the twelfth (news_sentiment) is flag-gated behind ENABLE_AI_SENTIMENT_ALIGNMENT until 30 days of paired data confirm per-horizon hit-rate ≥ 55% — when off, the category is absent from the response and the surface stays byte-identical to the pre-R13 11-category shape. Default baseline is 3 trading days (changed from 5d in 2026-05 to catch setups earlier); switch to 5d via the alignment card or GET /signals/alignment?baseline=5d.
Alignment is the first card on the dashboard. If most signals agree with price → conviction high. If they disagree → something’s about to resolve.
The 12 categories
Insertion order in app/signals/implications.py:IMPLICATION_FUNCS is canonical. Categories split across three transmission classes:
Leading (7) — move first
dark_pool— DIX primary (0.7) + intraday options-flow (0.3) — flow combines 0DTE PCR + SKEW/VIX divergence so the category doesn’t carry yesterday’s DIX read alone.gamma— GEXcredit— HY OAS (0.6) + NFCI (0.4) fusedbreadth— % above 50d SMAenergy— energy_regime categorical (0.7) + WTI 1y percentile magnitude (0.3) — disambiguates a $5 RISING-regime move (median percentile) from a $50 SHOCK move (extreme percentile).growth_expectations— Copper/Gold 20d RoC (0.6) + 10y real yield (0.4); directional-confirmation rule resolves disagreement.news_sentiment— single-leg AI briefnews_sentiment_score(0–10). Score ≥ 6.5 → BULLISH, ≤ 3.5 → BEARISH, else NEUTRAL. Flag-gated behindENABLE_AI_SENTIMENT_ALIGNMENT(default off); production flip waits on/api/v1/ai-brief/accuracy?days=30showing per-horizon hit rate ≥ 55%.
Coincident (4) — move with price
correlations— 4-pair basket: SPY/VIX (0.50) + SPY/DXY (0.20) + SPY/TNX (0.15) + SPY/Oil (0.15). Each pair’s classification (normal/elevated/stretched/extreme) maps to BULLISH/NEUTRAL/BEARISH/BEARISH and fuses through_fuse_legs.volatility— VIX+term (0.5) + SKEW (0.2) + VVIX/VIX (0.3)inflation— stagflation score from breakeven 5Y/10Y/5Y5Ycarry_risk— USD/JPY 5d RoC (0.5) + MOVE (0.5)
Lagging (1) — confirms after the fact
liquidity— Fed BS trajectory (0.5) + Fed cut probability (0.5)
Each category resolves to BULLISH / NEUTRAL / BEARISH against the recent SPY return. A category that agrees with price direction is aligned; disagrees is divergent.
Severity badge
The overall headline rolls up to one of four states:
Overall severity
STRONG_DIVERGENCE is the signal worth acting on — it’s where the plumbing and the price are telling different stories.
Severity intensity (per dot)
Each dot (11 by default, 12 when news_sentiment is enabled) renders at one of three intensities based on distance from the threshold, plus a regime-critical override:
- soft — near threshold
- medium — moderately removed
- hard — well past threshold or regime-critical (energy SHOCK/CRISIS, VIX backwardation, HY OAS > 5, VIX > 40, breadth < 20 or > 85). Hard dots render with a red outer ring.
A diverging DIX at 0.43 (just off-threshold) looks different from an HY OAS at 5.2% (deep critical), even though both are “diverging.”
Transmission order
The card’s dots split into a leading cluster (6 dots on the left, 7 when news_sentiment is enabled) and a lagging cluster (5 dots on the right). Ordering is regime-conditional — profiles live in ui/src/data/signal-transmission-order.json, one per regime plus an energy_shock override.
Example: RISK-OFF is credit-led (credit → breadth → gamma → dark pool → energy → growth). Energy_shock is oil-led (energy → credit → growth → breadth → dark pool → gamma). Correlations sits in the lagging cluster across all profiles — it confirms or denies cross-asset transmission after the structural signals move.
Enrichments
| Feature | What it tells you |
|---|---|
| Velocity | 5-day change in alignment %. Labeled improving (≥+2pp), deteriorating (≤-2pp), or flat. Often matters more than the absolute level. |
| Regime baseline | 90-day trailing average alignment % per regime. A 70% reading is below average in RISK-ON but unusually high in RISK-OFF — baseline tells you which. |
| Delta badges | Pulsing ▲/▼/◆ on dots that flipped state in the last 24 hours. |
| Drill-down popover | Click any dot for raw value (e.g. “HY OAS 3.21%”), reason, health-score contribution, last state-change timestamp. |
Weighted alignment %
Counts categories by information content, not raw count. Four multiplicative axes:
- Indicator class: leading 1.5×, coincident 1.0×, lagging 0.7×.
- Leg count: single-leg 0.7×, two-leg 1.0×, three-leg 1.2×.
- Regime relevance: energy_regime SHOCK/CRISIS amplifies energy 2×; PANIC amplifies credit + volatility 1.5×.
- Freshness (B1, 2026-05): each leg scores fresh 1.0×, aging 0.5×, stale 0.25×, blended across a category’s legs by their fusion weights. A category contributes proportionally to how live its underlying inputs are.
Surfaces alongside alignment_pct on /signals/alignment as weighted_alignment_pct with per-category weights for transparency. A 50% unweighted read with 3 leading aligned + 3 lagging divergent reads ~66% weighted.
Freshness handling (B1 + B5, 2026-05)
Each category carries a freshness tier — intraday, prior_close, or weekly — based on its slowest input. The tier sets the base threshold that flips a leg from fresh to aging to stale, and the blended status sets the multiplier applied in weighted alignment. news_sentiment (when flag-enabled) carries a 90-minute fresh window override — the AI brief scheduler fires 5 slots/day, so the bare 15-minute intraday threshold under-states its true cadence.
Release-aware windows. A structurally-lagged series is old even when perfectly current — NFCI’s report date is the week-ending Friday, published the following Wednesday (~7-day lag), so between releases it is naturally 7–14 days old. The fresh window therefore adds the publication lag to the base tier window: window = base + lag_days. Credit (NFCI, 7d lag) gets a 14-day window; liquidity (WALCL, 1d lag) gets 8 days. Without this, a current weekly series reads “aging” for most of its cycle.
Leg-weighted blend. A multi-leg category’s freshness is the fusion-weighted blend of its legs, not the single worst leg: each leg is scored against its own release-aware window, then factor = Σ(weight·leg_factor)/Σweight using the same weights the category fuses its implication on. So credit blends HY OAS (intraday, 0.6, the scored value) with NFCI (weekly, 0.4): a genuinely-late NFCI yields aging (0.6·1.0 + 0.4·0.5 = 0.8), not stale — a fresh dominant leg can’t be hidden behind a slow minor one.
| Category | Tier | Driving input | Why |
|---|---|---|---|
dark_pool | prior_close | DIX (squeezemetrics CSV) | EOD CSV; today’s read is yesterday’s close |
gamma | prior_close | GEX (squeezemetrics CSV) | Same EOD CSV as DIX |
credit | weekly | NFCI (Chicago Fed) | NFCI Wednesday weekly + 7d lag drives the fused category |
breadth | intraday | % above 50d SMA | Recomputed every cycle from yfinance closes |
energy | intraday | energy_regime | Recomputed every cycle from intraday WTI |
growth_expectations | prior_close | real_yield_10y (FRED DFII10) | Daily series; today’s read is yesterday’s |
correlations | intraday | rolling SPY/asset Pearson | Recomputed every cycle |
volatility | intraday | VIX + SKEW + VVIX | All intraday |
inflation | prior_close | breakeven_5y/10y + real_yield (FRED daily) | Inputs are prior_close even though computation runs every cycle |
carry_risk | intraday | USD/JPY + MOVE | Both intraday |
liquidity | weekly | WALCL (Fed H.4.1 Thursday) | WALCL weekly drives the category |
Base thresholds: intraday 15 min, prior_close 28 hours, weekly 7 days — each plus the leg’s publication lag. Aging applies up to 2× the window; stale beyond that.
Each categories.<cat> entry on /signals/alignment carries the blended factor plus a per-leg breakdown for transparency:
{
"implication": "...",
"freshness": {
"class": "weekly",
"age_seconds": 1814400,
"factor": 0.8,
"status": "aging",
"legs": [
{"metric": "hy_oas", "class": "intraday", "factor": 1.0, "status": "fresh", "weight": 0.6},
{"metric": "nfci", "class": "weekly", "factor": 0.5, "status": "aging", "weight": 0.4}
]
}
}
The age_seconds surfaces the slowest leg (the honest “as of”). The blended factor is the same multiplier folded into per_category_weight — surfaced explicitly per DOCTRINE P0 (no silent down-weighting). The dashboard fades the dot opacity based on status: fresh 100%, aging 60%, stale 30% + a [stale] micro-label.
Tests: tests/test_alignment_freshness.py pins the tier table, the attenuation arithmetic, and the round-trip contract.
See also
- Regimes — drives the transmission-order profile + per-category regime relevance multipliers.
- Health Score — orthogonal “is the plumbing working” view.
- Per-category implication rules:
app/signals/implications.py. - Validation guard:
app/signals/regime.py:validate_alignment_state()runs on every/signals/alignmentresponse.