# The ruler was wrong five times

*2026-08-12. What happened to the measuring instrument over two days, why each
error survived inspection, and what every number published before it means now.*

Everything this project has ever measured is a comparison between what a rule
did and what a rule that knew nothing would have done. The second half of that
comparison is the **chance model** in `oppollo/claims/grader.py`. It is the
ruler. Over 2026-08-11 and 12 all four of its parts were found to be wrong, and
all four were replaced with laws measured on 98.6 years of real bars and
validated out of sample. **Then the replacement for one of them was found to be
wrong in the same shape as the thing it replaced**, which is entry 5 and is the
reason this file is worth re-reading rather than filing.

None of this was found by looking for it. It was found because two agents
working on unrelated method families reported the same symptom from opposite
directions on the same day.

## The five

### 1. TIMING — the extreme is not uniform over the window

`_grade_timing` priced a turn claim as `in_band / len(win)`: the window's high or
low equally likely to land on any bar in it. Measured over 620,723 real windows,
it is not. A window's **low sits near its start and its high near its end** far
more often than chance — the drift signature, arrived at from the extremes alone.
And `_horizon` was placing every target about 87% of the way through its window,
which is exactly where the density piles up.

The uniform model **understated a TURN claim's chance of passing by 0.105–0.120**
at the horizon the engine actually emits into. Every `_horizon`-gated turn claim
this repository ever graded collected that as free accuracy.

The arcsine law — the right answer for the driftless walk the *rest* of the
grader already assumed, and therefore the answer that would at least have made
the grader self-consistent — halves the error and still leaves +0.067. Real
series trend and their volatility clusters; both push the extreme toward the
recent end harder than a driftless walk does. So the law was measured, not
derived. Out of sample, fitted 1927–74 and scored 1975–2026 and vice versa,
residual +0.004 / −0.025 against the uniform model's +0.120 / +0.105.

`oppollo/claims/extreme_law.py`.

### 2. TIMING again — a two-day band is five bars, not three

`_horizon` converted a calendar-day tolerance to bars with `round(band * 5/7)`.
For a two-day band that models **three** bars. A ±2-day tolerance either side of
a Wednesday target covers Monday to Friday — **five**.

The average is not the quantity that decides falsifiability. The worst case is.
`_band_window` now counts sessions exactly and `_horizon` sweeps all seven
weekday alignments and both extreme kinds, keeping the hardest. Band-2 turn
horizons went from 9 days to 22.

### 3. DIRECTION — driftless, then Gaussian, then measured

The first version priced a directional call against a **driftless** random walk
on a market that drifts up. That handed every UP claim up to +0.108 of free
accuracy over 90 days and charged every DOWN claim the same.

Adding the drift made the model unbiased **on average** — measured error +0.0015
at 537 days — and left it badly biased **conditional on the state**. And that is
the one that mattered, because on 2026-08-12 it was caught manufacturing this
project's only apparent result.

`woods.campaign_statistics` measured **+14.1pp on 1.39M claims at family
p = 3.19e-62** — the single rule ever to survive family-wise correction here.
Against a null matched on the state it actually fired in, its lift is **−0.6pp**.
It fired when trend was extreme, which is exactly where the model was wrong:

> After a decline the Gaussian priced a +5% year-ahead call at **0.056** where
> the measured chance is **0.597**. Mean reversion. Any rule that turns bullish
> after a fall was being paid roughly ten times over for saying what the tape
> does unasked.

Measured over 473,607 windows, bucketing by `d = mu*sqrt(n)/sigma`, the
standardised return runs from mean +2.38 at strongly negative trailing drift to
−0.76 at strongly positive. Monotone across the whole range. Two things drive it:
`mu*n` grows linearly in the horizon while `sigma*sqrt(n)` grows as its square
root, so extrapolating a five-year trailing mean over a two-year window projects
a drift the market does not deliver; and a trailing mean that is itself extreme
is the one most likely to revert.

`oppollo/claims/return_law.py`.

### 4. PRICE_LEVEL — wrong, and in the opposite direction to the prediction

`grader.py` named this hole in its own comment block for a day, and predicted its
sign: *a rising market reaches a level above spot more often than a driftless
walk does, so every RESISTANCE claim carries a bias in its favour.*

The prediction was backwards. A level claim passes on **reached-and-held**, and
drift moves those two clauses opposite ways: it makes a resistance easier to
reach and harder to hold under. Near spot the holding wins (−0.137 for
RESISTANCE at n=126); past about 1σ the reach wins and the sign flips.

