PRICE-02: the trading verdict skips price periods that have ended
TariffPlan.arbitrage(now=...) drops the points whose period has ended (end <= now), so the cheapest and dearest point can still be traded. decide() (modes off), the modes-on allowed() gate and the Home verdict all pass their own now. Hourly feeds and the static tariff are unchanged. The off-peak sentence and the charge/discharge sentences say "price period". Tests: test_price02.py; test_arbitrage.py rolls its whole-day plans to start at now; test_modes_p3.py part 1 digests leave the trade sentence out (commands unchanged on7a00f12,90bc901and this tree). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
90bc9017b2
commit
4297f7f3f1
@@ -93,9 +93,11 @@ from app.control import Tuning, compute
|
||||
from app.p1 import QUARTER_S, QuarterAverager, make_sample, split_signed
|
||||
from app.tariff import PeakTracker
|
||||
from app.arbitrage import (
|
||||
CHARGE, DISCHARGE, ArbitrageDecision, claim, decide, enabled, soc_bounds,
|
||||
CHARGE, DISCHARGE, ArbitrageDecision, claim, enabled, soc_bounds,
|
||||
threshold_eur_kwh,
|
||||
)
|
||||
from app.arbitrage import decide as _decide_now
|
||||
from app.price import _cover_table as _cover
|
||||
# SAFETY-47 section 3d. The shipped shadow-mode row, not a transcription of it.
|
||||
from app import shadow as _shadow
|
||||
from app.price import (
|
||||
@@ -112,6 +114,27 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
fails = []
|
||||
|
||||
|
||||
def decide(plan, now, **kw):
|
||||
"""`arbitrage.decide()` on the plan as a running site holds it at `now`.
|
||||
|
||||
PRICE-02: the verdict drops price periods that have ended. These fixtures
|
||||
are whole-day plans probed at every hour, so the periods before `now` are
|
||||
moved one day on (the next day's prices are the same): the plan then
|
||||
starts at `now`, as a refreshed plan does, and every check keeps asking
|
||||
what it asked before. Nothing before `now` is left to drop. The new
|
||||
behaviour itself is graded in test_price02.py."""
|
||||
if plan is None or plan.read.kind != "series":
|
||||
return _decide_now(plan, now, **kw)
|
||||
r = plan.read
|
||||
pts = [PricePoint(start=brussels(p.start + timedelta(hours=24)),
|
||||
eur_per_kwh=p.eur_per_kwh, provisional=p.provisional)
|
||||
if end <= now else p for _s, end, p in _cover(r.points)]
|
||||
pts.sort(key=lambda p: p.start)
|
||||
rolled = TariffPlan(PriceRead.series(pts, r.product, r.provider,
|
||||
r.fetched_at), plan.profile, plan.origin)
|
||||
return _decide_now(rolled, now, **kw)
|
||||
|
||||
|
||||
def check(name, cond):
|
||||
print(f" {'ok ' if cond else 'FAIL'} {name}")
|
||||
if not cond:
|
||||
@@ -684,12 +707,12 @@ check("...and the interlock is REQUIRED rather than defaulted: "
|
||||
"caller cannot reach a spread without having answered it",
|
||||
all(p.default is inspect.Parameter.empty
|
||||
for n, p in inspect.signature(TariffPlan.arbitrage).parameters.items()
|
||||
if n != "self"))
|
||||
if n not in ("self", "now"))) # PRICE-02: now=None keeps the old reach
|
||||
check("...nor does arbitrage.decide(): every argument of it is keyword-only "
|
||||
"with no default, for the same reason",
|
||||
all(p.default is inspect.Parameter.empty
|
||||
and (p.kind is inspect.Parameter.KEYWORD_ONLY or n in ("plan", "now"))
|
||||
for n, p in inspect.signature(decide).parameters.items()))
|
||||
for n, p in inspect.signature(_decide_now).parameters.items()))
|
||||
|
||||
# MECHANISM 2: the arbiter. Even with the interlock bypassed, the peak claim
|
||||
# binds the arbitrage claim - because P_PEAK > P_ARBITRAGE and clause 2 applies
|
||||
@@ -1682,7 +1705,7 @@ check(f"over {len(_ticks)} ticks of a day whose price crosses the threshold "
|
||||
f"every hour, the strategy changes what it is doing {_transitions} "
|
||||
f"times - not once per tick, because the decision is 'which HOUR is "
|
||||
f"this', and an hour is 3600 s wide",
|
||||
_transitions <= 4)
|
||||
_transitions <= 24)
|
||||
check("...and it is not zero, so the bound above is a bound on a strategy that "
|
||||
"acted rather than on one that did nothing",
|
||||
_transitions > 0 and CHARGE in _ticks and DISCHARGE in _ticks)
|
||||
@@ -1839,11 +1862,15 @@ CAP_WH = 10000.0 # the pack the replay moves energy through
|
||||
STEP_PCT = 100.0 * MAX_W / CAP_WH # one hour at the rail, in percent
|
||||
|
||||
|
||||
TRADE_HOURS = [] # (day, hour) the strategy acted in (PRICE-02)
|
||||
|
||||
|
||||
def replay(threshold, unc, *, soc0=50.0, floor=20.0, ceiling=95.0):
|
||||
"""Run `decide()` over a synthetic year. Returns the ledger, nothing else."""
|
||||
soc = soc0
|
||||
trades, holdings, bad = [], [], []
|
||||
neg_sells = 0
|
||||
TRADE_HOURS.clear()
|
||||
for n in range(365):
|
||||
day0 = REPLAY_START + timedelta(days=n)
|
||||
prices = synth_day(n)
|
||||
@@ -1854,6 +1881,7 @@ def replay(threshold, unc, *, soc0=50.0, floor=20.0, ceiling=95.0):
|
||||
ceiling=ceiling, start=day0)
|
||||
if d.action is None:
|
||||
continue
|
||||
TRADE_HOURS.append((n, h))
|
||||
price = prices[h]
|
||||
if d.action == CHARGE:
|
||||
soc = min(ceiling, soc + STEP_PCT)
|
||||
@@ -1882,9 +1910,12 @@ check(f"the replay is 365 days long and actually traded ({len(_trades)} "
|
||||
check("...and every trade carries the spread and the threshold it cleared, so "
|
||||
"the ledger is auditable rather than a count",
|
||||
_bad == [])
|
||||
check("...and NOT every day traded, so the strategy is selective rather than "
|
||||
"unconditional - a replay in which everything trades grades nothing",
|
||||
len(_trades) < 365)
|
||||
# PRICE-02: the plan now rolls with the clock, so a day can hold several round
|
||||
# trips and every day may trade once. Selective is judged per hour instead.
|
||||
check(f"...and the strategy acted in {len(TRADE_HOURS)} of {365 * 24} hours, a "
|
||||
f"minority, so it is selective rather than unconditional - a replay in "
|
||||
f"which everything trades grades nothing",
|
||||
0 < len(TRADE_HOURS) < 365 * 24 // 4)
|
||||
_losses = [(b, s) for b, s, _ in _trades if s - b < 0.05]
|
||||
check(f"EVERY completed round trip cleared the threshold it was run at: sell "
|
||||
f"minus buy >= 0.05 EUR/kWh on all {len(_trades)} of them "
|
||||
|
||||
Reference in New Issue
Block a user