Skip to content

KB/framework

Hybrid analogue matcher (Stream C2)

Last verified

The base-rate matcher is the platform’s flagship analytical surface — “when markets looked like this in the past, what happened next.” Stream C2 (May 2026) restructured how the matcher finds analogues: instead of distance-only ranking, the matcher now offers a tag pre-filter that picks up “structurally identical” days before the Euclidean leg ranks them by similarity. Per DOCTRINE §6 Q1 option C, P5 (hybrid > either extreme), and P8 (additive — the default stays soft; hybrid opts in).

The three modes

Every call to /api/v1/signals/base-rates accepts a mode= query parameter. The full catalog:

Mode Filter Rank When to use
hard Bin equality across all dims Match-count Legacy. Kept for comparison.
soft None Weighted Euclidean over all dims Default. The historical workhorse.
euclidean None Weighted Euclidean over all dims Alias for soft. Same behaviour, clearer name.
tag Coarse-tag overlap Recency (no Euclidean) When you want “every structurally similar day” without distance ranking.
hybrid Coarse-tag overlap Weighted Euclidean within the filtered set Directional default. Picks up structurally identical days, then ranks by closeness.

Default stays soft for backwards compatibility. hybrid is opt-in until the calibration loop validates it. Per DOCTRINE P8 — additive, not destructive.

The coarse-tag filter

tag and hybrid both filter history by overlap with today’s five-tag coarse subset (declared as _HYBRID_FILTER_TAGS in app/signals/base_rates.py):

Five tags, not the full 18 the signal_tags table emits per cycle. Per DOCTRINE §6 Q1 recommendation: filtering on all 18 fragments the analogue pool — five coarse components capture the “structurally identical day” signature without over-shrinking the sample.

A history row passes the filter iff for every tag in today’s coarse subset, the row’s recorded scale-level matches today’s exactly. “Exact overlap” semantics — no partial-match leniency at this stage. The C1 signal_tags table is the substrate; the matcher reads (component, scale_level) per trade_date from there.

The fallback rule

The filter is exact-overlap, no leniency. On most days the historical pool with a coarse-tag overlap is comfortably above the statistical-power threshold; on outlier days (e.g. PANIC with deep-negative GEX + crisis energy regime) the overlap can collapse to single digits.

When the filter under-shoots _HYBRID_MIN_ANALOGUES (default 30 analogues), the matcher falls back to Euclidean over the unfiltered history and emits two response fields:

{
  "hybrid_fallback": true,
  "hybrid_fallback_reason": "12 analogues after tag filter; minimum 30"
}

Per DOCTRINE P0 — no silent degradation. A response with hybrid_fallback=true tells the consumer “you asked for hybrid; the filter produced too few analogues to be statistically useful; the matcher fell back to Euclidean over the full history.” Dashboards and agents should surface this state visibly. Per the working agreement, the user must always know when a hybrid call was downgraded.

The 30-analogue floor calibrates against the live history depth: ~2 years of trading days at 250/year leaves ~500 rows; five-tag exact-overlap typically retains 50-150 for an in-distribution day, dropping to single digits on the rare outlier setups where the pool legitimately should be thin. 30 preserves enough power for the bootstrap CI on the mean to be meaningful.

Why not hierarchical fallback?

DOCTRINE §6 Q1 considered three options:

C ships. The Q1 narrative: hierarchical drop hides which tags the matcher relaxed, surfacing a smaller sample without explaining why; B fragments the sample too aggressively to start; C exposes the failure mode (fallback) loudly via the response field and lets the operator decide whether to widen the filter later. If exact-overlap proves too restrictive in practice (the fallback fires more than ~10% of cycles), C2’s follow-on can introduce option A — but not in this PR. Stay tight on the brief.

The matcher version

Every prediction the matcher emits carries MATCHER_VERSION:

2026.06.5-hybrid-15d-recency730-symmetric-rms-covgate-tradingday-topk100

Decoded:

Bump rules live in the # Bump the version when: comment block in app/signals/base_rates.py. The contract is binding: matcher logic changes mean a version bump, behaviour-identical refactors don’t. After every bump, python3 scripts/backfill_base_rate_predictions.py --force re-seeds the calibration substrate so the realised-vs-forecast curve isn’t polluted by stale-matcher predictions.

Reading the response

Calls with mode in {tag, hybrid} add four fields to the standard compute_base_rates() payload:

{
  "matcher_version": "2026.06.5-hybrid-15d-recency730-symmetric-rms-covgate-tradingday-topk100",
  "mode": "hybrid",
  "sample_size": 87,
  "hybrid_filter_tags": {
    "gex": "favorable",
    "energy_regime": "favorable",
    "dix": "leaning",
    "vix_regime": "favorable",
    "zero_dte_pcr": "favorable"
  },
  "hybrid_filtered_count": 87,
  "hybrid_fallback": false,
  "hybrid_fallback_reason": null,
  "forward_1d": { /* ... */ },
  /* full standard payload */
}

Every Euclidean-bearing mode (soft / euclidean / hybrid) additionally carries excluded_dims — the list of distance dimensions dropped by the coverage gate, each as {field, coverage}. It is [] when every dimension cleared the 55% coverage floor. On the live population it typically lists the three live-only positioning dimensions, so a reader can see exactly which dimensions did and didn’t condition the match.

How to think about the trade-off

soft ranks every historical day by similarity and picks the top 100. hybrid first throws away every day whose coarse-tag signature differs from today, then ranks the survivors. The two answer different questions:

Per DOCTRINE P5, hybrid > either extreme: pure tag matching is too coarse (no distance ranking); pure Euclidean is too leaky (a structurally different day can win on numeric similarity alone). The five-tag coarse filter encodes the “same kind of day” question; Euclidean then encodes “how close within that kind.”

See also