Generated with python run.py all --post 365, plus python run.py speccurve,
which all leaves out because its 200 permutations take about ten minutes.
To reproduce: python run.py ingest --what all, then python run.py all.
Sample: 5494 days, 2011-08-18 → 2026-09-01. Stitched series — Bitstamp for 2191 days up to 2017-08-16, Binance for 3303 days afterwards. Median divergence on the overlap 0.06%, maximum 9.0% (2017-12-23, a genuine divergence between exchanges at the peak of the mania). No missing days, no duplicates.
CAR computed across events, confidence interval from the t distribution with n−1 degrees of freedom.
| horizon | CAR | 95% CI | p |
|---|---|---|---|
| 30 days | −0.8% | [−25.7%, +24.0%] | 0.920 |
| 90 days | +12.1% | [−67.7%, +91.9%] | 0.663 |
| 180 days | +47.7% | [−117.7%, +213.2%] | 0.426 |
| 365 days | +125.2% | [−190.5%, +441.0%] | 0.296 |
n = 4. The effect may be enormous or negative — with four observations there is no way to decide.
Difference paired across events, 365-day horizon, calendar windows.
| CAR BTC | CAR control | difference | 95% CI | p | |
|---|---|---|---|---|---|
| vs NASDAQ | +125.2% | +17.0% | +108.2% | [−169.4%, +385.8%] | 0.303 |
| vs S&P 500 | +125.2% | +13.7% | +111.5% | [−167.7%, +390.7%] | 0.293 |
| vs gold | +125.2% | −17.3% | +142.5% | [−200.5%, +485.4%] | 0.278 |
No difference is distinguishable from zero. The placebo works in the other direction: run on the NASDAQ, the method finds no halving effect there (+17.0%, p = 0.307), so the absence of a result for BTC is not the method being powerless.
| category | n | CAR(365d) | raw p |
|---|---|---|---|
| regulation | 3 | −194.4% | 0.081 |
| market_structure | 3 | −301.3% | 0.197 |
| macro | 4 | +106.1% | 0.287 |
| halving | 4 | +125.2% | 0.296 |
| credit_event | 7 | −131.4% | 0.261 |
These numbers must not be read as results — they are the input to the correction below.
26 hypotheses (halving windows × event categories × macro phases):
cycle_extreme category was removed
(a cycle top is identified from the price itself, so an event study around
one recovers its own selection rule) and protocol_upgrade replaced it,event_credit_event_7d — the week after a credit event — passes the
replication rule: same sign in both windows, significant in the test window
(p = 0.031), effect larger rather than smaller. It is still not evidence of
anything, for two reasons that the table now shows explicitly:
significant_in_train column
exists so this cannot hide.The replication rule was deliberately left as it is rather than tightened to require training significance. Changing a criterion after seeing which hypothesis it would exclude is the same error this project exists to catch, merely pointed in the flattering direction. The fact is surfaced as a column instead, and the reader can weigh it.
For each hypothesis we check how often the sign of the effect survives the move from the training window to the test window. Under the null that is a coin flip.
| hypothesis | folds | agreeing | agreement | sign p | q (BH) | mean OOS effect | effect p | effect q |
|---|---|---|---|---|---|---|---|---|
| phase_expanding_rising | 5 | 5 | 100% | 0.063 | 1.000 | −0.0028 | 0.017 | 0.300 |
| phase_contracting_rising | 11 | 4 | 36% | 0.549 | 1.000 | +0.0005 | 0.638 | 0.932 |
| phase_contracting_falling | 10 | 6 | 60% | 0.754 | 1.000 | +0.0006 | 0.651 | 0.932 |
| halving_after_365d | 7 | 3 | 43% | 1.000 | 1.000 | +0.0097 | 0.344 | 0.932 |
| halving_after_180d | 4 | 2 | 50% | 1.000 | 1.000 | −0.0008 | 0.619 | 0.932 |
Significant after correction: 0 — on neither the sign test nor the out-of-sample effect. Both statistics go through the correction; correcting only one and quoting the other raw would mean picking the convenient one after seeing the results.
A power limit worth remembering: phase_expanding_rising has 5 out of 5
matching signs, the best possible outcome — and still p = 0.0625. At five
windows significance cannot be reached even with perfect stability.
Agreement on 42.3% of 5130 compared days — below chance level (50% for a binary axis), because in this sample the two are opposed.
| proxy: contracting | proxy: expanding | |
|---|---|---|
| M2: contracting | 1588 | 2193 |
| M2: expanding | 767 | 582 |
The phase “liquidity rising, rates rising” returns −20.6% a year computed from M2 and +105.9% computed from the proxy. Same label, opposite conclusion.
| strategy | return | CAGR | Sharpe | maxDD | exposure |
|---|---|---|---|---|---|
| buy and hold | +715,635% | 80.3% | 1.14 | −84.9% | 100% |
| trend 50/200 | +885,999% | 82.9% | 1.27 | −78.3% | 61% |
| halving +365d | +254,351% | 68.4% | 1.42 | −71.0% | 27% |
| halving +180d | +2,555% | 24.3% | 0.87 | −70.3% | 13% |
| halving +90d | +242% | 8.5% | 0.76 | −21.8% | 7% |
| halving +30d | +12% | 0.7% | 0.13 | −20.5% | 2% |
| macro (M2 up, rates down) | +257% | 8.8% | 0.46 | −57.4% | 10% |
Everything above holds four analytical decisions fixed: the estimation window (-250..-31), abnormal rather than raw returns, log rather than simple returns, and the price series stitched across three exchanges. All 26 hypotheses inherit all four. If one is wrong they are wrong together, and no other check here would notice.
So the halving study was re-run under every combination of them - 160 specifications, varying also the horizon and which exchange history to trust.
The curve. Median CAR across specifications +5.2%; 75% of them positive; 2 of 160 significant at 5% uncorrected.
Inference. The count of significant specifications is not a test. These are re-analyses of the same four halvings, so they move together and the count is nowhere near binomial. Inference comes from shifting the four dates as a block to a random place in the price history and rebuilding the entire curve, 200 times. The shift is circular, and because the halvings span 4161 days of a 5496-day index, 81% of the draws wrap - keeping the gaps between events circularly rather than on the calendar.
| observed | under the null | |
|---|---|---|
| median |CAR| exceeded | — | in 70.6% of draws |
| significant specifications | 2 | 15.1 on average, 43 at the 95th percentile |
VERDICT: NOT ROBUST. Random placement of the same four dates reproduces this curve, and in fact produces seven times more significant specifications than the real halvings do. The result does not depend on the specification, because there is no result to depend on it.
This is the part worth keeping. Median CAR, grouped by one decision at a time:
| decision | value | median CAR |
|---|---|---|
| horizon | 30 days | −0.9% |
| 365 days | +104.3% | |
| baseline | abnormal (subtract pre-event window) | +3.1% |
| raw (subtract nothing) | +24.5% | |
| estimation window | −90..−11 (ends near the event) | +0.3% |
| −250..−31 (project default) | +10.1% | |
| price series | Coinbase (3 of 4 halvings) | +0.5% |
| Bitstamp (4 of 4) | +9.5% | |
| return type | log | +5.3% |
| simple | +4.7% |
Three things fall out of this table.
Both are the same configuration:
binance/30d/log/raw/none n=2 CAR +11.9% p=0.033
binance/30d/simple/raw/none n=2 CAR +13.4% p=0.036
Binance’s history begins in August 2017, so it covers two of the four
halvings, and raw means no baseline was subtracted at all. The only
specifications that find anything are the ones using the least data and the
weakest correction. That is what a false positive looks like when you can see
where it came from.
Every result below rests on one question: how often does this method report an effect where there is none? The event registry now answers it with data rather than assertion.
protocol_upgrade holds the five consensus rule changes enforced on Bitcoin
mainnet inside the price window - BIP66, BIP65, CSV, SegWit, Taproot. The
category is chosen to be uninformative in two independent ways:
Nothing happened on those dates, in the only sense that matters to a market. So whatever the analysis reports around them is a measurement of the analysis.
Here is what it reports.
| where | placebo result | rank among all hypotheses |
|---|---|---|
| event study, 365d | CAR −20.4%, p = 0.76 | weakest of six categories |
| hypothesis scan, 30d | p = 0.209 | 5th of 26 |
| walk-forward, 30d | p = 0.032 | strongest raw p-value in the table |
| forecast model | coefficient −0.032 | 3rd largest of all features |
The last two rows are the useful ones.
In walk-forward, the placebo produces the smallest raw p-value of any hypothesis tested - smaller than every halving window, every regulatory event, every macro phase. Run without correction, this pipeline would have announced that Bitcoin reacts to software upgrades whose dates were public months in advance. After Benjamini-Hochberg the same number is q = 0.33 and the finding evaporates, which is the entire argument for correcting, produced here by the data instead of by a simulation.
In the forecast model, the placebo feature receives the third-largest
coefficient of any predictor, ahead of halving_after_365d. A penalised model
fitted on 26 candidate signals will happily assign weight to a column that
cannot carry information. That is what overfitting looks like from the inside,
and it is why the verdict is read from out-of-sample scores and never from the
coefficient table.
Calibration for reading everything else in this document: a raw p-value between 0.03 and 0.25 is routinely produced here by a variable known to be empty. The halving’s 90-day and 180-day windows (p = 0.64 and p = 0.41) are less distinguishable from noise than the placebo is. Nothing in this project should be believed on the strength of an uncorrected p-value.
Every date in data/raw/events.csv was checked against a source before this
snapshot, and two were wrong:
mtgox_halt was 2014-02-25; trading was actually suspended on 2014-02-24,cme_futures was 2017-12-17; CME’s own release announces an 18 December
launch (the book opened 17 Dec at 23:00 UTC, inside the 18 December daily bar
this project uses).One event was added: Mt. Gox halting withdrawals on 2014-02-07, which moved the price more than the shutdown three weeks later and was simply missing.
That is a 10% error rate on the dates in a file whose numbers feed every event
study here — which is the argument for the source column existing at all.
Correcting them changed the credit-event results; the halving results did not
move, because no halving date was wrong.
Probability that the forward 30-day return is positive, scored on 155 non-overlapping rows pooled across the walk-forward folds.
| predictor | Brier | log loss | accuracy | AUC | base rate |
|---|---|---|---|---|---|
| model | 0.2805 | 1.2817 | 54.8% | 0.474 | 59.4% |
| always_up | 0.2471 | 0.6876 | 59.4% | — | 59.4% |
| coin_flip | 0.2500 | 0.6931 | 40.7% | 0.500 | 59.4% |
| momentum | 0.2476 | 0.6893 | 59.4% | 0.465 | 59.4% |
Verdict: NO EDGE. The model loses to every baseline on the Brier score (−12.2% vs coin flip, −13.5% vs always-up, −13.3% vs momentum), and its AUC of 0.474 means its ranking is, if anything, marginally worse than random.
The penalty was selected per fold on an inner split of that fold’s training window, so this is not the result of a badly tuned model: with a fixed weak penalty the Brier score was 0.39, far worse still.
Calibration in the middle buckets is reasonable (gap −0.03 and +0.06 where 85% of the predictions fall), so the model is not nonsense — it simply has no information about the future beyond the base rate.
The most recent prediction at the time of this snapshot was 54.6% against a training base rate of 56.6%, i.e. 2 points below “the asset usually goes up”. Given the verdict above, that number is decoration.
The only strategy with a better Sharpe than buy-and-hold at meaningful exposure
(halving +365d, 1.42 at 27% time in market) fails statistical validation
and is indistinguishable from the NASDAQ. Four cycles are four observations,
and “be in the market for a year after the halving” largely overlaps with “be
in the market during a bull run”.
Four independent checks now say the same thing, and each closes a different escape route:
| check | the objection it answers | result |
|---|---|---|
| control group | “everything rose, so what?” | difference vs NASDAQ p = 0.30 |
| placebo category | “your method finds effects everywhere” | it does: raw p = 0.03 where nothing happened |
| walk-forward | “it worked in the past” | 0 of 26 stable out of sample |
| specification curve | “you picked the wrong window” | 160 windows, none of it survives |
The specification curve also says what the apparent effect actually is. Stop subtracting a pre-event baseline and it grows eightfold; move the baseline closer to the event and it nearly vanishes; extend the horizon and it grows without limit. Those are the signatures of drift, not of a response to an event. The halving is not moving the price in this data - the market’s own trend is, and the halving happens to sit inside it.
That is a result, not a failure. The project was built to tell a pattern from noise, and it did.
features.csv (3.9 MB) and lab.sqlite (11 MB) are deliberately outside
version control — one command regenerates them, and versioning them would bloat
the history on every run.