- TariffPlan.arbitrage(now=...) is required (keyword-only, no default); so are homeview.trade_verdict/refusal_key's now. Existing test calls pass one. - test_arbitrage.py: no module-level decide wrapper. Fixtures that probed a one-day plan after its extremes now hold two days (as a site does); the flap day gets a unique cheapest and dearest hour; the replay breaks its ties without direction. The flap bound is <= 4 and the replay check is len(_trades) < 365 again. 228 checks pass on the old and the new code. - test_price02.py part 6: ties (equal quarters, a real 15-minute day, alternating hourly prices) and the required now. - Identity pins: the masked digest is only for the older-tree comparison; this tree's own digests are pinned with nothing hidden. - DECISIONS: the tie analysis with numbers. Co-Authored-By: Claude <noreply@anthropic.com>
4.1 KiB
PRICE-02 - the trading verdict skips price periods that have ended
Status: for review. Tests: test_price02.py. Found by AUDIT-01 (F1).
The refresh floors the plan to the top of the hour, so on the 15-minute feed
it holds the periods of this hour that are already over. The verdict took the
cheapest and dearest over every point. With modes off it could aim at a period
that had passed and then do nothing, for up to 45 min plus the refresh lag.
With modes on the allowed() gate could be open only because of such a period.
Decision: TariffPlan.arbitrage(now=...) drops the points whose period has
ended (end <= now, the span price._cover_table() gives, as
priceruns.firm_slots() does). Its three callers pass their own now:
arbitrage.decide(), Controller._mode_roles() and homeview.trade_verdict().
- Taken in the verdict, not when the plan is built: a plan built at 03:00 is still used at 03:20, and the Home chart keeps the day's earlier bars.
- An hourly point that is still current stays in, so hourly feeds and the static day/night plan give the same verdict as before.
nowis required: keyword-only, no default, like the other three arguments. A caller that could leave it out would get the elapsed periods back and the defect with them. A test that wants the whole series passes an instant before it.- The sentences of this verdict in
arbitrage.py(status row and action log, English only) say "price period" where they said "hour": the idle one now reads "now is not the cheapest or the dearest price period". The UI texts that say "cheapest hour" (AUDIT-01 T1-T5) are not touched here. - Not changed here: F2 (static profile sampled at HH:00) and F3 (carried value).
Measured: with price off, and on a static tariff, every command over the
modes-off replay is byte-identical. On the dynamic replay every command is
the same too; only a sale's sentence quotes a smaller "above the cheapest"
figure once the first cheap period is over. The identity pins in
test_modes_p3.py and test_price_run_fixes.py hide just that figure and the
word change (opt11p3_identity.trade_digest()).
Ties (round 2)
The verdict takes min/max over the points still ahead and the first one wins
a tie, so with equal prices it is the first period still ahead, which is the
current one. Before, it was the first of the whole plan, often one that had
ended. Two effects follow, and both are the card's intent, not a side effect:
- Equal quarters (hour-constant data, or a feed that prints the same price in
several quarters) now act in every quarter of the cheap (or dear) block,
not in the first only. Test:
test_price02.pypart 6. - After the cheapest period has passed, the next cheapest still ahead is the target, if its spread still clears the cost. A hourly plan did this at every refresh already (it started at the current hour); a 15-minute plan did it only from the next hour on, because it kept the elapsed quarters of this one.
Measured with scratch/PRICE-02/dev/tiesim.py (refresh every 30 min from the
floored hour, verdict at 7 min into each period, threshold 0.05, old = the
verdict over the whole plan, new = this card):
| data | days | acting periods old -> new | action changes old -> new |
|---|---|---|---|
| real 15-minute day, energy-charts 2026-09-09 | 1 | 4 -> 5 | 6 -> 6 |
| real 15-minute days 2026-09-09 and -10 | 2 | 14 -> 26 | 24 -> 24 |
| EPEX 2025-26 hourly prices as four equal quarters | 12 | 18 -> 72 | 36 -> 20 |
| the same with random +-0.002 noise per quarter (no ties) | 12 | 18 -> 33 | 36 -> 48 |
So a real 15-minute day does not switch more often (24 -> 24), and a day of
equal quarters switches less. A synthetic day of noisy, tie-free quarters does
(3.0 -> 4.0 changes a day): each period that passes hands the target to the
next cheapest one ahead. Every change is still at a period edge, never within
one. The replay in test_arbitrage.py (synthetic year, hourly) gives the same
ledger on the old and the new code once its ties are broken (280 round trips,
85 held, both): it printed one maximum twice per day, and the old fixture
tested the first of them only because its plan began at midnight.