And drift was not even the dominant term. The biggest error is role-symmetric:
at 0.25–1σ reaches — exactly where this engine's levels sit — the driftless model
overstates by 0.03–0.07 for *both* roles, because real excursions of that size
are rarer than a Gaussian of the same measured volatility.

**4,299 claims (1.14% of the price-level population) sat on the wrong side of the
pre-registered `MAX_P_CHANCE = 0.5`** — counted as evidence where the measured
null demotes them.

`oppollo/claims/level_law.py`, measured over 767,317 windows.

### 5. DIRECTION a third time — the measured law pooled eleven horizons

The replacement in (3) buckets by `d = mu*sqrt(n)/sigma` and it does **not**
split by role and does **not** condition on the horizon: one table of z-quantiles
per bucket, measured over eleven horizons from 10 to 756 bars **at once** and
applied at every horizon, including the eight below its shortest. Streamed over
all 93,116 DIRECTION claims of the century ledger:

| role | n | rate | null | lift |
|---|---:|---:|---:|---:|
| DOWN | 56,482 | 0.2085 | 0.1680 | **+4.05pp** |
| UP | 36,634 | 0.2428 | 0.2847 | **−4.19pp** |
| pooled | 93,116 | 0.2220 | 0.2139 | +0.81pp |

Equal, opposite, cancelling in the pool — **the exact signature the Gaussian was
retired for, one level down**. Fourteen of the seventeen rules with 500+
DIRECTION claims carry it. It is why `jenkins.ninety_day_trend` cleared
family-wise correction at p = 1.26e-33 on a +3.33pp lift that is *below* the
+4.05pp every DOWN claim in the system was collecting: it cleared the bar on
**n**, not on effect.

**Why pooling horizons was illegal, and it is not a matter of taste.** `d` and
`n` are not independent coordinates. `d = mu*sqrt(n)/sigma` and `mu/sigma` is
roughly a property of the instrument, so `d` grows as `sqrt(n)`: at two bars 86%
of all windows sit in `d ∈ [−0.10, 0.10)` and nothing reaches `|d| > 0.25`; at
756 bars a quarter of the mass sits past 1.5. **Bucketing by drift was in large
part bucketing by horizon.** In the central bucket, `P(y ≥ 0.6)` runs .183 at two
bars, .254 at five, .291 at fourteen, .337 at 63, .431 at 252 and .562 at 756,
against one stored number, 0.338. The engine emits at a **median of 13 bars** and
a third of its DIRECTION claims are shorter than the shortest horizon the old law
ever measured, so it read a number calibrated near 63 bars and applied it near
10 — overstating every UP claim by ~0.06 and understating every DOWN claim by
~0.03, which are the two residuals above.

The law is now indexed on **(role, horizon, drift bucket)**: the two roles are
counted separately on the same windows (they are not each other's complement —
a window that does not move passes the grader on both sides, 2.5% of two-bar
windows), the horizon is interpolated in `log n` between 25 measured nodes as
`level_law` already did, and a thin cell is shrunk toward its own horizon's
drift-pooled row by a weight recorded in the file cell by cell. Out of sample
across the 1927/1975 split fitted both ways, separating the **design** error
(same sign in both folds) from the **era** error (opposite signs), the design
error at the horizons this engine emits into falls from DOWN +0.9…+2.1pp,
UP −8.1…−2.0pp to DOWN −0.7…+0.1pp, UP +0.3…+0.6pp. Re-streamed over the ledger:
DOWN +4.05 → **+2.14pp**, UP −4.19 → **+0.71pp**, pooled +0.81 → **+1.58pp**.

The pooled number got **worse**, and that is the point: it was small because two
large errors cancelled, and it is now larger than either half of the split that
produced it.

## The lesson, which is the same lesson five times

**An aggregate calibration is not a calibration.**

Every one of these five models passed an average check. The direction model's
error is *opposite in sign for UP and DOWN and cancels almost exactly*: pooled,
its mean error in the middle buckets is 0.006; split by direction, out of sample,
it is 0.05–0.27. The price-level model's pooled error across the whole grid is
−0.0052 against −0.023 / −0.017 by role. The aggregate check would have passed a
third time.

A null that is right on average and wrong exactly where the rules speak is more
dangerous than an obviously crude one, because it survives inspection and then
certifies a false result at p = 3e-62.

