Post-Mortem: Our Bull Regime Was Mathematically Unreachable for 156 Days
We build Regime to classify the crypto market as bull, bear, or chop from a weighted basket of signals. On 2026-08-03, while preparing a data sample for a prospective customer, we found that our classifier had not emitted a single bull label in 156 days — 68,098 snapshots — despite BTC running from a March low of $65,191 to a May high of $82,782, a +27% rally.
The label read bear the entire way up.
This is the post-mortem. We're publishing it because the alternative — quietly shipping a fix and saying nothing — is exactly the failure mode that let this happen in the first place.
The number
Across 2026-03-02 → 2026-08-03:
- 63,913 bear labels (93.9%)
- 4,186 chop labels (6.1%)
- 0 bull labels
chop itself collapsed to 8 observations in June and zero in July and August. So it wasn't just "bull is rare" — the model had degenerated to a single output.
Root cause: this was never a bad market call
The first question anyone asks about a "wrong" classification is whether the model was just miscalibrated for the conditions — an aggressive bear read during a choppy market, the kind of thing you'd tune and move on from. That's not what this was. We could prove, from the weights alone, that bull was arithmetically impossible to reach.
The classifier is a weighted vote: netScore = bullScore − bearScore, normalized across whichever signals are currently live, with bull requiring netScore > +0.20. Signal weights: BTC SMA 50/200 at 0.20, everything else at 0.10. Over the trailing 30 days before the fix:
| 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 |
Best possible outcome: 0.20 (max bullish) − 0.40 (guaranteed bearish) = −0.20. The ceiling sat exactly on the bear/chop boundary. That's also why chop vanished — there was no headroom above bear to even reach neutral territory, let alone bull.
Why four signals were frozen
Four of our ten signals had been stuck reporting neutral on every observation for months, silently dropping out of the vote while still occupying denominator weight:
-
BTC Funding Rate — a ~100x unit bug. Thresholds were written in percent (
> 0.01bearish,< -0.005bullish) but compared against a decimal rate. The observed 30-day range was[-0.00003, 0.0001]— three orders of magnitude away from either threshold. Unreachable by construction. -
Aggregated Funding Rate — a ~10-20x scale bug. Same shape of defect: bounds set at
-0.0001 / 0.0003against an observed range of[-0.000025, 0.000013]. -
Volatility Regime — thresholds outside the real distribution. Fired on
atr20/atr90 > 1.5or< 0.7; the observed range was[0.70, 1.00], mean 0.85. The bearish threshold sat right at the theoretical minimum of the ratio. - Stablecoin Supply — wrong comparison window. The ±0.5% move threshold was designed for a multi-day signal but was being computed against the previous poll, roughly two minutes earlier. The delta was always effectively zero.
A tenth signal, Liquidation Imbalance, had already dropped out entirely — its upstream data dependency lapsed on 2026-04-09, and the classifier silently degraded from 10 signals to 9 with no error and no alarm. HTTP 200 the entire time.
Why it stayed invisible for 156 days
This is the part that actually matters for anyone running a similar system: our test suite was green the whole time.
The unit tests asserted that bull was reachable — using inputs that could not occur in the real market. fundingRate: -0.01 (roughly 300x beyond the observed minimum). dxy: 95 (the observed floor was 99.49). btcDominance: 40 (the observed floor was 55.43). The tests passed against a market that doesn't exist. Nothing in CI, nothing in monitoring, nothing in the deploy pipeline would have caught this, because "does the code work" and "does the code work against reality" turned out to be two different questions and we'd only been answering the first one.
What we fixed (v2.0.0, 2026-08-03)
-
BTC Funding Rate: thresholds rescaled to the actual decimal range — bearish above
0.0001, bullish below0. -
Aggregated Funding Rate: bearish above
0.00001, bullish below0— cross-exchange aggregates run roughly an order of magnitude tighter than single-venue. -
Volatility Regime: recalibrated inside the observed distribution — bullish (compression) below
0.80, bearish (expansion) above0.95. -
Stablecoin Supply: the ±0.5% threshold itself was left unchanged; we fixed the window. The service now keeps a rolling buffer and compares against the reading ~7 days prior, rather than ~2 minutes prior. Until that buffer has a full week of history, the signal is omitted entirely rather than emitting a fabricated
neutral— a temporarily-smaller-but-honest signal set beats a fixed-size dishonest one. - Every classification now carries a
modelVersionfield.
What we explicitly did not do
No backfill. No restatement. Every v1 label stands exactly as it was recorded, defect included. We publish a point-in-time series and guarantee we never rewrite a published row — that guarantee is worthless if we quietly "fix" history the moment the fix is embarrassing. The correct record of a broken sensor is the broken reading plus a dated, public disclosure of what was wrong with it. Not a smoothed-over history.
We also left BTC SMA 50/200, Fear & Greed, and BTC Dominance bearish. They were reporting real conditions during that window — a genuine death cross, Fear & Greed in the 20-33 range, dominance at 55-60. Only demonstrably broken signals were touched. (BTC Dominance's >55 bearish threshold sits right at the floor of its observed range and is a legitimate open question for a future version — we deliberately didn't touch it here, to keep this release scoped to fixing defects rather than making a new opinion.)
Guardrails so this can't happen silently again
- A regression suite pinned to observed production ranges, not synthetic ones. Two assertions in particular would have caught this in April instead of August:
bullmust be reachable using only values the market has actually printed, and no signal may readneutralat both ends of its real range simultaneously. - A constant-output guard that fails CI when any individual signal returns the same interpretation for N consecutive days — the exact pattern that let four signals sit frozen for months undetected.
- We no longer publish a hardcoded signal count anywhere in our product surfaces or documentation. The live roster varies legitimately (a signal is correctly omitted during warm-up or when its upstream dependency is down) — describing it with a fixed number is a claim that goes stale the moment that happens, and it's checkable by anyone reading the API response. We'd rather point at the live
signals[]array than make a claim we can't keep current.
Why we're telling you this
We sell a point-in-time label series with a "never restated" guarantee. That promise is only meaningful if the thing producing the labels is itself auditable — and this incident is the uncomfortable proof that "auditable" has to include admitting when the sensor was broken, not just refusing to edit the readings after the fact.
If you're running anything that classifies real-world state from a weighted vote of signals — trading systems, alerting, anomaly detection — the actual lesson here isn't "check your thresholds." It's: test against the range of values your system has actually observed, not the range you assume it might see. A green test suite told us nothing for five months because it was testing an imaginary market.
Full model changelog, including every threshold that changed: docs/REGIME-MODEL-CHANGELOG.md.
Live model version and current signal breakdown: GET /api/v1/market/regime at getregime.com.
Try Regime Intelligence
Regime is a real-time crypto market regime detection API. One endpoint tells you if the market is bull, bear, or chop — so your bot only trades when conditions match your strategy.
Top comments (0)