Live calibration tracking¶
The holdout gate answers was this model calibrated on history. This answers is it still calibrated on money that was actually at risk.
They are different questions, and the second one is the one that costs you money when the answer changes.
The quick version¶
from pmlab import CalibrationTracker
tracker = CalibrationTracker.from_path("artifacts/paper_trades.json")
overall = tracker.overall()
print(f"{overall.n_trades} realized brier {overall.brier_score:.4f} "
f"reliability {overall.reliability:.4f}")
# Has it drifted? Compare early windows to late ones.
for w in tracker.rolling(window=50):
print(w.window_end, round(w.brier_score, 4))
# The gate promotes per segment, so check per segment.
for segment, w in tracker.by_segment().items():
print(segment, round(w.brier_score, 4), round(w.reliability, 4))
Which number to read¶
brier_score is the headline, but it moves with the base rate: a market that
resolves YES 95% of the time is easy to score well on. Two numbers are more
honest about calibration specifically:
reliability— the probability-weighted mean squared gap between what you forecast and what occurred. Lower is better. This is the miscalibration number. It does not improve just because the base rate is extreme.skill_score— Brier relative to always predicting the base rate.0.0means you added nothing over the climatology; negative means you did worse than a constant.
resolution (higher is better) says how much your forecasts actually vary with
the outcome. A model that always predicts the base rate has reliability 0 —
perfectly calibrated, and perfectly useless. Read it alongside reliability.
outcome is about the label, not your profit¶
This one is worth stopping on, because the field name invites the wrong reading.
SettlementEngine writes:
outcome = "won" if trade["outcome_label"] == winning_label else "lost"
There is no reference to direction. So a direction="no" trade whose label
does occur is recorded as "won" with a negative realized_pnl:
direction=no, outcome_label=YES, winning_label=YES
outcome = "won"
realized_pnl = -10.03
(A stake of 10 on the NO token at 0.60, with a 30bps fee: the token paid nothing, so the whole stake and the fee are gone. Releases up to 0.9.0 booked -6.6967 here; see the CHANGELOG for how to find the trades they settled.)
For calibration that is exactly what we want — we are scoring a forecast about
the label, not the profitability of a position. But if you write your own
reporting on this field, "won" means the thing happened, not we made money.
Consequence worth internalising: calibration and PnL are independent. A model can be perfectly calibrated and lose money (fees, adverse selection, sizing), or be badly calibrated and win for a while (luck, or a favourable base rate). You need both numbers; neither one substitutes for the other.
What gets scored, and what doesn't¶
Only trades that carry both a model_prob and a settled outcome:
- Open trades are excluded. No truth has arrived, and including them would be lookahead.
- Trades without
model_probare excluded and counted inn_skipped. Trade logs written before v0.8.5 don't carry the forecast, so those rows are unscoreable, not evidence of a bad forecast. Treating a missing prediction as0.0would manufacture miscalibration that never happened.
tracker = CalibrationTracker.from_path(path)
if tracker.n_skipped:
print(f"{tracker.n_skipped} rows have no forecast recorded — pre-0.8.5 log")
overall() returns None when nothing is scoreable, rather than raising or
reporting a zero. An empty history is not a calibrated model.
Ordering: truth time, not write time¶
Windows are ordered by target_date, not recorded_at. A trade recorded in
December about a June event belongs in June's calibration history — a forecast is
scored when its truth lands, not when someone wrote the row. This matters for any
backfilled or replayed log, where the two dates disagree.
Why per-segment¶
Because an aggregate can hide a broken segment, and the gate promotes per segment. 18 well-forecast politics trades plus 2 badly-forecast sports trades:
overall brier 0.0855 looks fine
politics brier 0.0025
sports brier 0.9025 broken
The aggregate is the number you'd put in a dashboard. The segment breakdown is the number that tells you to stop trading sports.
Limits¶
- Binary only. One
model_probper trade against one label occurrence. Multiclass markets are scored per-outcome-label row, not jointly. - No alerting. This computes numbers; deciding when a number is bad enough to act on is the watchdog's job (#14). Deliberately separate — thresholds are a policy choice, and burying one here would hide it.
- Reliability needs volume. With 10 bins and 20 trades, most bins hold one or
two samples and the curve is noise. Each curve point carries
nso you can see when you are reading noise; preferreliabilityover eyeballing the curve until you have a few hundred realized trades.