The corollary is now house rule: **split by role and bucket by state before
believing any calibration**, and `tests/test_chance_model.py` asserts that the
pooled view *hides* the error, so the lesson is pinned as code rather than prose.

Entry 5 adds the half of it that was missing: **split on every axis the law is
APPLIED across, not only the ones it is indexed on.** The 2026-08-12 validation
of the DIRECTION law reported by role and by drift bucket, and passed, because
the horizon was the axis it did not think to split. `test_chance_model.py` now
asserts the (role × horizon) grid against the model in use, and asserts that
pooling the roles hides the retired one's error.

## What was structurally missing, not just miscalibrated

Falsifiability gates existed for two claim types out of three. TIMING was gated
by `_horizon`; PRICE_LEVEL by `level_is_testable` over `band_ceiling`. **DIRECTION
was gated by nothing.** Any node could demand a move so small the claim could not
fail and the ledger booked the pass — and the refuted rule was the only one in
1.39M claims with a DIRECTION window longer than 100 days, so there was nothing
at its horizon to compare it against either.

Two wired nodes (`nirenstein.polar_ellipse`, `nirenstein.three_body_alignment`)
also turned out never to call the timing horizon at all. polar_ellipse's band
*scales with the cycle*, up to 20 days, inside a fixed 21-day window; 18% of its
graded claims sat above the grader's own demotion line. Neither was a clone of
anything, so the clone-detector never saw them — it asked "did anyone copy the
horizon" and the right question was also "does everyone who needs one use it".

Both questions are now guards, plus a third that pins one home per gate, written
the day a second `direction_is_testable` was added while an equivalent already
existed.

## What every earlier number means now

**Retired.** Any measurement in any report or rule record dated before
2026-08-12 was made against a ruler now known to be wrong. Several records carry
explicit retractions rather than edits.

**Four of the five corrections make results worse.** That is the direction an
honest correction to a too-generous null goes. The fifth splits: it takes 4pp
off every DOWN claim in the ledger and gives 5pp back to every UP claim, and the
pooled figure it leaves behind (+1.58pp) is larger than the one it replaced
(+0.81pp) because that one was a cancellation rather than a calibration.

**The fourth makes them better, and that deserves the suspicion it got.** The
price-level null *overstated* pass chances, so every level rule was scored
against a bar that was too high; overall price-level lift moved −0.0215 →
−0.0017. The defence is that the measurement was taken before the law existed and
by a tool that fits nothing, the split test is symmetric and both ways round, the
sign is forced by a property of real returns that nothing here chose, and the
pre-registered thresholds did not move. The population still does not beat its
null. It loses by less.

**`MAX_P_CHANCE = 0.5` and `MAX_P_CHANCE_DIRECTION = 0.4013` never moved.**
Making the model more accurate is the work; moving the threshold is the failure
pre-registration exists to prevent.

## Where it stands

The century measurement, 1,418,012 claims across 98.6 years and 22 instruments
under the first four corrections, reads **−0.45pp below its own null**, with the
tier ordering (verbatim > paraphrase > inferred) **retracted** — it was the
demotion artefact, not fidelity. Like for like it vanishes. Its 93,116 DIRECTION
claims have been re-priced under the fifth correction without re-running the
walk-forward, which holds the claim population fixed and is stated as a limit:
the emission gate inverts this same law, so a re-run would emit different bands.

Three rules survived family-wise correction; all three were audited
adversarially and **refuted**, and the fifth ruler error is the mechanism they
shared. At the time of writing this project has measured no edge it believes.
That is a result, and it was expensive to earn.

**What entry 5 leaves behind, stated rather than discovered later.** The residual
after the fix is no longer antisymmetric by role; it is a common-mode +1.6pp
that both roles now share, and it is not the ruler's drift or horizon
conditioning. Measured: claim windows have a standardised-return spread 3-13%
WIDER than tape windows in the same (horizon, drift) cell, in every decade from
the 1940s on and on GSPC alone as well as on the panel. Rules fire on days when
the next fortnight moves further than the trailing 60-bar volatility predicts,
which fattens BOTH tails and lifts both roles. The null cannot condition on that
without reading inside the window it is pricing, which would stop it being a
null. It is a smaller and less dangerous error than the one it replaced — a
symmetric residual cannot manufacture a DOWN rule's edge over an UP rule's — and
it is the next thing to measure, not the next thing to assume.
