Skip to content

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

Coincident (4) — move with price

Lagging (1) — confirms after the fact

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_ALIGNMENT 5+ aligned Conviction-confirming setup. Most categories agree with price.
MIXED default Partial agreement. The default fallback when nothing else fires.
MODERATE_DIVERGENCE 3+ divergent Early-warning state. Price and the plumbing starting to disagree.
STRONG_DIVERGENCE 5+ divergent Acute disagreement. Worth acting on — one side is about to give.

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:

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

FeatureWhat it tells you
Velocity5-day change in alignment %. Labeled improving (≥+2pp), deteriorating (≤-2pp), or flat. Often matters more than the absolute level.
Regime baseline90-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 badgesPulsing ▲/▼/◆ on dots that flipped state in the last 24 hours.
Drill-down popoverClick 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:

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 tierintraday, 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.

CategoryTierDriving inputWhy
dark_poolprior_closeDIX (squeezemetrics CSV)EOD CSV; today’s read is yesterday’s close
gammaprior_closeGEX (squeezemetrics CSV)Same EOD CSV as DIX
creditweeklyNFCI (Chicago Fed)NFCI Wednesday weekly + 7d lag drives the fused category
breadthintraday% above 50d SMARecomputed every cycle from yfinance closes
energyintradayenergy_regimeRecomputed every cycle from intraday WTI
growth_expectationsprior_closereal_yield_10y (FRED DFII10)Daily series; today’s read is yesterday’s
correlationsintradayrolling SPY/asset PearsonRecomputed every cycle
volatilityintradayVIX + SKEW + VVIXAll intraday
inflationprior_closebreakeven_5y/10y + real_yield (FRED daily)Inputs are prior_close even though computation runs every cycle
carry_riskintradayUSD/JPY + MOVEBoth intraday
liquidityweeklyWALCL (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