← Regime

Regime Classifier — Model Changelog

> The point-in-time guarantee. Regime records its bull/bear/chop label forward

> in time and never restates a published row. That guarantee is only meaningful

> if the model producing the labels is equally auditable — a silent change to a

> threshold shifts the meaning of the series just as surely as rewriting a row.

>

> This file is the complete version history and is published alongside the data

> sample. **No stored label has been deleted or rewritten, including by the

> corrections below.* One block of labels was* written after the fact; see the

> 2026-10-15 correction.

>

> ⚠️ Correction, 2026-10-15 — part of the unattributed epoch was backfilled.

> This header previously read *"No historical label has ever been altered or

> backfilled."* That was false. After an outage, scripts/backfill-gap.mjs

> reconstructed 5,264 regime labels (and the matching market snapshots) for

> 2026-04-02 → 2026-04-21 from historical market data and inserted them into the

> table later, out of time order. They carry no model_version, sit in the

> unattributed epoch below, are excluded from the record (/tape, /api/v1/stats

> and the sample file), and are kept unedited. The version-stamped record from

> 2026-08-26 contains no backfilled row. A "recorded forward" claim applies to that

> record only.

>

> ⚠️ Correction, 2026-08-26 — the version stamp is not retroactive. This

> paragraph previously read "Every label therefore carries a modelVersion."

> That was false against the data it describes, and it is the sentence a fund's

> diligence reads first. The truth:

>

> - REGIME_MODEL_VERSION existed in code from the start but was written to

> exactly one surface — the /sample CSV header. The stored series had **no

> version column at all** until 2026-08-26.

> - Worse, until the same day the series had two concurrent writers: the

> public dashboard and a trading-lane process that had been running since

> 2026-05-06 and therefore executed pre-v2.0.0 code. They interleaved, and

> disagreed.

> - Consequently **80,748 of 80,773 rows carry no model_version, and for those

> rows the producing model genuinely cannot be reconstructed.** They are shown

> as not recorded on /as-of/ rather than inferred.

>

> Rows written from 2026-08-26 onward carry the stamp. Treat the series as having

> two epochs: unattributed (2026-03-02 → 2026-08-26) and version-stamped

> (2026-08-26 →). Do not blend them on one chart or in one statistic.


v2.2.0 — 2026-08-25 — percentile calibration; SMA weight 0.20 → 0.10

What changed. Seven signals now vote on where their value sits in their own

trailing distribution (480 samples, roughly 10 days) instead of against an

absolute constant: BTC SMA 50/200, BTC Dominance, DXY, Volatility Regime,

Volume Profile, BTC Funding Rate, Agg Funding Rate. Above the 70th

percentile votes one way, below the 30th the other, between is neutral. A

negative funding rate still votes bullish outright — shorts paying longs is a

real economic event, not merely a low reading.

Fear & Greed, Stablecoin Supply and Liquidation Imbalance are deliberately

NOT calibrated. Their scales mean the same thing every day (F&G is a standardised

0-100 index with published 65/35 bands), so percentiling them would destroy real

information rather than recover it.

BTC SMA 50/200 drops from weight 0.20 to 0.10, equal to every other signal.

Why. Hand-tuned absolute thresholds had failed three times, each time fixed by

re-tuning the constant against a recent observed range, and each time re-pinning

once the market moved. On 2026-08-25 the live state was:

| signal | reading | weight |

|---|---|---|

| BTC SMA 50/200 | bearish in all 4,039 samples | 0.20 |

| BTC Dominance | bearish in all 4,021 samples — the signal v2.1.0 had just "fixed" | 0.10 |

| DXY | bullish in all 4,035 samples | 0.10 |

| BTC Funding Rate | neutral in all 4,037 samples | 0.10 |

Four of eight live signals carried no information; the three directional ones net

to a permanent −0.20 offset in every score. Across the whole recorded series that

produced bear 89.6% / chop 10.4% / bull 1 sample in 80,709 — through months in

which BTC ran +27% (Mar–May) and +30% intramonth (Aug). The SMA signal alone was

the clearest case: BTC traded at $78.8k while SMA50 sat at $66.1k and SMA200 at

$69.2k — price above both, an unambiguously constructive configuration — and the

signal still voted bearish, because a 50/200 crossover lags months behind a

recovery. At double weight, that is a constant, not a signal.

Percentile calibration removes the failure mode by construction rather than by

better tuning: roughly 30% of samples always sit above the 70th percentile, so a

calibrated signal cannot be one-sided across a window whatever the scale does.

Impact on the label series. Replayed over the recorded history (2,618

snapshots at 30-minute sampling), using each snapshot's real signal values:

| | bull | chop | bear |

|---|---|---|---|

| v2.1.0 as shipped | 0.0% | 11.4% | 88.6% |

| v2.2.0 | 14% | 34% | 52% |

Parameters were selected by sweeping 72 combinations, then tested **out of

sample**: tuned on the first 70% (to 2026-06-15), evaluated on the last 30% the

tuner never saw. On that held-out period the classifier separates forward returns

