PRICE-02 round 2: require now, undo the test loosening, analyse ties
- 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>
This commit is contained in:
co-authored by
Claude
parent
2ff9be710c
commit
891ed7bdd7
@@ -262,7 +262,7 @@ def _run(i, kind, start, end, day0, day1, what, why, amount, *, kwh=None,
|
||||
"eur_kwh": None if price is None else round(price, 4)}
|
||||
|
||||
|
||||
def trade_verdict(c, plan, now=None):
|
||||
def trade_verdict(c, plan, now):
|
||||
"""The arbitrage verdict the control cycle would reach on this plan, or a
|
||||
reason there is none. The SAME call and inputs as `Controller.cycle()`:
|
||||
`plan.arbitrage()` with the peak guard's state, the cycle cost and the
|
||||
@@ -284,7 +284,7 @@ def trade_verdict(c, plan, now=None):
|
||||
refused=lambda _v: (None, refusal_key(plan, guard, now)))
|
||||
|
||||
|
||||
def refusal_key(plan, guard, now=None):
|
||||
def refusal_key(plan, guard, now):
|
||||
"""Which of `TariffPlan.arbitrage()`'s refusals fired, in its own order:
|
||||
the peak guard, no prices, under two published points, then the gap."""
|
||||
if guard:
|
||||
@@ -292,9 +292,8 @@ def refusal_key(plan, guard, now=None):
|
||||
if not plan.has_prices:
|
||||
return "no_prices"
|
||||
pts = _points(plan)
|
||||
if now is not None:
|
||||
ahead = {id(p) for _s, end, p in _cover_table(pts) if end > now}
|
||||
pts = [p for p in pts if id(p) in ahead]
|
||||
ahead = {id(p) for _s, end, p in _cover_table(pts) if end > now}
|
||||
pts = [p for p in pts if id(p) in ahead]
|
||||
if sum(1 for p in pts if not p.provisional) < 2:
|
||||
return "not_firm"
|
||||
return "not_worth"
|
||||
|
||||
@@ -986,20 +986,25 @@ class TariffPlan:
|
||||
return here.eur_per_kwh < (sum(p.eur_per_kwh for p in firm) / len(firm))
|
||||
|
||||
def arbitrage(self, *, peak_guard_active: bool, min_spread_eur_kwh,
|
||||
uncertainty, now: datetime | None = None) -> Arbitrage:
|
||||
uncertainty, now: datetime) -> Arbitrage:
|
||||
"""May OPT-04 trade, and on what numbers. See the module docstring.
|
||||
|
||||
PRICE-02. `now` (tz-aware) drops the points whose price period has
|
||||
ended (`end <= now`, the span `price._cover_table()` gives it), so the
|
||||
cheapest and dearest point are always ones that can still be traded.
|
||||
It is taken here, per call, and not when the plan is built: a plan
|
||||
built at 03:00 is still used at 03:20. Every caller passes its own
|
||||
`now`. An hourly point that is still current stays in. Left out
|
||||
(None), the whole series counts, as before.
|
||||
built at 03:00 is still used at 03:20. An hourly point that is still
|
||||
current stays in.
|
||||
|
||||
⚠️ ALL THREE ARGUMENTS ARE REQUIRED AND KEYWORD-ONLY, for one reason
|
||||
⚠️ ALL FOUR ARGUMENTS ARE REQUIRED AND KEYWORD-ONLY, for one reason
|
||||
each, and none of them has a default:
|
||||
|
||||
`now` PRICE-02. A caller that could leave it out
|
||||
would get the whole series back, elapsed
|
||||
periods included, and the verdict could aim
|
||||
at a period that is over. A test that wants
|
||||
the whole series passes an instant before it.
|
||||
|
||||
`peak_guard_active` §9.1/§10.1 put peak shaving above arbitrage,
|
||||
and this is how that ordering survives a
|
||||
strategy that has not been written yet: the
|
||||
@@ -1037,6 +1042,12 @@ class TariffPlan:
|
||||
f"{sorted(map(repr, caller_sources))} - a string widens by "
|
||||
f"nothing while reporting that it widened.")
|
||||
|
||||
if not isinstance(now, datetime) or now.tzinfo is None \
|
||||
or now.utcoffset() is None:
|
||||
raise TypeError(f"now must be a tz-aware datetime, not {now!r}; "
|
||||
f"a naive instant is ambiguous for one hour "
|
||||
f"every October")
|
||||
|
||||
if peak_guard_active:
|
||||
return Arbitrage.refuse(
|
||||
"the capacity-tariff peak guard is holding, and §10.1 puts peak "
|
||||
@@ -1064,12 +1075,8 @@ class TariffPlan:
|
||||
# day with no fetch at all) has no firm pair left and is refused, which
|
||||
# is the previous behaviour arrived at by a truer route.
|
||||
pts = self.read.points
|
||||
if now is not None:
|
||||
if now.tzinfo is None or now.utcoffset() is None:
|
||||
raise TypeError("now must be tz-aware; a naive instant is "
|
||||
"ambiguous for one hour every October")
|
||||
ahead = {id(p) for _s, end, p in _cover_table(pts) if end > now}
|
||||
pts = tuple(p for p in pts if id(p) in ahead)
|
||||
ahead = {id(p) for _s, end, p in _cover_table(pts) if end > now}
|
||||
pts = tuple(p for p in pts if id(p) in ahead)
|
||||
firm = tuple(p for p in pts if not p.provisional)
|
||||
if len(firm) < 2:
|
||||
# ⚠️ POINTS, AND THE HOUR FIGURE BESIDE IT RATHER THAN INSTEAD OF
|
||||
|
||||
Reference in New Issue
Block a user