Thales
← research journal

An internal research document, published verbatim by the automated daily export — not written for an audience, and better for it. All performance discussed is simulated paper trading; nothing here is investment advice.

CNT-1 — the go-live gate progress PUBLISHED on thales.report is one ahead of what the gate's own evaluator computes

status: open · raised 2026-08-27 (interactive session, while reviewing PR #115) · class: truth-divergence-between-layers (reporting only; the gate itself is correct) · judgement: YES · effort ~0.25 pd · horizon: live now on the public site; matters most at the December gate


Plain-language summary for an owner reading one paragraph. Three places count "how far into the clean clock are we." The one that DECIDES — go_live.py — is right. The two the owner and the public READ are both one day ahead: the daily digest says forward OOS day 53/60 and gate.json publishes Clean live history 53 of 126, while the gate's own evaluator computes 52. Nobody makes a decision off the two that are wrong, so the consequence today is small — but the site's entire premise is that every number is authored by the CLI, and this is a published number that disagrees with the criterion it claims to report. It is also the same marks-vs-returns confusion the 2026-08-17 weekly audit already had to correct once, when a hand-count mis-dated the oos-monitor maturity by four days.

Mechanism — verified line-by-line at HEAD 2072f4b

Two counting conventions, three call sites:

sitecountsvalue at the 2026-08-26 mark
backtest/oos_monitor.py:185 load_live_returnsreturns52
execution/go_live.py:234 n_live = int(live.size)returns (calls the loader)52 ✅ decides
execution/digest.py:127 sum(1 for e in equity_hist if e["date"] >= oos_since)marks53 ❌ owner reads daily
execution/public_export.py:159 "clean_trading_days": len(clean)gate.json progress.have (:312)marks53 ❌ published

load_live_returns' own docstring states the cause: "One return is lost at the boundary (it needs the prior day's equity), which is correct." A daily return is a difference between two marks, so N marks give N−1 returns. Both reporting sites count marks with an inclusive >= since filter and label the result as though it were the gate's unit.

Confirmed empirically, not by inspection alone: the loader returns 52 against committed state at the 08-26 mark, while momentum.json/gate.json published clean_trading_days: 53 the same day.

Why the obvious fix is WRONG

Do not subtract one. The gap is not a fixed offset: load_live_returns(trim_leading_flat=True) also trims leading flat days (warm-up, not performance), so the marks count is an upper bound on the returns count, not returns + 1. It happens to be exactly one today because there are no leading flats after 2026-06-11. Hardcoding -1 would be another hand-count — precisely the error that produced the 2026-09-04 mis-dating the 08-17 audit had to fix.

The honest fix is that a surface reporting the gate's progress must call the same function the gate calls, or must label its own number as marks and stop calling it gate progress.

Data plan

None — owned committed state plus static code. Reproduce with:

python -c "from thales.backtest.oos_monitor import load_live_returns; \
print(load_live_returns(state_dir='data/processed', since='2026-06-11').size)"

Test design + kill criterion (pre-registered, negative-controlled)

  1. On a fixture equity history with a known clean-clock boundary, assert the digest's live_oos_days equals load_live_returns(...).size — deny- asserted (red today).
  2. Same assertion for public_export.sleeve_stats(...)["clean_trading_days"] as consumed by gate.json progress.have.
  3. The leading-flat negative control, which is the whole point: a fixture carrying two leading flat days after since must still agree. A -1 implementation passes tests 1–2 and fails this one, so the test suite distinguishes the correct fix from the tempting one.

Kill criterion: all three green post-fix; revert the change and tests 1–2 red. If test 3 cannot be made to pass without duplicating the loader's trimming logic, that is itself the finding — it means the surfaces should call the loader rather than reimplement it, which is the preferred fix shape anyway.

Fix shape (~0.25 pd, PROPOSE-only — this row does not implement it)

Preferred: both surfaces call load_live_returns and report its .size, so there is one counter and it is the one that decides.

Fallback if the digest must stay loader-free (it is observability and must never import a decision path that could raise): keep the marks count but rename it — "53 clean marks" is true; "forward OOS day 53/60" is not, because /60 is the loader's threshold.

Scope note

vrp.json's forward gate day 33/126 is a DIFFERENT clock (the VRP 126-td forward gate, fleet_digest.py:788) and is not covered by this row; whether it shares the convention was not checked. Left explicit rather than silently assumed.


Triage note — 2026-08-31 (annotation only; the body above is untouched)

This row acquires a hard date it does not currently state: 2026-09-04. It was filed as a reporting divergence that "matters most at the December gate". That understates it — the off-by-one produces a false maturity announcement on the owner's only daily push channel, four trading days from now, in the week the 60-day clocks actually mature, during a probable owner absence.

Verified at HEAD 528793d, both counters reproduced against committed state at the 2026-08-28 mark — 55 marks vs 54 returns:

  • decides: oos_monitor.load_live_returns and go_live.py count returns;
  • reports: digest.py and public_export.py count marks;
  • published today: web/public/data/gate.json carries progress.have: 55 against a true 54.

The dated consequence. The marks counter reaches 60 on 2026-09-04, while the judge is still four days and one holiday short (the returns counter reaches 60 on 2026-09-08, independently recomputed this run and matching NORTHSTAR.md §6 exactly). At that point digest.py's if n_oos < OOS_MIN_DAYS branch flips: the daily email stops saying "verdict pending" and begins reporting the window as complete. From 09-08 fleet_digest.py prints the raw counter against a hardcoded /60 with no branch at all, so the email will read "forward OOS day 61/60", "62/60".

And digest.py carries an active false reassurance about exactly this. Its import line is annotated # one truth — true of the threshold (MIN_OOS_DAYS is shared), false of the counter, which is the half that is wrong. A maintainer reading that comment concludes the divergence cannot exist.

Ranking consequence: this moves from "cosmetic, December" to the cheapest dated item in the queue — 0.25 pd against a consequence nine days out on the channel the owner reads when he is not at his desk.


BUILT — 2026-09-02

Outcome: shipped, the honest fix. One function — oos_monitor.live_returns_from_marks (the pure core load_live_returns now calls) plus live_return_count — and every progress surface calls it: the daily digest's breadcrumb, the fleet email (which also gains a matured branch instead of "61/60" against a hardcoded /60), and public_export.sleeve_stats / gate.json's progress.have. No -1, no hand-count: marks minus trimmed leading flats minus one, computed by the judge's own code.

Route — recorded honestly: built on the owner's direct instruction (interactive, 2026-09-02, "let's do it" on the post-audit pending list), NOT via queue/approved/; a human merge.

Kill criterion — met: the lockstep test builds a history with two leading flats after the clock start (4 marks, 2 returns) and asserts the site's clean_trading_days equals load_live_returns(...).size; the two existing marks-count assertions were changed to the judge's unit (2 marks → 1), which is exactly the one-day-ahead defect the row describes. The digest line that would have announced "60/60" on 2026-09-04 now reads 59/60 that day and matures on 09-08 with the judge.


Provenance recorded by triage 2026-09-07 (annotation only; the record above is untouched): shipped in PR #141 (b50ae9b, merged 2026-09-03).