in the correct direction at both horizons — bull +0.234% vs bear −0.067% at

4h, bull +0.592% vs bear −0.710% at 24h.

What we are NOT claiming. The three-way ordering did not survive out of

sample: chop was the worst-performing bucket on held-out data, so it is not a

"neutral" state and must not be described as one. Sample sizes are small

(n=150 bull observations out of sample), the period is a single 5.8 months, and

only one of 72 swept configurations was monotonic in-sample — which is as much an

overfitting signature as a result. **The structural claim is that the classifier

now has three usable states; it is not a claim of validated predictive edge, and

no accuracy or return figure from this work should be published as a performance

claim.**

No historical label was altered. Every row published under v2.1.0 keeps its

v2.1.0 stamp and its original value. This changes classification going forward

only, which is the entire point of stamping modelVersion on every label.


v2.1.0 — 2026-08-20 — BTC Dominance: absolute level → 7-day trend

What changed. The BTC Dominance signal now votes on the direction of

dominance over a 7-day lookback (> +0.3pp bearish / < −0.3pp bullish / else

neutral) instead of its absolute level (> 55 bearish / < 45 bullish). Its

value field is now the 7-day delta in percentage points, not the level. Weight

is unchanged at 0.10.

Why. The signal was documented as a trend ("rising = risk-off, falling =

risk-on") but implemented as a level test. BTC dominance has sat in the 55–59

band for the entire life of this dataset (30d observed range 55.71–58.83), so the

level rule returned bearish in 7,961 of 7,961 samples — a permanent −0.10

bear vote that was never a market observation.

Impact on the label series. Combined with a legitimate 0.20-weight death

cross (BTC SMA 50/200 bearish in 7,967 of 7,967 samples), the permanent

dominance vote held the bear floor at 0.30/0.90 and made bull

(netScore > +0.20) unreachable: the maximum netScore actually observed

across 7,967 v2.0.0 snapshots was exactly 0.0000. This is the same defect

class the v2.0.0 post-mortem describes — v2.0.0 raised the ceiling from −0.20 to

0.0, which unpinned chop, not bull.

Threshold calibration. ±0.3pp was chosen against the real 7-day-delta

distribution (1,708 hourly samples, range −0.72pp to +2.48pp), producing a

genuine three-state split of ~20% bullish / 25% bearish / 55% neutral. The

±0.5pp alternative was tested and rejected as neutral-heavy (8/23/70). Thresholds

are calibrated against observed distributions, never chosen to produce a desired

label.

What did NOT change, deliberately. The bull/bear thresholds remain symmetric

at ±0.20 and BTC SMA 50/200 keeps its 0.20 weight. Replaying 13 days of history

with the dominance fix applied raises the maximum netScore from 0.0000 to

0.2000 — still at, not above, the bull threshold, so bull remains unprinted

while BTC is in a sustained death cross. **That is a disclosed design property,

not a defect**: a model that weights a 50/200 death cross most heavily should be

reluctant to print bull during one. It has NOT been "fixed" by lowering the

threshold or re-weighting SMA, because tuning a classifier until the sales

artifact looks discriminating would convert a fixable bug into a

misrepresentation — the precise failure the point-in-time warranty above is sold

against.

No historical row was altered. v2.0.0 labels remain exactly as published;

v2.1.0 applies forward only, and the version stamp marks the boundary.

Controls added with this change.

is never observed in the window. It already detected and printed this exact

condition ("classes never observed in window: bull") — but excluded it from the

pass/fail verdict, so it exited 0 for 13 days. A warning nobody must act on is

not a control.

ratio. It previously omitted the model's heaviest input, which let the

"bull must be REACHABLE" test pass using a golden cross — an input production

has never produced.

market_snapshots, so a restart no longer silently drops the signal from the

roster for 7 days.


Canonical signal roster (SSoT)

Never publish a signal COUNT. The roster is version-scoped and the live set

varies legitimately — a signal is omitted when its data is unavailable or its

warm-up window has not elapsed, which is correct behaviour, not a defect. Any

hardcoded "N-signal classifier" claim is stale the moment that happens. It is

also a checkable claim, and we sell integrity to people who check.

The authoritative live roster is the signals[] array on

GET /api/v1/market/regime. Describe the model qualitatively and point at that.

pnpm check:signal-claims enforces this.

| Signal | Weight | v1 | v2 |

|---|---|---|---|

| BTC SMA 50/200 | 0.20 | ✅ | ✅ |

| BTC Funding Rate | 0.10 | ⚠️ pinned neutral | ✅ recalibrated |

| Fear & Greed | 0.10 | ✅ | ✅ |

| BTC Dominance | 0.10 | ⚠️ level, pinned bearish | ✅ v2.1.0 — 7d trend (omitted during warm-up) |

| Stablecoin Supply | 0.10 | ⚠️ pinned neutral | ✅ 7d lookback (omitted during warm-up) |

| Volume Profile | 0.10 | ✅ | ✅ |

| Volatility Regime | 0.10 | ⚠️ pinned neutral | ✅ recalibrated |

| Agg Funding Rate | 0.10 | ⚠️ pinned neutral | ✅ recalibrated |

| DXY (Dollar Index) | 0.10 | ✅ | ✅ |

| Liquidation Imbalance | 0.10 | ❌ retired 2026-04-09 | ❌ retired |

Liquidation Imbalance is RETIRED. Its upstream (CoinGlass) lapsed on

2026-04-09 and the signal silently vanished from the input set. Per

FINANCE-RULING-2026-08-04-001 the dependency is not being restored: funding

and open interest are still served without it, the model already self-normalizes

over the signals actually present (no confidence deflation), and re-adding a

voting input after v2.0.0 would force a v2.1.0 and restart the 7-day history

window ahead of a time-sensitive sale. It may return in a future version only if

a customer names it in writing as a purchase condition.


v2.0.0 — 2026-08-03

Type: defect correction (threshold calibration + upstream data window).

Historical labels affected: none. v1 labels remain exactly as recorded.

What was wrong with v1

v1 could not emit a bull label. Not "rarely" — mathematically could not, given

the values the market actually produced.

Across 2026-03-02 → 2026-08-03 (156 days, 68,098 snapshots) the v1 series

contains zero bull labels: 63,913 bear (93.9%) and 4,186 chop (6.1%).

chop itself fell to 8 observations in June and 0 in July and August. During

that same window BTC traded from a March low of $65,191 to a May high of

$82,782 (+27%) while the model printed bear throughout.

Root cause — the arithmetic. Signal weights are BTC SMA 50/200 = 0.20, all

others 0.10. netScore = bullScore − bearScore, normalized over the total weight

of present signals, and bull requires netScore > +0.20. In the trailing 30

days of v1:

| Bucket | Signals | Weight |

|---|---|---|

| Permanently bearish | BTC SMA 50/200, Fear & Greed, BTC Dominance | 0.40 |

| Frozen at neutral (dead weight in the denominator) | BTC Funding Rate, Agg Funding Rate, Stablecoin Supply, Volatility Regime | 0.40 |

| Actually variable | DXY, Volume Profile | 0.20 |

Maximum attainable netScore was 0.20 − 0.40 = −0.20. The ceiling sat exactly

on the bear/chop boundary, which is also why chop disappeared.

Why four signals were frozen:

  • BTC Funding Rate — unit mismatch (~100×). Thresholds (> 0.01 bearish,
  • < -0.005 bullish) were expressed in percent but compared against a

    decimal rate. Observed 30d range: [-0.00003, 0.0001]. Unreachable.

  • Agg Funding Rate — scale mismatch (~10–20×). Bounds -0.0001 / 0.0003
  • against an observed range of [-0.000025, 0.000013].

  • Volatility Regime — bounds outside the distribution. Fired on
  • > 1.5 or < 0.7; observed atr20/atr90 range was [0.70, 1.00], mean 0.85.

  • Stablecoin Supply — wrong comparison window. The ±0.5% threshold was
  • written for a multi-day move, but the caller supplied the previous poll

    (~2 minutes earlier), so the delta was always ≈0%.

    Contributing: the 10th signal, Liquidation Imbalance, disappeared from the

    input set when its upstream data dependency lapsed (2026-04-09). The classifier

    degraded from 10 signals to 9 silently — no error, no alarm, HTTP 200

    throughout.

    Why it stayed invisible: the unit test suite asserted bull was reachable

    using inputs that cannot occur — fundingRate: -0.01 (≈300× beyond the observed

    minimum), dxy: 95 (observed floor 99.49), btcDominance: 40 (observed floor

    55.43). The tests passed against a market that does not exist.

    What changed in v2.0.0

    baseline; above it longs pay a premium), bullish below 0 (shorts paying).

    Cross-exchange aggregates run an order of magnitude tighter than single-venue.

    (expansion) above 0.95 — recalibrated inside the real distribution.

    fixed. The service now keeps a rolling buffer and compares against the reading

    ~7 days prior. Until the buffer spans a week it returns 0 and the signal is

    omitted entirely rather than emitting a fabricated neutral.

    Guardrails added so this cannot recur silently

    regime-detector.test.ts) with two assertions that would have caught this in

    April: bull must be reachable using only values the market has printed, and

    no signal may read neutral at both ends of its real range.

    when any signal returns a single interpretation for N consecutive days.

    What was explicitly NOT done

    included. The correct record of a broken sensor is the broken reading plus a

    dated disclosure — not a quietly improved history.

    reporting real conditions (a genuine death cross; F&G 20–33; dominance 55–60).

    Only demonstrably broken signals were touched. BTC Dominance's > 55 threshold

    sits at the floor of its observed range and is a known open question for a

    future version — deliberately not changed here, to keep this release scoped to

    defects rather than opinions.


    v1.0.0 — 2026-03-02 → 2026-08-03

    Initial 10-signal weighted classifier. Superseded. Contains the defect

    described above: bull unreachable; chop unreachable from June onward;

    9 signals from 2026-04-09 after Liquidation Imbalance dropped out.

    Retained verbatim as the point-in-time historical record.