> 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.
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.
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.
scripts/check-signal-variance.mjs now fails (exit 1) when a regime classis 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.
OBSERVED table now constrains the SMA 50/200ratio. 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.
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.
Type: defect correction (threshold calibration + upstream data window).
Historical labels affected: none. v1 labels remain exactly as recorded.
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:
> 0.01 bearish,< -0.005 bullish) were expressed in percent but compared against a
decimal rate. Observed 30d range: [-0.00003, 0.0001]. Unreachable.
-0.0001 / 0.0003against an observed range of [-0.000025, 0.000013].
> 1.5 or < 0.7; observed atr20/atr90 range was [0.70, 1.00], mean 0.85.
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.
bearish above 0.0001 (the 0.01%/8h exchangebaseline; above it longs pay a premium), bullish below 0 (shorts paying).
bullish below 0, bearish above 0.00001.Cross-exchange aggregates run an order of magnitude tighter than single-venue.
bullish (compression) below 0.80, bearish(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.
modelVersion is now stamped on every classification.OBSERVED inregime-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.
scripts/check-signal-variance.mjs) that failswhen any signal returns a single interpretation for N consecutive days.
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.
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.