SoW: MMLPv2 Settlement Calendar

Author: Andrew Lee (with Claude)

Tracking: A-4770 (parent: settlement ignores market sessions) → A-4771 (calendar core) · A-4772 (trading-calendar proration) · A-4813 (daily maintenance window) · A-4774 (sessions in the rewards GUI) · A-4783 (coverage invariant alignment).

Date: 2026-08-31

Why now: MMLPv2 went live on prod today in recording-only mode (MMLP_SNAPSHOT_ENABLED/MMLP_BACKFILL_ENABLED on; settle and payout off). Every gap in this SoW is a settlement gap: nothing here loses recorded data, but none of it may be wrong when the settler first runs, because the accrual ledger is immutable — an epoch settled thin stays thin. This SoW is the gate on MMLP_SETTLE_ENABLED.

Decision (2026-09-03), superseding work item 2's open question: design (b) is what shipped for the pool, on top of the maintenance-window constant from (a). epoch_pool now returns the full daily pool regardless of coverage; prorated_pool is deleted. The per-snapshot normalization in daily_weights already distributes that pool over exactly the scoring minutes, which is what the published terms describe (§3.3, Example 3) — so the pre-cap allocations of a thin day sum to the whole P_daily, the 40% cap and the min($10, 1% × P_daily) threshold are computed against it, and a day with zero scoring snapshots pays nothing because it is never settled at all.

The (a) half is kept and unchanged: the maintenance hour still leaves both the numerator and the denominator, so covered/expected stay meaningful as a coverage figure. They are now displayed and alerted on (#3889) and never priced in. MMLP_SETTLE_MIN_COVERAGE_PCT stays off by default and stays an operational hold rather than a payout rule: above the floor an epoch pays in full, below it the tick refuses to settle rather than paying less. The accepted trade-off is the one item 2 named — a genuine sampling outage concentrates a whole day's pool on whoever quoted through it, rather than shrinking the pool.

Decision (2026-09-01): AX trades every calendar day — prod shows fills on all seven weekdays for equity, FX, metals and energy perps — so class 1 below does not exist for us and the trading-calendar branch (#3872, A-4771/A-4772/ A-4774) is abandoned. Class 2 is fixed by design (a): the maintenance hour is a constant in ax-mmlp2 (MAINTENANCE_WINDOW, epoch.rs), and every minute in it leaves the settlement denominator. Sampling coverage (the admin cards and the A-4783 invariant) counts sampled cursor minutes, which the snapshotter writes through the window, so those already read 100% on a clean day and need no calendar.

Addendum (2026-09-01): minutes scored inside the window are also ignored by the settler (the numerator, scoring_rows in settle.rs), not only removed from the denominator. Demo's EP3 sandbox scores straight through the window (every 2026-08-31 demo epoch had 57–60 rows inside 20:00–21:00Z, so covered = 1440 > 1380 and the epoch refused to settle), and prod's EP3 can return from maintenance early. covered is the number of scoring minutes present outside the maintenance window; rows scored inside it are ignored and logged. Over-coverage therefore means duplicate rows only.

The problem, in two layers

Settlement prices each epoch as pool_daily × covered / expected (prorated_pool, rs/sdk-internal/mmlp2/src/settle.rs:291), where pool_daily = P / calendar-month days, covered = scored minutes (settle_epoch, rs/accrual-engine/src/mmlp/settle.rs:70), and expected = all wall-clock minutes of the epoch (1440, DST-adjusted). Two classes of minutes can never score, and both silently dilute payouts:

  1. Non-trading days (weekends, holidays). A session-bound symbol (equity / FX / metals / treasuries perps) pays only its trading fraction of the intended pool, and closed calendar days dilute the monthly budget. This is A-4770/A-4771/A-4772.
  2. The daily maintenance window, 3–4 PM US/Central. EP3 is down; no minute in that hour produces a valid mark, so even a perfect trading day settles at ≤ 1380/1440 — ~4.17% of every daily pool retained, every day, on every symbol (~$8,300/month against the $200k program). This is A-4813, found 2026-08-31. It also makes the settler warn! on every epoch and would make the coverage invariant (A-4783 / PR #3889) alert daily.

Both diverge from the published program doc (AX_Liquidity_Program_v8262026, §3.3), which distributes each day's pool entirely across scoring snapshots — "non-scoring snapshots count toward neither the numerator nor the denominator" — with no wall-clock proration anywhere in the customer-facing math.

Existing work (do not rebuild)

Branch tin/mmlp2-session-calendar (Tin, current with main as of 2026-08-29; PR #3872 was closed unreviewed and needs reopening or a successor):

Work items

  1. Land the calendar core (A-4771) and trading-calendar proration (A-4772). Reopen #3872 or split the branch into the two PRs the tickets describe. Needs review attention first — it changes money math.
  2. Maintenance-window awareness (A-4813). Two candidate designs — decide before implementation, with BD in the room:
  3. Downtime source of truth (only if design (a) is chosen). The question the maintenance window begs: where does "3–4 PM CT daily" live? Candidates, in rough order of preference:
  4. Coverage invariant alignment (A-4783 / PR #3889). mmlp-epoch-coverage-complete must measure against the same calendar (and downtime window, if modelled) as settlement, or it alerts on every epoch. Same for the epoch-coverage displays in the admin GUI.
  5. Sessions in the rewards GUI (A-4774). Surface the open/closed/holiday classification so operators and makers can tell a closed market from a broken pipeline.
  6. Reconcile the published doc. The doc's P_daily = P / n (calendar-month days) is itself the class-1 bug in prose; the fix changes the published formula. Changes must be announced "prior to the start of the monthly reward period" (doc, Appendix B) — coordinate the doc revision and MM announcement with BD so code and terms flip together, ideally for the October period with September settled under the corrected math from day one.

Sequencing and the enable-settle gate

Recording is live and unaffected; the settler's lookback re-scans past epochs, so nothing is lost while this lands. Two clocks are running:

Enable-settle checklist (all must hold before MMLP_SETTLE_ENABLED=true on prod): items 1–2 merged and released; item 4 merged (or #3889 held back); MMLP_SETTLE_GRACE_SECS ≥ MMLP_BACKFILL_LOOKBACK_SECS (the runbook claims this is validated at startup — it is not; validate it in code as part of this work); a spot-check of one settled demo epoch against the doc's worked examples.

Effort

Item 1 is review-and-land (the code exists). Item 2 is the design decision plus a focused change to settle.rs/prorated_pool either way; (b) is less code than (a). Items 3–5 are small once 2 is decided. Item 6 is coordination, not code. The critical path is the design decision and review bandwidth on money math, not implementation volume.