"""Runnable check for OPT-04 - price arbitrage. `python3 test_arbitrage.py` ⚠️ RUN IT WITH `python3 -B` AND PURGE `__pycache__` FIRST. TEST-15 records a size-preserving edit, reverted inside one second, whose OLD BYTECODE was still what ran - confirmed by disassembly - so a mutation graded code that was not there. Every mutation reported in the PR for this card was run as: find . -name __pycache__ -type d -exec rm -rf {} + ; python3 -B test_*.py WHAT THIS FILE IS DEFENDING, in the order the card's acceptance criteria name it: AC1 NO ACTION when the expected benefit is below `cycle_cost_threshold_eur`. Measured by BISECTING the accept/refuse boundary and comparing the boundary with a threshold derived here by different arithmetic - not by reading a constant back out of the module that defines it. AC2 NEGATIVE-PRICE HOURS NEVER TRIGGER EXPORT (§17). Both sides: the same series with the sign of one hour changed must trade in one case and refuse in the other, or the guard is graded by nothing. AC3 YIELDS TO PEAK SHAVING (§9.1). Through the two mechanisms that already exist - static_tariff's required `peak_guard_active` interlock and arbiter.py's clause 2 - both driven end to end, plus the third state (the guard never ran) that neither of them can express on its own. AC4 NET-POSITIVE ACROSS 12 MONTHS OF REPLAYED PRICES. ⚠️ MET AS OF DATA-02, AND SECTIONS 7 AND 7B DO DIFFERENT HALVES OF IT. 7B replays twelve months of REAL EPEX BE day-ahead prices - `scenarios/epex-be-dayahead-*.json`, 8760 hours, public data under CC BY 4.0 - through the shipped `decide()`, and reports the euro figure at the shipped threshold. Section 7 keeps the SYNTHETIC year, because the real one turns out to be unable to grade §17 at all: not one of its 365 days has a negative DEAREST hour, so a count of zero negative-price discharges over it grades nothing. 7B asserts that degeneracy rather than quietly reporting the zero. THREE RULES THIS FILE HOLDS ITSELF TO, because the reviewer will: * EVERY VERDICT IS DERIVED. No `check("the guard works", True)`. The widened threshold is bisected rather than read; the arbiter's ordering is computed from the priority constants rather than typed; the replay's net figure is accumulated from the same trades the strategy actually chose. * NO STIMULUS IS EVALUATED WHERE THE QUANTITY IT GRADES IS DEGENERATE. This ticket is thick with that trap and each one is asserted before it is used: - A SERIES WHOSE CHEAPEST HOUR IS ALSO ITS FIRST HOUR cannot tell "acts in the cheapest hour" from "acts in the first hour". The fixture's cheapest and dearest hours are neither the first nor the last, and neither is adjacent to the other. - THE NEGATIVE-PRICE CASE MUST STILL CLEAR THE THRESHOLD, or §17's guard is graded on a series that the spread rung already refused, and deleting the guard changes nothing. The fixture's all-negative day has a 0.25 EUR/kWh spread, asserted before it is used. - IT MUST ALSO BE THE DEAREST HOUR THAT IS NEGATIVE. A day with one negative hour that is the CHEAPEST one is a day the guard never consults, and it would pass with the guard removed. - `peak_guard_active` FALSE AND TRUE MUST DIFFER ON THE SAME PLAN, or AC3 is graded by a plan that refuses for some other reason anyway. - THE PEAK FLOOR MUST BE INSIDE THE ARBITRAGE CHARGE, or clause 2 clamps nothing and the arbiter check passes for an unbound claim. - SoC MUST START STRICTLY BETWEEN FLOOR AND CEILING in every check that is not about SoC, or a refusal attributed to price is really about charge. * A FAILED READ IS NOT AN OBSERVATION. No plan yet resolved, a plan that has aged out, an hour the series does not cover, an unread state of charge and a peak guard that never ran are five DIFFERENT reasons - and none of them is an answer. Section 5 drives all five and asserts they are distinguishable. ⚠️ MUTATING THE MODULE THIS FILE GRADES? PURGE `__pycache__` FIRST - or run `tools/mutation_run.sh`, which does. `python3 -B` alone does NOT save you: it stops bytecode being WRITTEN, not a stale one being READ, and a size-preserving edit reverted inside one second is invisible to CPython's staleness test. That is TEST-15, and it has already turned one live mutation into a false survivor. Read MUTATION.md before you report a survivor. """ import ast import inspect import math import os import sys from datetime import datetime, timedelta, timezone from app import arbitrage as _arb from named_absence import first from app import main as _main from app.price_chain import PriceWatch from app import peakguard as _pg from app.arbiter import ( P_ARBITRAGE, P_LOOP, P_MAINT_SHAPE, P_MAINTENANCE, P_PEAK, P_SAFETY, Claim, resolve, ) # SAFETY-45 section 3c. The shipped loop, the shipped billed-quarter # accumulator and the shipped projection - nothing here is transcribed. 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, threshold_eur_kwh, ) # SAFETY-47 section 3d. The shipped shadow-mode row, not a transcription of it. from app import shadow as _shadow from app.price import ( PRICE_OPTION_DEFAULTS, Absence, PricePoint, PriceProduct, PriceRead, Uncertainty, WIDEN_FACTOR, brussels, required_spread, widen_factor, ) from app.static_tariff import ( TariffPlan, parse_static_profile, resolve_tariff, static_plan, ) from app.validate import validate_startup_config UTC = timezone.utc HERE = os.path.dirname(os.path.abspath(__file__)) # PRICE-02: `TariffPlan.arbitrage(now=...)` is required. This instant is before every # point these fixtures build, so no period has ended and the whole series counts. from datetime import datetime as _DT, timezone as _TZ # noqa: E402 LONG_AGO = _DT(2000, 1, 1, tzinfo=_TZ.utc) fails = [] def check(name, cond): print(f" {'ok ' if cond else 'FAIL'} {name}") if not cond: fails.append(name) def raises(fn, exc): """True only if fn() raises EXACTLY `exc`. A different exception is False - 'it was refused' and 'it broke on the way to being refused' must not grade the same.""" try: fn() except exc: return True except Exception: return False return False class _Raised: def __init__(self, e): self.e = e def __eq__(self, other): return False def __hash__(self): return id(self) def __bool__(self): return False def __repr__(self): return f"" def derived(fn): """A value a check grades. An exception becomes `_Raised`, which equals nothing and is falsy - NOT a default. See test_price.py for why.""" try: return fn() except Exception as e: # noqa: BLE001 return _Raised(e) def _fn(module_src, name): tree = ast.parse(module_src) for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) \ and node.name == name: return node return None def _awaited(node): """Every name awaited anywhere inside `node`.""" out = set() for n in ast.walk(node): if not isinstance(n, ast.Await): continue f = n.value.func if isinstance(n.value, ast.Call) else n.value out.add(getattr(f, "id", None) or getattr(f, "attr", None)) return {n for n in out if n} def near(a, b, tol=1e-9): try: return abs(float(a) - float(b)) <= tol except (TypeError, ValueError): return False # --------------------------------------------------------------------------- # # Fixtures # --------------------------------------------------------------------------- # D0 = datetime(2026, 1, 15, 0, 0, tzinfo=UTC) MAX_W = 2000.0 NO_WEATHER = frozenset({Uncertainty.NO_WEATHER}) def plan_of(prices, *, product=PriceProduct.DAY_AHEAD, start=D0, provisional=False, provider="generic_dayahead", days=2): """A TariffPlan over consecutive hourly prices starting at `start`. `provisional` may be a single bool or a per-hour sequence. PRICE-02: the verdict skips price periods that have ended, so a plan probed at 18:30 no longer sees the cheap hour at 03:00 of the same day. A running site holds 24-48 h, so `days=2` repeats the day (tomorrow's prices are today's): probes in day one keep their extremes, and the first cheapest/dearest hour still ahead is today's. Pass `days=1` for a plan that must be exactly the given hours. """ prices = list(prices) * days flags = ([bool(provisional)] * len(prices) if isinstance(provisional, bool) else list(provisional) * days) pts = [PricePoint(start=brussels(start + timedelta(hours=i)), eur_per_kwh=round(float(p), 6), provisional=f) for i, (p, f) in enumerate(zip(prices, flags))] return TariffPlan(PriceRead.series(pts, product, provider, start), None, "provider") def hour(i, *, start=D0): """Mid-hour instant of slot `i` - never a boundary, where two answers agree.""" return start + timedelta(hours=i, minutes=30) # SAFETY-45. THE HEADROOM EVERY CHECK THAT IS NOT ABOUT THE HEADROOM USES, and # it is deliberately NOT binding: `min(MAX_W, this)` is MAX_W, so every existing # check in this file grades what it always graded and the new rung is decoration # in all of them. Section 3b is where it is made to bind, and 3b asserts the # precondition that it does (MUTATION.md habit 1) rather than hoping. HEADROOM_CLEAR = MAX_W * 10.0 def act(plan, i, *, peak=False, soc=50.0, thresh=0.0, unc=frozenset(), floor=20.0, ceiling=95.0, start=D0, headroom=HEADROOM_CLEAR): """`decide()` with the fixture's constants filled in. One call shape.""" return decide(plan, hour(i, start=start), peak_guard_active=peak, soc=soc, charge_headroom_w=headroom, min_spread_eur_kwh=thresh, uncertainty=unc, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=floor, soc_ceiling_pct=ceiling) # ⚠️ THE SHAPE OF THIS DAY IS THE WHOLE DEGENERACY ARGUMENT. The cheapest hour # is slot 3 and the dearest is slot 18: neither is first, neither is last, # neither is adjacent to the other, and the last hour is not the dearest. An # implementation that acted on "the first hour", "the last hour", "the hour # after the cheapest" or "min/max swapped" produces a different answer from the # correct one at every probe below. DAY = [0.240, 0.230, 0.215, 0.150, 0.190, 0.225, 0.260, 0.300, 0.320, 0.310, 0.295, 0.280, 0.270, 0.275, 0.285, 0.300, 0.330, 0.360, 0.410, 0.380, 0.340, 0.300, 0.270, 0.250] CHEAP_H, DEAR_H = 3, 18 SPREAD = round(max(DAY) - min(DAY), 6) print("0. the fixture is not degenerate (asserted before anything is graded)") check(f"the cheapest hour is slot {CHEAP_H} and the dearest slot {DEAR_H} - " f"neither first, neither last, and not adjacent", DAY.index(min(DAY)) == CHEAP_H and DAY.index(max(DAY)) == DEAR_H and CHEAP_H not in (0, len(DAY) - 1) and DEAR_H not in (0, len(DAY) - 1) and abs(CHEAP_H - DEAR_H) > 1) check(f"...and its spread is {SPREAD:.4f} EUR/kWh, comfortably above the " f"shipped threshold, so the checks below grade the strategy rather than " f"the spread rung", SPREAD > PRICE_OPTION_DEFAULTS["cycle_cost_threshold_eur"] * 1.5) check("...and its minimum and its maximum each occur EXACTLY ONCE, so 'the " "cheapest hour' names one hour and an undefined tie-break is not what " "any check below is really grading", DAY.count(min(DAY)) == 1 and DAY.count(max(DAY)) == 1) check("...and no price in it is zero or negative, so section 2's sign flip is " "the only source of a negative price in this file", all(p > 0 for p in DAY)) # --------------------------------------------------------------------------- # print() print("1. AC1 - nothing happens below cycle_cost_threshold_eur") PLAN = plan_of(DAY) _buy = act(PLAN, CHEAP_H, thresh=0.0) _sell = act(PLAN, DEAR_H, thresh=0.0) check("with a zero threshold the cheapest hour CHARGES and the dearest hour " "DISCHARGES - the two actions this strategy has, both reachable", _buy.action == CHARGE and _sell.action == DISCHARGE) check("...charging is a NEGATIVE setpoint and discharging a positive one, " "which is the sign convention maintenance's `hi <= 0` shaping and " "peakguard's `lo >= 0` floor are both written against", _buy.power_w is not None and _sell.power_w is not None and _buy.power_w < 0 < _sell.power_w) check("...and an hour that is neither buy nor sell does NOTHING, so the " "strategy is not simply always-on", act(PLAN, 10, thresh=0.0).action is None) # ⚠️ MEASURED, NOT READ. The boundary is found by bisection on the threshold, # so this grades the shipped refusal ladder rather than restating a constant # that lives in the module under test. def boundary(unc, plan=PLAN, lo=0.0, hi=10.0): """The largest threshold this plan still trades under, to 1e-9.""" for _ in range(200): mid = (lo + hi) / 2.0 if act(plan, CHEAP_H, thresh=mid, unc=unc).action is None: hi = mid else: lo = mid return lo _b_plain = derived(lambda: boundary(frozenset())) check(f"the accept/refuse boundary with no uncertainty is the spread itself " f"({_b_plain!r} vs {SPREAD}) - a cycle trades exactly while its spread " f"covers its cost, and not one cent further", near(_b_plain, SPREAD, 1e-6)) check("...so a threshold a hair ABOVE the spread refuses and a hair BELOW it " "trades: the boundary is a boundary, not a slope", act(PLAN, CHEAP_H, thresh=SPREAD + 1e-4).action is None and act(PLAN, CHEAP_H, thresh=SPREAD - 1e-4).action == CHARGE) _refusal = act(PLAN, CHEAP_H, thresh=SPREAD + 1e-4).reason check("...and the refusal SAYS the spread and the number it had to cover, so " "an operator can tell 'no opportunity' from 'broken'", "does not cover" in _refusal or "needs" in _refusal) # The widening path, end to end: the boundary must move by exactly the declared # factor. Derived twice - once by bisection, once through required_spread(). _b_wide = derived(lambda: boundary(NO_WEATHER)) check("the two widening factors this repository ships DIFFER, or 'max', 'sum' " "and 'product' would be indistinguishable combination rules", len(set(WIDEN_FACTOR.values())) == len(WIDEN_FACTOR)) check(f"a widened threshold moves the boundary by exactly the declared factor: " f"{_b_wide!r} x {widen_factor(NO_WEATHER)} = {_b_plain!r}", near(_b_wide * widen_factor(NO_WEATHER), _b_plain, 1e-6)) check("...which is the same number required_spread() computes from the other " "direction, so the widening is one mechanism and not two", isinstance(_b_wide, float) and near(required_spread(_b_wide, NO_WEATHER), _b_plain, 1e-6)) _wide_at = round(SPREAD / widen_factor(NO_WEATHER) + 1e-4, 6) check("...and the difference is LIVE, not arithmetic on paper: at one " "threshold the plain site trades and the no-weather site does not", act(PLAN, CHEAP_H, thresh=_wide_at, unc=frozenset()).action == CHARGE and act(PLAN, CHEAP_H, thresh=_wide_at, unc=NO_WEATHER).action is None) # The threshold this add-on SHIPS, against the tariff most of the installed # base is on. Reported, not tuned - see the module docstring in app/arbitrage.py. STATIC_BE = ('{"periods": [' '{"name":"day","from":"07:00","to":"22:00","days":"all",' '"eur_per_kwh":0.34},' '{"name":"night","from":"22:00","to":"07:00","days":"all",' '"eur_per_kwh":0.26}]}') _st_plan = derived(lambda: static_plan({"static_profile": STATIC_BE}, D0, D0 + timedelta(days=2), now=D0)) _st_spread = derived(lambda: _st_plan.spread_eur_kwh()) check(f"a realistic Belgian day/night profile has a spread of " f"{_st_spread!r} EUR/kWh - the number the product decision is about", near(_st_spread, 0.08)) _shipped = PRICE_OPTION_DEFAULTS["cycle_cost_threshold_eur"] _st_verdict = derived(lambda: _st_plan.arbitrage(now=LONG_AGO, peak_guard_active=False, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER)) check(f"...and at the SHIPPED threshold of {_shipped} EUR/kWh that site does " f"NOT trade. Reported, not tuned: the shipped default is the owner's " f"decision and this check exists to make the consequence visible rather " f"than to argue with it", _st_verdict.kind == "refused") check(f"...and it is the SPREAD rung that refuses it, not a flat-tariff or a " f"provisional rung that would have refused any static site whatever the " f"threshold was: {_st_spread!r} < " f"{required_spread(_shipped, NO_WEATHER)!r}", _st_spread < required_spread(_shipped, NO_WEATHER) and "cover its own round-trip cost" in _st_verdict.reason) check("...while the SAME site trades once the threshold is set below its " "spread, so the refusal above is about the number and not about static " "tariffs being unsupported", derived(lambda: _st_plan.arbitrage(now=LONG_AGO, peak_guard_active=False, min_spread_eur_kwh=_st_spread / widen_factor(NO_WEATHER) - 1e-4, uncertainty=NO_WEATHER).kind) == "allowed") # --------------------------------------------------------------------------- # print() print("2. AC2 - §17: a negative-price hour never triggers export") # ⚠️ THE WHOLE DAY IS NEGATIVE AND THE DEAREST HOUR IS THE LEAST NEGATIVE ONE. # That is the case a guard written as "the spread is positive" or "not all # prices are negative" lets through, and it is the realistic one: Belgian # day-ahead has gone negative for a whole summer midday block. NEG = [round(-0.30 + 0.010 * i, 6) for i in range(24)] # -0.30 .. -0.07 NEG[DEAR_H] = -0.05 # dearest, still < 0 NEG_PLAN = plan_of(NEG) _neg_spread = round(max(NEG) - min(NEG), 6) check(f"the negative day's dearest hour is slot {DEAR_H} and it is BELOW ZERO " f"({max(NEG):+.4f}), which is the only shape that exercises the guard", NEG.index(max(NEG)) == DEAR_H and max(NEG) < 0) check(f"...and its spread is {_neg_spread:.4f} EUR/kWh, which CLEARS the " f"shipped threshold - so the trade is refused by §17 and not by a rung " f"that would have refused it anyway", _neg_spread > required_spread(_shipped, NO_WEATHER)) _neg_sell = act(NEG_PLAN, DEAR_H, thresh=_shipped, unc=NO_WEATHER) check("selling into a negative-price hour is REFUSED", _neg_sell.action is None) check("...and the reason names §17 and the price, so the log says which rule " "and not merely that nothing happened", "§17" in _neg_sell.reason and "-0.0500" in _neg_sell.reason) # The other side of the guard, without which nothing above is discriminating. POS = list(NEG) POS[DEAR_H] = +0.05 # the SAME day with ONE SIGN FLIPPED POS_PLAN = plan_of(POS) _pos_sell = act(POS_PLAN, DEAR_H, thresh=_shipped, unc=NO_WEATHER) check("...while the SAME series with that one price's SIGN FLIPPED does " "discharge - so the refusal is about the sign and about nothing else " "that differs between the two runs", _pos_sell.action == DISCHARGE) check("...and the two runs differ in exactly one price, by exactly its sign", sum(1 for a, b in zip(NEG, POS) if a != b) == 1 and near(NEG[DEAR_H], -POS[DEAR_H])) # §17 is about EXPORT. Buying in a negative hour is the right thing to do. check("BUYING in a negative-price hour is still permitted - §17 forbids " "exporting into one, and a guard that also blocked the buy would be " "refusing to be paid to charge", act(NEG_PLAN, NEG.index(min(NEG)), thresh=_shipped, unc=NO_WEATHER).action == CHARGE) # A zero price is the boundary and it is NOT negative. ⚠️ KEEP THIS PROBE. It # pins the boundary from ABOVE - it is what catches a guard written `<= 0` - and # the SAFETY-26 block below pins it from BELOW. Neither replaces the other, and # the whole defect that card exists to close is that only one of the two sides # was ever stated. ZERO = list(POS) ZERO[DEAR_H] = 0.0 check("a sell hour priced at exactly ZERO is not a negative one, so the guard " "is `< 0` rather than `<= 0` - and the boundary is stated by a check " "rather than left to whoever reads the operator", act(plan_of(ZERO), DEAR_H, thresh=_shipped, unc=NO_WEATHER).action == DISCHARGE) # --------------------------------------------------------------------------- # # SAFETY-26. THE OTHER SIDE OF THE SAME BOUNDARY, AND WHY IT HAD TO BE ADDED. # # OPT-04's reviewer mutated `if price < 0` to `if price < -0.001` and EVERY # SUITE STAYED GREEN, with a -0.0005 EUR/kWh sell hour discharging. The shipped # code was right the whole time; the CHECKS could not tell correct from # nearly-correct, because the only probe near the boundary sat at 0.0000. A # probe above the line catches `<=`. It says nothing at all about a tolerance # invented below the line, and a tolerance below the line IS §17's regression # case walking through with the suite passing. # # ⚠️ AND -0.0005 IS NOT AN EXOTIC INPUT. Belgian day-ahead prints small negative # hours regularly; an all-in price a twentieth of a cent below zero is an # ordinary day, not a fuzz value. That is what makes this a live regression # rather than a theoretical one. # # ⚠️ SO THE PROBE IS A SWEEP, NOT A POINT, and the sweep is the discriminating # part. One probe at -0.0005 closes `-0.001` and nothing else; a mutation # reaching for `-0.01` would survive it, which is the identical defect one # decimal place further out. The sweep spans four orders of magnitude down to # the last place the canonical six-decimal format can even express, so there is # no tolerance a guard can invent that some member of it does not straddle. # `_ROUNDING` asserts the smallest member SURVIVES the format rather than being # rounded to zero on the way in - a probe that silently became 0.0 would be the # `0.0000` check again, wearing a different name. TINY_NEG = (-0.05, -0.005, -0.0005, -0.00005, -0.000001) _ROUNDING = [round(p, 6) for p in TINY_NEG] check(f"every probe price survives the canonical 6-decimal rounding as a " f"STRICTLY negative number {_ROUNDING} - one that rounded to 0.0 would " f"be the check above again under another name", all(r < 0 for r in _ROUNDING) and len(set(_ROUNDING)) == len(TINY_NEG)) check("...and they span four orders of magnitude, so no single invented " "tolerance can sit below all of them and above none", abs(min(TINY_NEG)) / abs(max(TINY_NEG)) > 10000) def tiny_neg_plan(price): """The POS day with its DEAREST hour set to a small negative print.""" day = list(POS) day[DEAR_H] = price return plan_of(day), day # The stimulus has to survive the rungs ABOVE §17 or it grades nothing: the # hour must still be the dearest, and the spread must still clear the widened # threshold, at every probe. Asserted first, for all of them, as its own check. _wellformed = [ (p, day.index(max(day)) == DEAR_H, round(max(day) - min(day), 6) > required_spread(_shipped, NO_WEATHER)) for p, (_pl, day) in ((p, tiny_neg_plan(p)) for p in TINY_NEG)] check(f"at every probe the negative hour is still the DEAREST one and the " f"spread still clears the threshold, so §17 is the only rung that can " f"refuse {_wellformed}", all(dearest and clears for _p, dearest, clears in _wellformed)) _swept = [(p, act(tiny_neg_plan(p)[0], DEAR_H, thresh=_shipped, unc=NO_WEATHER).action) for p in TINY_NEG] check(f"SAFETY-26: a sell hour that is negative BY ANY MARGIN AT ALL refuses - " f"down to -0.000001 EUR/kWh, so the guard is `< 0` and not `< -epsilon` " f"for any epsilon {_swept}", all(a is None for _p, a in _swept)) check("...and each refusal is §17's, not some other rung that happens to fire " "at small numbers", all("§17" in act(tiny_neg_plan(p)[0], DEAR_H, thresh=_shipped, unc=NO_WEATHER).reason for p in TINY_NEG)) # The mirror image, one place above zero. Without this the sweep above would # pass for a guard that refused EVERYTHING, which grades §17 by nothing. _tiny_pos = [(-p, act(tiny_neg_plan(-p)[0], DEAR_H, thresh=_shipped, unc=NO_WEATHER).action) for p in TINY_NEG] check(f"...while THE SAME MAGNITUDES with the sign flipped all DISCHARGE, so " f"the sweep is a boundary at exactly zero rather than a refusal of small " f"numbers - which a guard that refused everything would also have passed " f"{_tiny_pos}", all(a == DISCHARGE for _p, a in _tiny_pos)) # SAFETY-26 AC4 - THE LOOK AT THE OTHER BOUNDARIES, NAMED RATHER THAN FIXED. # # The card asks whether the same one-sided pinning exists elsewhere. Every # price/quantity boundary in arbitrage.py and static_tariff.py was given the # tolerance mutation that survived on `price < 0`, in both directions where # both exist, under `-B` with a recursive purge. Two survived, BOTH in # static_tariff.py - TARIFF-03's file, and deliberately NOT touched here: # # D `spread == 0.0` (the flat-tariff rung) -> `spread <= 0.001` SURVIVES. # Measured, not reasoned: with it loosened, a series priced 0.20000 / # 0.20050 - a REAL 0.0005 spread - flips from ALLOWED to REFUSED at # min_spread_eur_kwh=0.0, which `arbitrage()`'s own docstring documents as # a supported value ("pass 0.0 to mean any positive spread, explicitly"). # At a shipped threshold the OUTCOME is unchanged, but the operator's # reason becomes the false sentence "the tariff is flat - every one of the # 2 firm hours costs 0.2000 EUR/kWh" about a tariff that is not flat. # Same shape as §17's: the rung is only ever probed with an EXACTLY flat # series, so nothing pins the tiny-but-real side. # # F `len(firm) < 2` (§8.3's firm-pair rung) -> `< 1` SURVIVES, at +0 BYTES, # which is the delta TEST-15 names as the risky one. It is not live on its # own: a single firm hour has a spread of 0.0, so the flat rung refuses it # anyway - but the reason degrades from §8.3's honest "only 1 of 3 hours # carry a published price" to "the tariff is flat". THE FINDING THAT # MATTERS IS THE INTERACTION: D and F MASK EACH OTHER. Neither is # individually live because the other catches it, which is exactly the # arrangement in which a later edit to either one opens a hole nothing # reports. # # Everything else in arbitrage.py is pinned on BOTH sides and was verified so: # `price < 0` (this card), `soc <= floor_pct` and `soc >= ceiling_pct` are each # red under a +5 and a -5 tolerance; static_tariff's `spread < need` is red # under +/-0.001 and additionally bisected to 1e-6 in section 1; `cheap_now`'s # `price < mean` is red under a +0.001 tolerance in test_static_tariff.py. # # Not fixed here on purpose. D and F are TARIFF-03's rungs, they need probes in # test_static_tariff.py rather than in this file, and the card is explicit that # its fourth criterion is a look and not a refactor. # # ✅ CLOSED BY TEST-16, in test_static_tariff.py SECTION 9, and the two rows # above are kept as the diagnosis rather than as an open finding. D is pinned by # a sweep of small-but-real spreads down to 0.000001 EUR/kWh at # min_spread_eur_kwh=0.0; F is pinned by a ONE-FIRM-HOUR series graded on WHICH # RUNG'S SENTENCE came back, which is the only way it goes red without borrowing # the flat rung; and the masking is denied by a table that grades every stimulus # on its rung's attribution rather than on `kind`. Both are red on their own and # five checks are red with BOTH weakened at once. Ledger in that section's # header. Nothing in app/ changed - these rungs were correct all along. # --------------------------------------------------------------------------- # print() print("2c. TARIFF-18 - where the \u00a717 boundary sits in SPOT terms, and the " "shipped default that decides it") # ⚠️ WHAT THIS GRADES AND WHAT IT DOES NOT CLAIM. `price_taxes_eur_kwh` is a # LEVEL component: it is added identically to every hour, so it cancels exactly # in `max - min` and cannot move which hour is cheapest. test_price.py 16b # measures that as a FAMILY - five flat scalars spanning and exceeding a real # per-DSO range, identical argmin and identical spread to the last digit - and # nothing here re-litigates it. Raising the shipped default 0.06 -> 0.11736 is # therefore NOT an arbitrage-correctness fix, and this section does not say it # is. # # What it IS: §17's guard is the one rung in this module that reads an # ABSOLUTE price rather than a difference, so it is the one decision a level # component can reach. The boundary is `spot < -(markup + taxes)`, and moving # the taxes default moves it. Below, driven through the shipped `decide()`. # # ⚠️ AND THE TWO ERROR DIRECTIONS ARE NOT SYMMETRIC, WHICH IS WHY THE DEFAULT # SITS AT THE BOTTOM OF THE PER-DSO BAND RATHER THAN IN THE MIDDLE. A taxes # figure set too HIGH carries a genuinely negative hour up above zero and the # add-on exports into paid-for disposal - §17's own regression case. A figure # set too LOW only declines a discharge that would have paid. Where one default # must stand in for eight Fluvius DSOs, it belongs at the low end. _T18_MARKUP = PRICE_OPTION_DEFAULTS["price_markup_eur_kwh"] _T18_TAXES = PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"] _T18_OLD_TAXES = 0.06 # what config.yaml shipped before TARIFF-18 # ⚠️ THE PRECONDITION FIRST (MUTATION.md habit 1). Every comparison below is # "the old level and the new level answer differently". If the two levels were # ever the same number, all of it would be one figure graded against itself - # DEV-02's `(-5000, 5000)` against `(-5000, 5000)`, one card over. check(f"the shipped taxes default {_T18_TAXES} and the superseded " f"{_T18_OLD_TAXES} are DIFFERENT numbers, and differ by more than the " f"0.01 EUR/kWh step this fixture's spot series is built on", abs(_T18_TAXES - _T18_OLD_TAXES) > 0.01) # The §17 boundary, computed HERE from the two option values rather than read # off arbitrage.py - habit 2. all-in = spot + markup + taxes, so the guard # fires exactly when spot < -(markup + taxes). _T18_B_NEW = round(-(_T18_MARKUP + _T18_TAXES), 6) _T18_B_OLD = round(-(_T18_MARKUP + _T18_OLD_TAXES), 6) _T18_PROBE = round((_T18_B_NEW + _T18_B_OLD) / 2, 6) check(f"the guard's spot-price boundary moves {_T18_B_OLD:+.5f} -> " f"{_T18_B_NEW:+.5f} EUR/kWh, and the probe {_T18_PROBE:+.5f} lies " f"STRICTLY BETWEEN them - so one level answers each way and the probe is " f"not on either boundary", _T18_B_NEW < _T18_PROBE < _T18_B_OLD) # A whole-day-negative spot series whose DEAREST hour is the probe. Same shape # as section 2's: the case a guard written "not all prices are negative" lets # through, in SPOT rather than all-in terms. _T18_SPOT = [round(-0.40 + 0.010 * i, 6) for i in range(24)] _T18_SPOT[DEAR_H] = _T18_PROBE check(f"the spot day's dearest hour is slot {DEAR_H} at {_T18_PROBE:+.5f} and " f"every other hour is below it, so the hour the guard is asked about is " f"the one the probe was chosen for", _T18_SPOT.index(max(_T18_SPOT)) == DEAR_H and _T18_SPOT.count(max(_T18_SPOT)) == 1 and all(p < 0 for p in _T18_SPOT)) def _t18_all_in(taxes): """spot + markup + taxes, added here, by this file's own arithmetic.""" return [round(p + _T18_MARKUP + taxes, 6) for p in _T18_SPOT] _T18_OLD_SERIES = _t18_all_in(_T18_OLD_TAXES) _T18_NEW_SERIES = _t18_all_in(_T18_TAXES) print(f" dearest hour all-in: old level {_T18_OLD_SERIES[DEAR_H]:+.5f}, " f"shipped level {_T18_NEW_SERIES[DEAR_H]:+.5f} EUR/kWh") # ⚠️ THE LEVEL BUCKET RESTATED ON THIS FIXTURE, BEFORE ANYTHING IS CONCLUDED # FROM IT. If the two arms differed in spread or in argmin, the verdicts below # could be attributed to that instead, and this section would be claiming an # arbitrage effect it must not claim. check(f"the two levels leave the SPREAD identical to the last digit " f"({round(max(_T18_OLD_SERIES) - min(_T18_OLD_SERIES), 6)} EUR/kWh) and " f"the cheapest hour in the same slot - so nothing below is an optimiser " f"effect", round(max(_T18_OLD_SERIES) - min(_T18_OLD_SERIES), 6) == round(max(_T18_NEW_SERIES) - min(_T18_NEW_SERIES), 6) and _T18_OLD_SERIES.index(min(_T18_OLD_SERIES)) == _T18_NEW_SERIES.index(min(_T18_NEW_SERIES))) _T18_SELL_OLD = act(plan_of(_T18_OLD_SERIES), DEAR_H, thresh=_shipped, unc=NO_WEATHER) _T18_SELL_NEW = act(plan_of(_T18_NEW_SERIES), DEAR_H, thresh=_shipped, unc=NO_WEATHER) check(f"at the SUPERSEDED 0.06 the same spot hour is " f"{_T18_OLD_SERIES[DEAR_H]:+.5f} all-in, so \u00a717 REFUSES the discharge " f"- and the reason names \u00a717 and that price, not merely that nothing " f"happened", _T18_SELL_OLD.action is None and "§17" in _T18_SELL_OLD.reason and f"{_T18_OLD_SERIES[DEAR_H]:+.4f}" in _T18_SELL_OLD.reason) check(f"...and at the SHIPPED {_T18_TAXES} the SAME spot hour is " f"{_T18_NEW_SERIES[DEAR_H]:+.5f} all-in and the battery DISCHARGES. That " f"is the whole of what this card changes in a decision: the operator " f"really does pay those levies, so this hour was never paid-for disposal " f"and the old default was declining a trade that paid", _T18_SELL_NEW.action == DISCHARGE) # ⚠️ AND THE OTHER SIDE OF THE SHIPPED BOUNDARY, or the pair above grades # "bigger taxes discharge more" rather than "the boundary is at -(markup + # taxes)". One step below the shipped boundary is still refused. _T18_BELOW = list(_T18_SPOT) _T18_BELOW[DEAR_H] = round(_T18_B_NEW - 0.001, 6) _T18_SELL_BELOW = act( plan_of([round(p + _T18_MARKUP + _T18_TAXES, 6) for p in _T18_BELOW]), DEAR_H, thresh=_shipped, unc=NO_WEATHER) check(f"a spot hour at {_T18_BELOW[DEAR_H]:+.5f} - one tenth of a cent BELOW " f"the shipped boundary - is still refused by \u00a717, so the boundary is " f"where -(markup + taxes) puts it and not merely somewhere lower than " f"before", _T18_SELL_BELOW.action is None and "§17" in _T18_SELL_BELOW.reason) # --------------------------------------------------------------------------- # print() print("3. AC3 - peak shaving outranks arbitrage, through both mechanisms") # MECHANISM 1: static_tariff's interlock. The spread is not obtainable without # answering the question, so this cannot be skipped by a caller. _clear = act(PLAN, CHEAP_H, thresh=_shipped, unc=NO_WEATHER, peak=False) _held = act(PLAN, CHEAP_H, thresh=_shipped, unc=NO_WEATHER, peak=True) check("on ONE plan, at ONE instant, with ONE threshold, the peak guard being " "CLEAR trades and the peak guard HOLDING does not - the only difference " "between the two runs is that boolean", _clear.action == CHARGE and _held.action is None) check("...and the refusal cites §10.1's ordering and the capaciteitstarief, so " "it cannot be mistaken for 'no opportunity today'", "§10.1" in _held.reason and "peak" in _held.reason.lower()) check("...and the interlock is REQUIRED rather than defaulted: " "TariffPlan.arbitrage() has no default for peak_guard_active, so a " "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")) 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())) # 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 # every limit at or above the winner's priority. check("the priority ordering IS §10.1's, derived from the constants rather " "than asserted about them: safety > maintenance > peak > maintenance " "shaping > arbitrage > loop", P_SAFETY > P_MAINTENANCE > P_PEAK > P_MAINT_SHAPE > P_ARBITRAGE > P_LOOP) _charge_claim = claim(_clear) PEAK_FLOOR = 500.0 check(f"the peak floor used below ({PEAK_FLOOR:+.0f} W) is strictly INSIDE the " f"arbitrage charge ({_charge_claim.value:+.0f} W), or clause 2 would " f"clamp nothing and the resolution would pass for an unbound claim", _charge_claim.value < PEAK_FLOOR) _res = resolve([ Claim.limit("supervised", P_SAFETY, -MAX_W, MAX_W, "max_w"), Claim.limit(_pg.SOURCE, P_PEAK, lo=PEAK_FLOOR, reason="quarter peak"), _charge_claim, Claim.set("loop", P_LOOP, -120.0, "following"), ]) check(f"an arbitrage charge that WINS the arbitration is still clamped by the " f"peak floor: {_res.explain()}", near(_res.target_w, PEAK_FLOOR) and _res.winner == _arb.SOURCE and _res.bound_by is not None and _pg.SOURCE in _res.bound_by) check("...and it is P_PEAK > P_ARBITRAGE that does it, not luck: the identical " "floor filed BELOW the arbitrage priority does not bind at all", near(resolve([ Claim.limit("supervised", P_SAFETY, -MAX_W, MAX_W, "max_w"), Claim.limit(_pg.SOURCE, P_ARBITRAGE - 1, lo=PEAK_FLOOR), _charge_claim]).target_w, _charge_claim.value)) # The floor really is peakguard's own claim, not a hand-rolled stand-in. _pd = _pg.PeakDecision(engaged=True, floor_w=PEAK_FLOOR, ceiling_w=3500.0, ceiling_from=_pg.CEILING_CAP, reason="test") _real_peak = derived(lambda: _pg.claim(_pd)) check("...and the claim doing the clamping is peakguard.claim()'s own output, " "so this grades the shipped floor rather than a transcription of it", _real_peak is not None and _real_peak.priority == P_PEAK and near(resolve([ Claim.limit("supervised", P_SAFETY, -MAX_W, MAX_W, "max_w"), _real_peak, _charge_claim]).target_w, PEAK_FLOOR)) # ...and a DISCHARGE is deliberately not capped by it (§9.1: only offtake bills). check("a peak floor does NOT cap an arbitrage DISCHARGE - §9.1 bills offtake " "only, and peakguard's claim leaves `hi` at +inf to say so", near(resolve([ Claim.limit("supervised", P_SAFETY, -MAX_W, MAX_W, "max_w"), _real_peak, claim(_sell)]).target_w, MAX_W)) # MECHANISM 3, which is neither of the above: the guard that never ran. _blind = decide(PLAN, hour(CHEAP_H), peak_guard_active=None, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0) check("a peak guard that NEVER RAN is a third state, and it refuses: `None` is " "not `False`, and 'nothing measured the quarter' is not 'the quarter is " "clear'", _blind.action is None and "NOT COVERED" in _blind.reason) check("...and it is distinguishable from the guard HOLDING, so the status page " "does not report an unmeasured install as a peak event", _blind.reason != _held.reason) # ⚠️ AND THE THREE STATES ARE GRADED ON THE SHIPPED EXPRESSION. `guard_state()` # is what main.py's cycle() passes; a transcription of it here would be the # defect `shaping_suspended()` was extracted to remove - a mutation back to a # bare `engaged_now` left every suite green in the case that record describes. _idle_pd = _pg.PeakDecision(engaged=False, floor_w=None, ceiling_w=3500.0, ceiling_from=_pg.CEILING_CAP, reason="idle") check("guard_state() maps peakguard's three real situations onto the three " "states decide() takes, and None is NOT False", _arb.guard_state(None) is None and _arb.guard_state(_idle_pd) is False and _arb.guard_state(_pd) is True) check("...and cycle() calls it rather than inlining the conjunction, so a " "mutation back to a bare `engaged_now` is red rather than silent", "arbitrage.guard_state(self.peak_guard)" in ast.get_source_segment( open(os.path.join(HERE, "app", "main.py"), encoding="utf-8").read(), _fn(open(os.path.join(HERE, "app", "main.py"), encoding="utf-8").read(), "cycle"))) # --------------------------------------------------------------------------- # print() print("3b. SAFETY-45 - the buy leg is SIZED by the guard, not switched by it") # --------------------------------------------------------------------------- # AC3 above is the SWITCH: `peak_guard_active` refuses the whole trade while # the guard holds. That is right and it stays. What was missing is the other # half - the guard measures a CONTINUOUS number, "the watts this billed quarter # can still afford", and until this card nothing consumed it. So a trade sat at # max_w through the whole window in which the guard was silent, blew the # quarter, tripped the switch, was cancelled outright, and came back at max_w # the moment the meter fell far enough to release it. Measured on TEST-23 # stage 3's closed rig at peak_cap_w 1500 / max_w 2000 / a 1200 W house: # 28 charge onsets in one hour, mean period 131 s, grid -456 <-> +3200 W. # # `control.maintenance_charge_floor()` has always given a MAINTENANCE charge # exactly this shape - "capped by the headroom left under the site's cap" - and # its own docstring says why: "a maintenance charge is the only thing this # system does that is big enough and long enough to set that peak". OPT-04 is # the second such thing and was given no cap. This section grades the cap. # --------------------------------------------------------------------------- # ⚠️ THE PRECONDITION, AND IT IS THE HABIT-1 ONE. The headroom must be strictly # between 0 and the size the trade would otherwise ask for, or "sized by the # guard" is indistinguishable from "not sized at all" and from "refused". S45_HEADROOM = 300.0 check(f"PRECONDITION: the headroom under test ({S45_HEADROOM:.0f} W) is " f"strictly inside the trade's own sizing ({MAX_W:.0f} W) and strictly " f"positive, so a cap that did nothing and a cap that refused everything " f"are both distinguishable from the answer below", 0.0 < S45_HEADROOM < MAX_W) _s45_capped = act(PLAN, CHEAP_H, thresh=_shipped, unc=NO_WEATHER, headroom=S45_HEADROOM) _s45_clear = act(PLAN, CHEAP_H, thresh=_shipped, unc=NO_WEATHER) print(f" headroom {S45_HEADROOM:.0f} W -> {_s45_capped.power_w:+.0f} W; " f"headroom clear -> {_s45_clear.power_w:+.0f} W") print(f" {_s45_capped.reason}") check("the trade still HAPPENS - §10.1 ranks peak shaving above arbitrage, it " "does not abolish it - and it is sized to the watts the billed quarter " "can afford rather than to max_w", _s45_capped.action == CHARGE and near(_s45_capped.power_w, -S45_HEADROOM)) check("...and the claim the arbiter receives carries the SIZED number, so the " "cap reaches the actuator and is not merely printed", near(claim(_s45_capped).value, -S45_HEADROOM) and claim(_s45_capped).priority == P_ARBITRAGE) check("...and the SAME call with the headroom clear asks for max_w, so the " "check above is graded against a difference this fixture can actually " "produce (habit 7: one thing varied, from an accepted baseline)", _s45_clear.action == CHARGE and near(_s45_clear.power_w, -MAX_W)) # ⚠️ THE ORDERED PAIR (MUTATION.md habit 14). Two numbers in one sentence can # always be transposed by a ZERO-BYTE edit, and this sentence is what an # operator reads when the battery buys less than the price says it should. So # the check asserts each value inside the literal text that names it, and # asserts that the TRANSPOSED sentence is not what it says. _S45_PAIR = (f"the trade asked for {MAX_W:.0f} W and this billed quarter can " f"afford {S45_HEADROOM:.0f} W") _S45_SWAP = (f"the trade asked for {S45_HEADROOM:.0f} W and this billed " f"quarter can afford {MAX_W:.0f} W") check("the reason names WHAT WAS ASKED FOR and WHAT WAS AFFORDABLE in their " "own slots, so an operator can tell a small trade from a small " "opportunity", _S45_PAIR in _s45_capped.reason) check("...and the transposed sentence is NOT what it says, so the check above " "discriminates rather than decorates - both numbers are in the string " "either way and a substring test would pass under the swap", _S45_SWAP not in _s45_capped.reason) check("...and it names the guard and the two spec sections, so the sentence " "cannot be read as a pack limit or a price limit", "CAPACITY-TARIFF GUARD" in _s45_capped.reason and "§9.1" in _s45_capped.reason and "§10.1" in _s45_capped.reason) check("...and an UNCAPPED trade does not carry that clause at all: a sentence " "that always says 'sized by the guard' tells nobody anything", "CAPACITY-TARIFF GUARD" not in _s45_clear.reason and f"charging {MAX_W:.0f} W" in _s45_clear.reason) # --- the two refusals, and they are told apart from the ones that existed --- _s45_none = act(PLAN, CHEAP_H, thresh=_shipped, unc=NO_WEATHER, headroom=None) _s45_zero = act(PLAN, CHEAP_H, thresh=_shipped, unc=NO_WEATHER, headroom=0.0) print(f" headroom None -> {_s45_none.action}; headroom 0 W -> " f"{_s45_zero.action}") check("a headroom nothing MEASURED refuses. This is guard_state()'s None " "argument one layer down: charging creates the billed quantity (§9.1) " "and an unmeasured quarter is not a clear one", not _s45_none.trading and _s45_none.reason == _arb.IDLE_HEADROOM_UNMEASURED) check("...and it is DISTINGUISHABLE from the guard never having run at all, so " "the status page does not report a missing battery sensor as a missing " "meter", _s45_none.reason != _arb.IDLE_NOT_COVERED) check("a headroom of ZERO refuses rather than filing a 0 W claim - " "`Claim.set(..., 0.0)` at P_ARBITRAGE is a PARK ORDER that outranks the " "loop, which is the failure claim()'s own note is written against", not _s45_zero.trading and claim(_s45_zero) is None and _s45_zero.reason == _arb.IDLE_NO_HEADROOM) # ⚠️ TEST-16's RULE: TWO RUNGS THAT CAN ANSWER THE SAME STIMULUS MUST BE # SEPARATED. `charge_w = 0` and `headroom = 0` both produce a 0 W sizing, and # grading only `kind == "refused"` would let either absorb the other's work - # after which weakening either one reports nothing. So the REASONS are graded, # and each rung is reached with the other one clear. _s45_nosize = decide(PLAN, hour(CHEAP_H), peak_guard_active=False, soc=50.0, charge_headroom_w=HEADROOM_CLEAR, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_w=0.0, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0) check("...and the PACK being sized at 0 W still reports OPT-09's own sentence, " "not this card's: the two rungs answer the same stimulus and a refusal " "that named the wrong subsystem would send an operator to the wrong page", not _s45_nosize.trading and _s45_nosize.reason == _arb.IDLE_NO_SIZE and _s45_zero.reason != _s45_nosize.reason) # --- §9.1: the SELL leg is untouched --------------------------------------- _s45_sell = act(PLAN, DEAR_H, thresh=_shipped, unc=NO_WEATHER, headroom=0.0) check("a ZERO headroom does not cap a DISCHARGE. §9.1: 'discharge and export " "are never capped by the peak guard; only charging and import are' - the " "capacity tariff bills offtake, and a sell creates none", _s45_sell.action == DISCHARGE and near(_s45_sell.power_w, MAX_W)) # --- and main.py actually passes it, pinned on the AST ---------------------- # ⚠️ THE CALL SITE IS THE UNIT OF COVERAGE (MUTATION.md habit 15). Nothing # behavioural in this file can see main.py drop the argument - every check # above calls decide() itself - and a required keyword-only parameter that no # caller fills is a TypeError only on the one path a suite does not run. Same # pin, same reason, as `weights=` in test_packshare.py part 10. _S45_MAIN = open(os.path.join(HERE, "app", "main.py"), encoding="utf-8").read() _S45_MAIN_TREE = ast.parse(_S45_MAIN) _s45_calls = [n for n in ast.walk(_S45_MAIN_TREE) if isinstance(n, ast.Call) and ast.unparse(n.func).endswith("arbitrage.decide")] _s45_arg = [ast.unparse(k.value) for c in _s45_calls for k in c.keywords if k.arg == "charge_headroom_w"] def _s45_assigned(name, clear=("None",)): """Every expression main.py assigns to `self.` that is not a CLEAR. ⚠️ SAFETY-47 HOISTED THE HEADROOM OUT OF THE CALL, so pinning the call argument alone stopped covering anything: the literal a mutation would put there now goes one line up instead. Both ends are pinned. ⚠️ `clear` NAMES THE CONSTANTS THAT MEAN "NOTHING HAS ANSWERED THIS YET" - `__init__`'s value and the per-cycle reset - and they are not a second source for the number. Excluding them does NOT soften the pin: a mutation that replaces the real assignment with one of those constants leaves this list EMPTY, which matches no expected value and is red. """ return [ast.unparse(n.value) for n in ast.walk(_S45_MAIN_TREE) if isinstance(n, ast.Assign) and [ast.unparse(t) for t in n.targets] == [f"self.{name}"] and ast.unparse(n.value) not in clear] _s45_head_src = _s45_assigned("arb_headroom_w") print(f" main.py has {len(_s45_calls)} `arbitrage.decide(` call site(s); " f"charge_headroom_w= is {_s45_arg}, and self.arb_headroom_w is assigned " f"from {_s45_head_src}") check("main.py passes the guard's OWN measured headroom at its one " "arbitrage.decide() call site - the shipped expression, not a " "transcription of it, so a mutation back to a literal or to max_w is red " "rather than silent", _s45_arg == ["self.arb_headroom_w"] and _s45_head_src == ["peakguard.charge_headroom_w(self.peak_guard)"]) # ⚠️ AND THE HOIST DID NOT MOVE IT OUT OF THE CYCLE. An `arb_headroom_w` set # once in `__init__` and never refreshed would satisfy the pin above and would # size every trade from the guard decision of whatever cycle happened to run # last - which is the defect the hoist's own comment warns about, one level up. _s45_cycle = first((n for n in ast.walk(_S45_MAIN_TREE) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == "cycle" and any(isinstance(c, ast.Call) and ast.unparse(c.func).endswith("arbitrage.decide") for c in ast.walk(n))), "no cycle() calling arbitrage.decide") check("...and the assignment is INSIDE the same `cycle()` that makes the call, " "so the headroom the trade is sized from is this cycle's guard decision " "and not one carried from an arbitrary earlier one", any(isinstance(n, ast.Assign) and [ast.unparse(t) for t in n.targets] == ["self.arb_headroom_w"] and ast.unparse(n.value) == "peakguard.charge_headroom_w(self.peak_guard)" for n in ast.walk(_s45_cycle))) # --------------------------------------------------------------------------- # print() print("3c. SAFETY-45 - THE REGRESSION: the closed loop SETTLES") # --------------------------------------------------------------------------- # ⚠️ THIS IS THE CHECK THE CARD IS ACTUALLY ABOUT, AND A UNIT TEST ON decide() # IS NOT IT. The defect is an INTERACTION - two strategies, a 900 s integrator # and a plant that answers back - and every individual decision in it was # correct. So the loop is closed here: prices decide, the battery model # responds, the shipped QuarterAverager reports the consequence, and the # shipped guard reacts to that. # # WHAT IS REAL, AND WHAT IS MODELLED - the same discipline TEST-23 stage 3's # harness holds itself to, because a transcription proves what somebody typed: # # REAL peakguard.decide/claim/charge_headroom_w, arbitrage.decide/claim/ # guard_state, control.compute + Tuning, arbiter.resolve, and # p1.QuarterAverager - the billed-quarter accumulator itself, so the # quarters reported below are the ones a meter would bill. # MODEL ONE cycle's ORDER (peak block, then arbitrage reading its # conclusion, then the loop claim, then resolve) - twelve lines # transcribed from main.py:2660-2900 and pinned above in 3b; the # PLANT, an 800 W/s ramp into a 10 s first-order lag, which is # plantsim's; and a flat synthetic house. # # ⚠️ AND THE HEADLINE NUMBERS WERE NOT TAKEN HERE. They were taken on the # closed rig at /home/homelab/test23s3 against the shipped container's own # checkout, which is where a divergence between this model and main.py's real # event loop would show up. This section exists so CI can go red. # --------------------------------------------------------------------------- S45_CAP_W, S45_LOAD_W, S45_CYCLE_S, S45_RUN_S = 1500.0, 1200.0, 5.0, 2700.0 S45_RAMP_W_S, S45_LAG_S = 800.0, 10.0 # ⚠️ THE RUN STARTS ON A QUARTER BOUNDARY AND ENDS INSIDE THE SAME CHEAP HOUR, # and both halves matter. Starting on the boundary makes the three closed # blocks below whole billed quarters. Ending inside the hour keeps TEST-23 # finding 1 - the export overshoot at the END of any trade, ~0.37 x max_w, a # separate and already-recorded property - out of a swing figure this card is # grading. The run still crosses TWO quarter boundaries, which is the half of # the defect the card says arbitrage must learn from. S45_T0 = D0 + timedelta(hours=CHEAP_H) def s45_loop(*, sized: bool, cap_w=S45_CAP_W, load_w=S45_LOAD_W, seconds=S45_RUN_S, shadow=False): """One closed hour. `sized=False` is the behaviour this card removes. ⚠️ THE COUNTERFACTUAL IS ONE ARGUMENT, NOT A SECOND CODE PATH. `sized=False` hands `decide()` a headroom nothing can bind - which is exactly what the tree did before this card, because nothing was passing one - so the two runs below differ in one value and share every line of shipped code. Habit 7. ⚠️ SAFETY-47's `shadow=True` IS THE SAME DISCIPLINE, ONE ARGUMENT AGAIN. It reproduces the THREE things `Controller._write_unit()` and `_shadow_settle()` do and nothing else: the resolved command is RECORDED, the plant is driven from `shadow.WIRE_W` instead of from it, and the carried command is RE-SEEDED to `shadow.WIRE_W` after the record is taken. Those three lines are a TRANSCRIPTION of main.py's placement and are marked as one - `test_clamp.py` part 8 is the check on the shipped placement, and it drives the real `Controller`. What is NOT transcribed is every number that matters here: the averager, the projection, the guard, the headroom, the strategy, the arbiter and the row's own words are all the shipped ones. """ avg = QuarterAverager(phases=1, max_hold_s=900.0) tuning = Tuning(gain=0.6, max_w=MAX_W, slew_w=0.0) t0 = S45_T0 target = i_w = 0.0 sat = 0 loop_w = None batt_cmd = batt = 0.0 # + discharge, as main.py's site_batt_w onsets = blocks = 0 rec = [] prev_action = "" grid_lo, grid_hi = None, None billed = [] acc = 0.0 t = 0.0 while t <= seconds: grid = load_w - batt imp, exp = split_signed(grid) for b in avg.add(make_sample("ha_signed", imp, exp, phases=1, ingest_ts=t0 + timedelta(seconds=t), ingest_mono=(t0.timestamp() + t))): billed.append(round(float(b.offtake_avg_w), 1)) blocks += 1 if acc <= 1e-9: acc = S45_CYCLE_S now = t0 + timedelta(seconds=t) claims = [] pk = _pg.decide(PeakTracker.project(avg, now), batt_w=batt, cap_w=cap_w, margin_w=0.0, reading=None, achievable_w=MAX_W, soc=50.0, soc_floor=20.0, max_hold_s=900.0, frozen=False) pc = _pg.claim(pk) if pc is not None: claims.append(pc) arb = decide(PLAN, now, peak_guard_active=_arb.guard_state(pk), charge_headroom_w=(_pg.charge_headroom_w(pk) if sized else HEADROOM_CLEAR), soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0) ac = claim(arb) if ac is not None: claims.append(ac) if arb.action == CHARGE and prev_action != CHARGE: onsets += 1 prev_action = arb.action if loop_w is None or target != loop_w: i_w = target d = compute(prev_w=target, grid_w=grid, actual_w=batt, tuning=tuning, sat_count=sat, i_w=i_w) sat, i_w, loop_w = d.sat_count, d.i_w, d.target_w claims.append(Claim.limit("supervised", P_SAFETY, -MAX_W, MAX_W, "max_w")) claims.append(Claim.set("loop", P_LOOP, d.target_w, d.reason)) res = resolve(claims) target = res.target_w head_w = _pg.charge_headroom_w(pk) if sized else HEADROOM_CLEAR row = dict( t_s=t, action=arb.action, power_w=arb.power_w, reason=arb.reason, price=arb.price_now_eur_kwh, spread=arb.spread_eur_kwh, winner=res.winner, target_w=float(res.target_w), headroom_w=head_w, engaged=bool(pk.engaged), sized_from_meter=_arb.sized_from_meter( arb, head_w, winner=res.winner)) rec.append(row) if shadow: # --- TRANSCRIBED 1/3: `_shadow_settle()`'s record, taken # BEFORE the re-seed destroys its evidence. row["would_w"] = float(res.target_w) row["row"] = _shadow.words( {"shadow_mode": "on"}, control_started=False, would_w=float(res.target_w), sized_from_meter=row["sized_from_meter"], reason=res.explain()) # --- TRANSCRIBED 2/3: `_write_unit()`'s substitution - the # plant below is driven from WIRE_W, not from the decision. # --- TRANSCRIBED 3/3: `_shadow_settle()`'s re-seed of the # carried command, which is what `compute(prev_w=...)`, # `standing_w()` and SAFETY-42 read next cycle. target = _shadow.WIRE_W # THE PLANT, MODELLED: an 800 W/s ramp into a 10 s first-order lag. step = min(1.0, acc) delta = target - batt_cmd batt_cmd += math.copysign(min(abs(delta), S45_RAMP_W_S * step), delta) batt += (batt_cmd - batt) * (step / S45_LAG_S) acc -= step t += step grid_lo = grid if grid_lo is None else min(grid_lo, grid) grid_hi = grid if grid_hi is None else max(grid_hi, grid) return dict(onsets=onsets, grid_lo=round(grid_lo, 1), grid_hi=round(grid_hi, 1), swing=round(grid_hi - grid_lo, 1), billed=billed, blocks=blocks, rec=rec, engaged=sum(1 for r in rec if r["engaged"]), cycles=len(rec)) S45_BEFORE = derived(lambda: s45_loop(sized=False)) S45_AFTER = derived(lambda: s45_loop(sized=True)) for _lab, _r in (("unsized (the shipped behaviour before this card)", S45_BEFORE), ("sized by the guard", S45_AFTER)): print(f" {_lab}: {_r['onsets']} charge onsets/h, grid " f"{_r['grid_lo']:.0f} .. {_r['grid_hi']:.0f} W (swing " f"{_r['swing']:.0f} W), billed quarters {_r['billed']} W") # ⚠️ THE NEGATIVE CONTROL FIRST. If the unsized run did NOT limit-cycle, every # assertion below would be a statement about a loop that was never unstable - # MUTATION.md's "a check whose data cannot reach the failure it names", which # has now appeared in eleven cards in this repository. check("PRECONDITION: with the trade UNSIZED this loop really does run away - " "many charge onsets in one hour and a grid swing of kilowatts. Without " "this, everything below grades a loop that was never unstable", S45_BEFORE["onsets"] >= 10 and S45_BEFORE["swing"] > 2000.0) check("...and it is the bang-bang shape and not a slow drift: the meter " "reaches both a hard import and an EXPORT, so the actuator is being " "handed back and forth rather than converging", S45_BEFORE["grid_hi"] > S45_CAP_W + 1000.0 and S45_BEFORE["grid_lo"] < 0.0) check(f"THE REGRESSION: sizing the trade to the guard's measured headroom " f"settles the loop. {S45_BEFORE['onsets']} charge onsets an hour become " f"{S45_AFTER['onsets']}, and the grid swing falls from " f"{S45_BEFORE['swing']:.0f} W to {S45_AFTER['swing']:.0f} W", S45_AFTER["onsets"] <= 2 and S45_AFTER["swing"] < 0.25 * S45_BEFORE["swing"]) check("...and the battery is still TRADING for the whole hour rather than " "settling by giving up: §10.1 ranks peak shaving above arbitrage, it " "does not cancel it, and a fix that simply stopped buying would pass an " "onset count and fail the card", S45_AFTER["onsets"] == 1) check(f"...and every BILLED quarter comes in at or under the " f"{S45_CAP_W:.0f} W cap, which the unsized run does not manage - the " f"oscillation was not merely ugly, it was losing the money it was " f"oscillating about (§10.2, §9.1)", S45_AFTER["blocks"] >= 3 and all(v <= S45_CAP_W + 0.5 for v in S45_AFTER["billed"]) and any(v > S45_CAP_W for v in S45_BEFORE["billed"])) check("...and the two runs are the same code on the same fixture: one " "argument differs, and the loop, the guard, the arbiter, the averager " "and the plant are shared line for line", S45_BEFORE["blocks"] == S45_AFTER["blocks"]) # ⚠️ AND IT IS INERT WHERE THE DEFECT IS INERT, WHICH IS THE OTHER HALF OF A # FIX. The rig's sweep found the bite rule `load + max_w > peak_cap_w`; below # it, the trade never threatens the quarter and this card must change NOTHING. S45_WIDE = derived(lambda: s45_loop(sized=False, cap_w=5000.0, load_w=1200.0, seconds=900.0)) S45_WIDE_S = derived(lambda: s45_loop(sized=True, cap_w=5000.0, load_w=1200.0, seconds=900.0)) print(f" peak_cap_w 5000 W, 1200 W house (load + max_w = " f"{S45_LOAD_W + MAX_W:.0f} W, under the cap): unsized " f"{S45_WIDE['billed']} W, sized {S45_WIDE_S['billed']} W") def _s45_outcome(r): """Everything the two runs put on the PLANT and the METER. ⚠️ `rec` IS EXCLUDED AND THE EXCLUSION IS NAMED. SAFETY-47 added a per-cycle record to this harness, and it carries `headroom_w` and `sized_from_meter` - which are THE ONE ARGUMENT the two runs differ in, by construction. Comparing them would make this check fail for the reason it exists to rule out. The resolved command per cycle IS compared, below, and that is a stronger statement than the six aggregates ever were. """ return {k: v for k, v in r.items() if k != "rec"} check("on a site where the trade cannot reach the cap the two runs are " "IDENTICAL to the watt - so this card costs nothing on the installs it " "is not about, and the check above is not passing because the cap " "dominates everything", _s45_outcome(S45_WIDE) == _s45_outcome(S45_WIDE_S) and S45_WIDE["onsets"] == 1) check("...and identical CYCLE BY CYCLE, not merely in the six aggregates: " "every resolved command and every traded size matches to the float", [(r["target_w"], r["power_w"]) for r in S45_WIDE["rec"]] == [(r["target_w"], r["power_w"]) for r in S45_WIDE_S["rec"]]) # --------------------------------------------------------------------------- # print() print("3d. SAFETY-47 - the SHADOW record against a LIVE run, at a BINDING cap") # --------------------------------------------------------------------------- # WHAT THIS PART EXISTS FOR, AND IT IS NOT A SECOND SAFETY-45 CHECK. # # SAFETY-10 shipped shadow mode saying, in three merged documents, that "the # strategies are unaffected - they ask for a fixed power from inputs that do # not depend on what the battery did - so a shadow run does show you the # trades, the peak-guard engagements and the SoC-limit behaviour". That was # true until 3c's own fix merged. SAFETY-45 makes the buy leg # `min(asked, headroom)`, and the headroom is # # (900 / remaining) x (ceiling - projected) # # where `projected` reads the OPEN QUARTER'S ACCUMULATOR - the meter - which is # a function of what the battery did earlier in that same quarter. The strategy # became closed-loop the day 3c landed, and shadow mode holds the battery at # 0 W, so in a shadow run that accumulator contains THE HOUSE AND NOTHING ELSE. # # ⚠️ AND SAFETY-10 §6's COMMITTED EVIDENCE COULD NEVER HAVE FAILED - TWO # INDEPENDENT REASONS, AND CLOSING EITHER ONE ALONE CLOSES NOTHING. # (1) its fixture is `peak_cap_w: 15000` against a 350 W house, so the guard # never engages and the sizing is inert - MUTATION.md habit 4, inside a # merged card's own proof; # (2) every identity row reads THE FIRST CHARGING SAMPLE, and at `elapsed=0` # the projection IS the held level, so the two runs agree to the float # there even AT a binding cap. 3d-2 drives that and says so. # # So this part is the grader for the class: a shadow run and a live run, the # same hour, the same code, at a cap that BINDS. What is REAL and what is # TRANSCRIBED is in `s45_loop()`'s own docstring - the three lines that stand # in for `Controller._write_unit()` and `_shadow_settle()` are named there, and # `test_clamp.py` part 8 is the check on the shipped placement. # --------------------------------------------------------------------------- S47_LIVE = derived(lambda: s45_loop(sized=True)) S47_SHAD = derived(lambda: s45_loop(sized=True, shadow=True)) # The SAFETY-45 defect itself, watched from inside shadow mode - which is what # SAFETY-10's header claimed a shadow run would surface. S47_UNSIZED_SHAD = derived(lambda: s45_loop(sized=False, shadow=True)) _s47_lc = derived(lambda: [r for r in S47_LIVE["rec"] if r["action"] == CHARGE]) _s47_sc = derived(lambda: [r for r in S47_SHAD["rec"] if r["action"] == CHARGE]) # ⚠️ THE SECTION CARRIES ITS OWN PRECONDITION INTO EVERY CHECK, AND THIS IS # FCST-06's SHAPE RATHER THAN TIDINESS. A mutation that stops either run # trading at all leaves these lists EMPTY; the first `min()` below then raises, # the file dies before part 4, and the run reports ZERO reds - which reads # exactly like a survivor. Measured: driving `wanted = batt_w - ex` through # this section did precisely that. So the lists are replaced by a sentinel that # cannot satisfy anything, and `s47check()` fails every check in the section # when the stimulus was not produced. _S47_DEAD = dict(action=None, power_w=0.0, reason="", price=None, spread=None, winner=None, target_w=0.0, headroom_w=0.0, engaged=False, sized_from_meter=False, would_w=0.0, row="", t_s=0.0) _S47_OK = bool(_s47_lc) and bool(_s47_sc) if not _S47_OK: _s47_lc = _s47_sc = [dict(_S47_DEAD)] _s47_n = min(len(_s47_lc), len(_s47_sc)) def s47check(name, cond): """A 3d check, AND the section's own precondition, in one verdict.""" check(name, _S47_OK and bool(cond)) check("PRECONDITION: both runs actually reached a charging decision - without " "this the section raises on its first `min()`, the file dies before " "part 4 and the run reports ZERO reds, which reads exactly like a " "survivor (MUTATION.md, FCST-06's shape). Measured, not imagined", _S47_OK) print(f" live : {len(_s47_lc)} charging cycles, traded " f"{min(r['power_w'] for r in _s47_lc):+.1f} .. " f"{max(r['power_w'] for r in _s47_lc):+.1f} W, " f"headroom {min(r['headroom_w'] for r in _s47_lc):.1f} .. " f"{max(r['headroom_w'] for r in _s47_lc):.1f} W, " f"grid {S47_LIVE['grid_lo']:.0f} .. {S47_LIVE['grid_hi']:.0f} W") print(f" shadow: {len(_s47_sc)} charging cycles, RECORDED " f"{min(r['power_w'] for r in _s47_sc):+.1f} .. " f"{max(r['power_w'] for r in _s47_sc):+.1f} W, " f"headroom {min(r['headroom_w'] for r in _s47_sc):.1f} .. " f"{max(r['headroom_w'] for r in _s47_sc):.1f} W, " f"grid {S47_SHAD['grid_lo']:.0f} .. {S47_SHAD['grid_hi']:.0f} W") # --- 3d-1. THE PRECONDITION: the cap binds, and the meter proves it -------- # ⚠️ WITHOUT THIS EVERY CHECK BELOW IS SAFETY-10 §6's DEFECT IN A NEW FILE. At # a cap the trade cannot reach, live and shadow agree exactly and nothing here # could ever go red - which is precisely what the committed evidence did. s47check("PRECONDITION: the cap BINDS - the live run's trade is sized by the " f"guard on every charging cycle, never reaching the {MAX_W:.0f} W ask", _s47_lc and all(abs(r["power_w"]) < MAX_W - 1.0 for r in _s47_lc)) s47check("...and the live METER is what closed the gap: it is held up at the " f"{S45_CAP_W:.0f} W ceiling rather than sitting at the " f"{S45_LOAD_W:.0f} W house, which is the feedback a shadow run does not " f"have", S47_LIVE["grid_hi"] > S45_LOAD_W + 200.0) s47check("...and the SHADOW meter sees the house and nothing else for the whole " "run, which is what makes its billed quarter a different quarter", near(S47_SHAD["grid_lo"], S45_LOAD_W, 0.001) and near(S47_SHAD["grid_hi"], S45_LOAD_W, 0.001)) # --- 3d-2. WHY THE OLD SHAPE COULD NOT FAIL -------------------------------- # Graded as an EXPECTATION rather than left as a footnote, so that nobody # rebuilds a check on `[0]` and believes it covers something. print(f" first charging cycle: live {_s47_lc[0]['power_w']:+.4f} W, " f"shadow {_s47_sc[0]['power_w']:+.4f} W") s47check("⚠️ THE FIRST CHARGING CYCLE IS IDENTICAL IN BOTH RUNS EVEN HERE, so " "widening SAFETY-10 §6's fixture ALONE would not have made its table go " "red: at elapsed=0 the projection IS the held level and neither plant " "has moved yet. Both reasons had to be closed, and this is the one a " "reviewer cannot see from a diff", _s47_lc[0]["power_w"] == _s47_sc[0]["power_w"] and _s47_lc[0]["reason"] == _s47_sc[0]["reason"]) # --- 3d-3. AND ACROSS THE WHOLE RUN THEY DIVERGE --------------------------- _s47_mis = sum(1 for i in range(_s47_n) if _s47_lc[i]["power_w"] != _s47_sc[i]["power_w"]) _s47_worst = min(_s47_sc, key=lambda r: r["power_w"]) _s47_live_worst = min(_s47_lc, key=lambda r: r["power_w"]) # ⚠️ NaN AND NOT AN EXCEPTION when the sentinel above is in play: `nan > 2.0` # is False, so the check goes RED, while a ZeroDivisionError here would kill # the file and report zero reds. Same defect, one line further down - it was # driven, and it is why this is not a plain division. _s47_over = (abs(_s47_worst["power_w"]) / abs(_s47_live_worst["power_w"]) if _s47_live_worst["power_w"] else float("nan")) print(f" size mismatches {_s47_mis}/{_s47_n} charging cycles; the shadow " f"run records {_s47_worst['power_w']:+.0f} W where the live run's " f"hardest trade is {_s47_live_worst['power_w']:+.1f} W - " f"{_s47_over:.2f}x") s47check("ACROSS THE WHOLE RUN the recorded trade size diverges on all but the " "first few cycles - the check SAFETY-10 §6 should have been", _s47_mis > 0.9 * _s47_n) s47check(f"...and the shadow run RECORDS a trade the live run never commanded, " f"over by {_s47_over:.2f}x. `900/remaining` amplifies a " f"(ceiling - projected) gap the battery never closed, and the row " f"carried that number to a commissioning engineer with no caveat", _s47_over > 2.0) s47check("...and the amplification is IN THE HEADROOM, which is where the " f"measurement enters: the shadow guard offers up to " f"{max(r['headroom_w'] for r in _s47_sc):.0f} W of charging headroom " f"against a {S45_CAP_W:.0f} W ceiling and a {S45_LOAD_W:.0f} W house, " f"while the live guard never exceeds " f"{max(r['headroom_w'] for r in _s47_lc):.0f} W", max(r["headroom_w"] for r in _s47_sc) > MAX_W and max(r["headroom_w"] for r in _s47_lc) < MAX_W) # --- 3d-3b. THE SENTENCE THE ROW PRINTS, GRADED AS ARITHMETIC -------------- # ⚠️ THE ROW CLAIMS THE RECORDED NUMBER IS AN **UPPER BOUND**, and a claim on # an operator-facing surface is a claim this file has to be able to fail. # Charging only ADDS offtake, so a live `projected` is never below a shadow # one and the headroom is never above it. The earlier wording said "smaller", # which is FALSE on every cycle where the two are equal - the start of each # quarter, where `900/remaining` is 1.0 and neither plant has moved. _s47_below = [i for i in range(_s47_n) if abs(_s47_sc[i]["power_w"]) < abs(_s47_lc[i]["power_w"]) - 1e-9] _s47_equal = sum(1 for i in range(_s47_n) if abs(_s47_sc[i]["power_w"]) == abs(_s47_lc[i]["power_w"])) print(f" the shadow record is below the live command on " f"{len(_s47_below)}/{_s47_n} cycles and exactly equal on {_s47_equal}") s47check("the recorded size is an UPPER BOUND on the live one, on every compared " "cycle - which is the sentence `shadow.SIZED_FROM_METER` puts on the " "status row, so the row is making a claim this file can fail", not _s47_below) s47check("...and it is NOT strictly greater: the two are equal at the start of " f"each quarter ({_s47_equal} cycles here), which is why the row says " f"'upper bound' and not 'smaller' - the second would be a false " f"sentence on exactly those cycles", _s47_equal > 0) # ⚠️ AND THE MECHANISM BEHIND THE BOUND, AS TWO POINTS OF THE SHIPPED # ARITHMETIC RATHER THAN AS ONE PROPERTY OF A WHOLE RUN. Same ceiling, same # elapsed, same house: one quarter that carried the battery's charge at the # meter (LIVE) and one that did not (SHADOW). The closed-loop version above # cannot be driven red by any mutation that ALSO stops the fixture trading - # measured - so the relation it rests on is graded directly here. def _s47_headroom(*, meter_w, batt_w, elapsed_s=450.0): """`charge_headroom_w()` for a quarter that banked `meter_w` for `elapsed_s`, with `batt_w` measured at the pack. All shipped.""" avg = QuarterAverager(phases=1, max_hold_s=900.0) t = 0.0 while t <= elapsed_s: imp, exp = split_signed(meter_w) avg.add(make_sample("ha_signed", imp, exp, phases=1, ingest_ts=S45_T0 + timedelta(seconds=t), ingest_mono=(S45_T0.timestamp() + t))) t += 5.0 pk = _pg.decide( PeakTracker.project(avg, S45_T0 + timedelta(seconds=elapsed_s)), batt_w=batt_w, cap_w=S45_CAP_W, margin_w=0.0, reading=None, achievable_w=MAX_W, soc=50.0, soc_floor=20.0, max_hold_s=900.0, frozen=False) return _pg.charge_headroom_w(pk) _S47_TRADE_W = 300.0 _s47_h_shadow = derived(lambda: _s47_headroom(meter_w=S45_LOAD_W, batt_w=0.0)) _s47_h_live = derived(lambda: _s47_headroom( meter_w=S45_LOAD_W + _S47_TRADE_W, batt_w=-_S47_TRADE_W)) print(f" half-way through one quarter, {S45_LOAD_W:.0f} W house under a " f"{S45_CAP_W:.0f} W ceiling: the SHADOW meter (battery at 0 W) offers " f"{_s47_h_shadow:.0f} W of headroom, the LIVE meter (the same house " f"plus a {_S47_TRADE_W:.0f} W charge) offers {_s47_h_live:.0f} W") s47check("the SHADOW meter offers STRICTLY MORE charging headroom than the " "live one on the same house, same ceiling and same elapsed - which " "is the whole mechanism, in two points of the shipped arithmetic " "rather than as a property of a whole run", _s47_h_shadow > _s47_h_live) s47check("...and the live answer is the one that lands the block ON the " "ceiling, so the shadow answer is the one that is wrong about the " "bill rather than the other way round", near(_s47_h_live, S45_CAP_W - S45_LOAD_W, 1.0)) # --- 3d-4. WHAT SURVIVES OF THE EXEMPTION, GRADED -------------------------- # The narrowed claim the three documents now make, as a check rather than as a # sentence: WHICH hour and WHY are unaffected; HOW HARD is not. s47check("WHICH hour the strategy picked and at WHAT price are identical on " f"every one of the {_s47_n} compared cycles - the clock and the price " f"series are not functions of what the battery did, which is the half " f"of the old exemption that is still true", all(_s47_lc[i]["price"] == _s47_sc[i]["price"] for i in range(_s47_n))) s47check("...and so is the spread it had to clear", all(_s47_lc[i]["spread"] == _s47_sc[i]["spread"] for i in range(_s47_n))) # --- 3d-5. THE CLOSED-LOOP DEFECT THE DOCUMENTS SAID WAS VISIBLE ----------- # ⚠️ SAFETY-10's HEADER NAMED SAFETY-45's LIMIT CYCLE AS EXAMPLE #1 OF WHAT A # SHADOW RUN SURFACES. 3c's own PRECONDITION establishes that the unsized loop # really does run away; the same stimulus watched from inside shadow mode is # a settled, uneventful hour. print(f" SAFETY-45's own defect, unsized: live {S45_BEFORE['onsets']} " f"onsets/swing {S45_BEFORE['swing']:.0f} W, guard engaged " f"{S45_BEFORE['engaged']}/{S45_BEFORE['cycles']} -> in shadow mode " f"{S47_UNSIZED_SHAD['onsets']} onsets/swing " f"{S47_UNSIZED_SHAD['swing']:.0f} W, guard engaged " f"{S47_UNSIZED_SHAD['engaged']}/{S47_UNSIZED_SHAD['cycles']}") s47check("⚠️ SAFETY-45's LIMIT CYCLE IS NOT VISIBLE FROM A SHADOW RUN, and three " "merged documents said it was. The same hour, the same unsized code: " f"{S45_BEFORE['onsets']} charge onsets and a " f"{S45_BEFORE['swing']:.0f} W grid swing live, " f"{S47_UNSIZED_SHAD['onsets']} onsets and " f"{S47_UNSIZED_SHAD['swing']:.0f} W in shadow", S45_BEFORE["onsets"] >= 10 and S45_BEFORE["swing"] > 2000.0 and S47_UNSIZED_SHAD["onsets"] <= 2 and S47_UNSIZED_SHAD["swing"] < 1.0) s47check("...and neither are the guard ENGAGEMENTS the same documents promised: " f"{S45_BEFORE['engaged']} engaged cycles live against " f"{S47_UNSIZED_SHAD['engaged']} in shadow. Every term of " f"`wanted = batt_w + excess` is MEASURED, and in shadow mode the " f"battery contributes nothing to either term", S45_BEFORE["engaged"] > 100 and S47_UNSIZED_SHAD["engaged"] == 0) # --- 3d-6. THE THREE RUNGS OF `sized_from_meter()`, SEPARATED -------------- # TEST-16's rule: three rungs answer one question, so each is reached with the # other two clear. Any two of them left standing would mask the third. _s47_chg = _s47_worst _s47_sell = derived(lambda: act(PLAN, DEAR_H, thresh=_shipped, unc=NO_WEATHER, headroom=0.0)) s47check("the CHARGE rung: a metered headroom that sized a buy leg answers True, " "which is the state the caveat exists for", _arb.sized_from_meter( ArbitrageDecision(action=CHARGE, power_w=-300.0, reason="x", spread_eur_kwh=1.0, required_eur_kwh=0.1, price_now_eur_kwh=0.05), 300.0, winner=_arb.SOURCE) is True) s47check("...the SELL rung: a DISCHARGE answers False however the headroom reads. " "§9.1 - discharge and export are never capped by the peak guard - so a " "caveat on a sell would be a FALSE sentence on the status row", _s47_sell.action == DISCHARGE and _arb.sized_from_meter(_s47_sell, 300.0, winner=_arb.SOURCE) is False) s47check("...the UNMEASURED rung: a None headroom answers False. Nothing sized " "anything from a meter, so there is nothing to disclose - and `decide()` " "refuses the trade there anyway", _arb.sized_from_meter( ArbitrageDecision(action=CHARGE, power_w=-300.0, reason="x", spread_eur_kwh=1.0, required_eur_kwh=0.1, price_now_eur_kwh=0.05), None, winner=_arb.SOURCE) is False) s47check("...the WINNER rung: the same sized charge answers False once something " "else owns the resolution. `Resolution.target_w` is then somebody " "else's number and a caveat filed against it would be false", _arb.sized_from_meter( ArbitrageDecision(action=CHARGE, power_w=-300.0, reason="x", spread_eur_kwh=1.0, required_eur_kwh=0.1, price_now_eur_kwh=0.05), 300.0, winner="loop") is False) # --- 3d-7. THE ROW, AT THE WORST CYCLE ------------------------------------- _s47_row = _s47_chg["row"] _s47_first_row = _s47_sc[0]["row"] print(f" worst cycle row: {_s47_row[:230]}") s47check("the shipped row carries the caveat at the worst cycle: the operator is " "told the number was sized from a meter with only the house in it", _shadow.SIZED_FROM_METER in _s47_row) s47check("...and it still carries the watts, in their own slot, and the standing " "0 W beside them - the caveat NARROWS the claim rather than deleting " "the record a shadow run exists to produce", "it would ask for" in _s47_row and "the inverters stay at 0 W" in _s47_row and f"{round(_s47_chg['would_w']):+d}".replace("+", "") != "") s47check("...and the watts are introduced as what it would ask for THIS CYCLE, " "in every shadow state and not only the caveated ones - no number a " "shadow run records is a forecast of a live run (UX-08: in plain words)", "This cycle it would ask for" in _s47_row and "This cycle it would ask for" in _s47_first_row) # --- 3d-7b. THE ACTION LOG, WHICH IS THE RECORD READ AFTERWARDS ----------- # ⚠️ THE TWO SURFACES ARE GRADED SEPARATELY BECAUSE THEY FAILED SEPARATELY. # Driven as one mutation, deleting the caveat from the log line alone left all # 29 files green: the only check that could see that line drives a fixture with # `meter_source: off`, where no arbitrage decision - and therefore no caveat - # is reachable. The sentence is `shadow.log_line()` now, so it can be graded. _s47_log = derived(lambda: _shadow.log_line( would_w=_s47_chg["would_w"], reason="arbitrage -> -2000 W", sized_from_meter=_s47_chg["sized_from_meter"])) _s47_log_plain = derived(lambda: _shadow.log_line( would_w=_s47_chg["would_w"], reason="loop -> -300 W", sized_from_meter=False)) print(f" action log: {_s47_log[:190]}") s47check("the ACTION LOG line carries the same caveat the row does - a page that " "discloses and a 40-row record that does not is the worse half of the " "two, because the record is what is read after the run", _shadow.SIZED_FROM_METER in _s47_log) s47check("...and it is the SAME constant, not a second wording of it, so the two " "surfaces cannot start disagreeing about which cycles are caveated", _shadow.SIZED_FROM_METER in _s47_row and _shadow.SIZED_FROM_METER in _s47_log) s47check("...and an uncaveated cycle gets the plain sentence, so the caveat is " "not simply printed on everything", _shadow.SIZED_FROM_METER not in _s47_log_plain and "would have commanded" in _s47_log_plain and "ON THIS CYCLE" in _s47_log_plain) # --- 3d-8. ⚠️ AND IT IS NOT KEYED ON WHETHER THE TRADE WAS CAPPED ---------- # THE TRAP THIS PART EXISTS TO SPRING. `arbitrage`'s own sentence says "SIZED # BY THE CAPACITY-TARIFF GUARD" only when the headroom came out BELOW the ask. # In a shadow run that is the START of the quarter - where the two runs still # agree to the float. By the end `900/remaining` has multiplied the headroom # PAST the ask, the trade is NOT capped, the sentence is silent, and the # recorded number is at its most wrong. A caveat keyed on that sentence would # be quietest exactly where it is needed loudest. _S47_CAPPED = "SIZED BY THE CAPACITY-TARIFF GUARD" print(f" at the worst cycle the shipped trade sentence says " f"{'' if _S47_CAPPED in _s47_chg['reason'] else 'NOT '}capped; at the " f"first it says " f"{'' if _S47_CAPPED in _s47_sc[0]['reason'] else 'NOT '}capped") s47check("PRECONDITION: the shipped 'capped' sentence IS present at the first " "charging cycle, where the shadow number is RIGHT - so the inverse " "below is a real inversion and not an absence everywhere", _S47_CAPPED in _s47_sc[0]["reason"]) s47check("⚠️ ...and ABSENT at the worst cycle, where the shadow number is " f"{_s47_over:.2f}x out. A caveat keyed on `capped` would fire only " f"where the record is accurate and go silent where it is multiples " f"wrong; `sized_from_meter()` is keyed on the headroom having been " f"MEASURED, so it fires on both", _S47_CAPPED not in _s47_chg["reason"] and _s47_chg["sized_from_meter"] is True and _s47_sc[0]["sized_from_meter"] is True) # --- 3d-9. main.py's CALL SITES, PINNED ------------------------------------ # ⚠️ THE CALL SITE IS THE UNIT OF COVERAGE (habit 15), same as 3b's. Nothing # behavioural in this file can see main.py render the row with a hard-coded # False, because every check above calls `shadow.words()` itself. _s47_words = [n for n in ast.walk(_S45_MAIN_TREE) if isinstance(n, ast.Call) and ast.unparse(n.func).endswith("shadow.words")] _s47_flag = [ast.unparse(k.value) for c in _s47_words for k in c.keywords if k.arg == "sized_from_meter"] _s47_flag_src = _s45_assigned("shadow_sized_from_meter", clear=("None", "False")) print(f" main.py: shadow.words(sized_from_meter={_s47_flag}), and " f"self.shadow_sized_from_meter is assigned from {_s47_flag_src}") s47check("main.py renders the row from the flag THE CYCLE RECORDED, at its one " "shadow.words() call site - not from a literal and not re-derived on " "the status task, where `self.peak_guard` is a different cycle's", _s47_flag == ["self.shadow_sized_from_meter"]) s47check("...and that flag comes from the SHIPPED predicate with all three of its " "arguments - the decision, the headroom the trade was sized from, and " "the arbiter's winner - so a mutation to `False`, to a literal or to a " "two-argument transcription is red rather than silent", _s47_flag_src == ["arbitrage.sized_from_meter(" "self.arb, self.arb_headroom_w, " "winner=resolution.winner)"]) _s47_logcalls = [n for n in ast.walk(_S45_MAIN_TREE) if isinstance(n, ast.Call) and ast.unparse(n.func).endswith("shadow.log_line")] _s47_logflag = [ast.unparse(k.value) for c in _s47_logcalls for k in c.keywords if k.arg == "sized_from_meter"] print(f" main.py: shadow.log_line(sized_from_meter={_s47_logflag})") s47check("...and the ACTION LOG line comes from the shipped sentence with the " "same flag, at main.py's one call site - not re-assembled as an " "f-string where no check can reach it, which is how it survived a whole " "mutation battery once already", _s47_logflag == ["self.shadow_sized_from_meter"]) # ⚠️ AND THE PARAMETER HAS NO DEFAULT. TEST-14's rule, and this card is the # reason it matters here: a caller that FORGOT the caveat and a caller that # DECIDED it did not apply would render the identical row. _s47_sig = inspect.signature(_shadow.words) _s47_logsig = inspect.signature(_shadow.log_line) s47check("`shadow.words()` and `shadow.log_line()` BOTH require " "`sized_from_meter` - no default on either, so a caller cannot omit the " "caveat by accident and get the un-caveated surface", "sized_from_meter" in _s47_sig.parameters and _s47_sig.parameters["sized_from_meter"].default is inspect.Parameter.empty and "sized_from_meter" in _s47_logsig.parameters and _s47_logsig.parameters["sized_from_meter"].default is inspect.Parameter.empty) # --------------------------------------------------------------------------- # print() print("4. §8.3 - no irreversible action on a provisional price") # The realistic shape: the feed has published the morning and the afternoon is # carried from yesterday. The CHEAPEST hour of the whole day is in the carried # part, which is the lure - and the firm part still has a tradeable spread, so # a correct implementation trades on the firm hours rather than doing nothing. _flags = [i >= 12 for i in range(24)] _part = plan_of(DAY, provisional=_flags) _firm = [p for p, f in zip(DAY, _flags) if not f] check("the fixture's carried half contains the day's DEAREST hour, so 'the " "carried hours are ignored' is graded by a case where ignoring them " "changes the answer", _flags[DEAR_H] is True) check("...and the firm half still has a spread over the shipped threshold, so " "the correct behaviour is to TRADE on the firm hours - a check that " "expected silence would pass for an implementation that gave up", round(max(_firm) - min(_firm), 6) > required_spread(_shipped, NO_WEATHER)) _p_buy = act(_part, CHEAP_H, thresh=_shipped, unc=NO_WEATHER) _p_sell_carried = act(_part, DEAR_H, thresh=_shipped, unc=NO_WEATHER) _p_sell_firm = act(_part, _firm.index(max(_firm)), thresh=_shipped, unc=NO_WEATHER) check("the firm cheapest hour still CHARGES", _p_buy.action == CHARGE) check("...the firm dearest hour still DISCHARGES", _p_sell_firm.action == DISCHARGE) check("...and the CARRIED hour that is the day's real maximum does nothing - " "it is not the sell hour, because it is not an observation", _p_sell_carried.action is None) _all_prov = act(plan_of(DAY, provisional=True), CHEAP_H, thresh=0.0) check("a FULLY carried series trades nothing at all, whatever its spread - " "§11.4's fallback plan and a pre-publication day both land here", _all_prov.action is None and "§8.3" in _all_prov.reason) # ⚠️ A SURVIVOR, REPORTED RATHER THAN HIDDEN, AND THE ARGUMENT THAT MAKES IT # HARMLESS. Disabling `_act`'s own `if here.provisional:` guard changes no # answer in this file, and it cannot: the verdict's `cheapest`/`dearest` are # firm by `_firm_or_die`, starts are unique within a series, and the point # `at()` returns for a matched instant is therefore THE SAME OBJECT. The guard # is unreachable by construction, exactly the way peakguard's seventh mutation # is - and the honest response is to demonstrate the construction rather than # to write a check that cannot bite. It stays because it is the only thing # standing if a future caller ever hands `_act` a plan the verdict did not come # from. _probe = _arb._point_at(PLAN, hour(CHEAP_H)) _verdict = PLAN.arbitrage(now=LONG_AGO, peak_guard_active=False, min_spread_eur_kwh=0.0, uncertainty=frozenset()) check("the point matched for the buy hour IS the verdict's own cheapest point, " "by identity - which is why `_act`'s provisional guard is unreachable " "here, and why disabling it changes nothing this file can observe", _probe is _verdict.cheapest and _probe.provisional is False) check("...and the starts really are unique in a series, which is the other " "half of that argument", len({p.start for p in PLAN.read.points}) == len(PLAN.read.points)) # --------------------------------------------------------------------------- # print() print("4b. §10.2 hysteresis - a price on the boundary cannot flap at 30 s") # The card's technical notes ask for this to be "testable by replaying a price # series oscillating around the threshold and asserting a bounded transition # count". ⚠️ THE OSCILLATION IS THE POINT: these prices cross the shipped # threshold repeatedly, hour to hour, which is the shape that makes a # per-tick price-vs-threshold comparison flap. FLAP = [0.20 + (0.101 if i % 2 else 0.0) for i in range(24)] # PRICE-02: the verdict skips periods that have ended, and this day had ties # (0.20 at every even hour, 0.301 at every odd one). With ties the first hour # still ahead is the extreme, so the verdict moves on every hour - once per # hour, never per tick, which test_price02.py part 6 grades. This check is # about a verdict that stays put while the prices keep crossing the threshold, # so the day gets one cheapest and one dearest hour at its end (22: 0.60, # 23: 0.05), unique and ahead of every probe until they come. FLAP[22], FLAP[23] = 0.60, 0.05 _flap_plan = plan_of(FLAP) check("the flapping fixture really does straddle the threshold hour by hour - " "every neighbouring pair differs by more than it", all(abs(FLAP[i] - FLAP[i + 1]) > _shipped for i in range(len(FLAP) - 1))) _ticks = [] for _t in range(24 * 120): # 24 h at the 30 s control cadence _d = decide(_flap_plan, D0 + timedelta(seconds=30 * _t), peak_guard_active=False, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=frozenset(), charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0) _ticks.append(_d.action) _transitions = sum(1 for a, b in zip(_ticks, _ticks[1:]) if a != b) 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) 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) # --------------------------------------------------------------------------- # print() print("5. a failed read is not an observation - five reasons, all different") _no_plan = act(None, CHEAP_H, thresh=_shipped) _no_soc = act(PLAN, CHEAP_H, thresh=_shipped, soc=None) _uncovered = decide(PLAN, D0 + timedelta(days=9), peak_guard_active=False, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=frozenset(), charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0) _reasons = [_no_plan.reason, _no_soc.reason, _uncovered.reason, _blind.reason, _held.reason] check("no plan, no state of charge, an uncovered hour, an unmeasured peak and " "a held peak are five DISTINCT reasons - none of them silence, none of " "them each other", len(set(_reasons)) == 5 and all(a.action is None for a in (_no_plan, _no_soc, _uncovered, _blind, _held))) check("...and none of them is empty: `_idle` refuses a blank reason at the " "call site, so a status line cannot decay into nothing", all(r.strip() for r in _reasons) and raises(lambda: _arb._idle(""), ValueError)) # The no-data plan: `price_at` must RAISE rather than answer 0.0, and 0.0 is # exactly the value that would make every hour look like the cheapest. _empty = TariffPlan(PriceRead.no_data("nothing was fetched"), None, "none") check("a plan built on a no_data read trades nothing, and asking it for a " "price RAISES rather than answering 0.0 - 'no data' becoming 'free' is " "the defect this whole type family exists to remove", act(_empty, 0, thresh=0.0).action is None and raises(lambda: _empty.price_at(hour(0)), Absence)) # SoC: the reserve and the headroom, each graded where the other cannot explain # the answer. check("at the reserve FLOOR the sell hour does nothing, and the reason names " "the floor rather than the price", act(PLAN, DEAR_H, thresh=0.0, soc=20.0, floor=20.0).action is None and "reserve floor" in act(PLAN, DEAR_H, thresh=0.0, soc=20.0, floor=20.0).reason) check("...one percent above it, the same hour sells - so the refusal is about " "the floor and not about the hour", act(PLAN, DEAR_H, thresh=0.0, soc=21.0, floor=20.0).action == DISCHARGE) check("at the CEILING the buy hour does nothing, and one percent below it buys", act(PLAN, CHEAP_H, thresh=0.0, soc=95.0, ceiling=95.0).action is None and act(PLAN, CHEAP_H, thresh=0.0, soc=94.0, ceiling=95.0).action == CHARGE) check("...and a FULL pack in a NEGATIVE sell hour reports §17 rather than the " "SoC - the rule that actually forbids the trade is the one logged", "§17" in act(NEG_PLAN, DEAR_H, thresh=0.0, soc=20.0, floor=20.0).reason) # --------------------------------------------------------------------------- # print() print("6. the claim, and what NO claim means") check("an idle decision files NO claim - not a 0 W one, which would out-rank " "the loop with a park order and silently switch self-consumption off on " "every site that armed arbitrage", claim(_held) is None and claim(_no_plan) is None) _idle_res = resolve([Claim.limit("supervised", P_SAFETY, -MAX_W, MAX_W), Claim.set("loop", P_LOOP, -120.0, "following")]) check("...demonstrated through the arbiter: with arbitrage idle the loop's " "number is what goes on the wire, unchanged", near(_idle_res.target_w, -120.0) and _idle_res.winner == "loop") check("...whereas a trading decision DOES out-rank the loop, which is the " "point: the loop would otherwise cancel every charge the moment it " "created import", resolve([Claim.limit("supervised", P_SAFETY, -MAX_W, MAX_W), Claim.set("loop", P_LOOP, -120.0, "following"), _charge_claim]).winner == _arb.SOURCE) check("the claim carries the §10.2 reason code, so the action log names the " "strategy rather than describing it", _charge_claim.reason == CHARGE and claim(_sell).reason == DISCHARGE) # SAFETY-02's arming gate, which an arbitrage winner must NOT leave armed. check("SAFETY-02 is disarmed while arbitrage drives, exactly as it is during a " "maintenance grid charge: the strategy commands AGAINST the meter on " "purpose, so sign agreement is not the expectation", _main.loop_is_following( resolve([Claim.limit("supervised", P_SAFETY, -MAX_W, MAX_W), Claim.set("loop", P_LOOP, -120.0, "following"), _charge_claim]), -120.0) is False) check("...and it is still ARMED when arbitrage is idle and the loop wins, so " "the line above is not simply a watchdog that never fires", _main.loop_is_following(_idle_res, -120.0) is True) # --------------------------------------------------------------------------- # print() print("7. AC4 - 12 months of replayed prices, and what could NOT be measured") # ⚠️ READ THIS BEFORE READING THE NUMBERS BELOW. THIS SECTION IS THE SYNTHETIC # YEAR, AND IT IS NOT AC4's EVIDENCE ANY MORE - SECTION 7B IS. # # When OPT-04 shipped, this repository held no price history: `scenarios/` # carried three gzipped P1 METER captures and nothing else. AC4 was therefore # reported NOT MET rather than redefined into something passable, which is what # TARIFF-01 and SAFETY-07 had both been upheld for doing. # # DATA-02 pointed out that the missing ingredient was never a capture. EPEX BE # day-ahead is PUBLIC DATA; the 30-day meter criterion it was being carried # beside genuinely needs waiting, and this one only ever needed a fetch. Section # 7B replays the real year and AC4 is met there. # # ⚠️ SO WHY IS THE SYNTHETIC YEAR STILL HERE? Because the real one cannot grade # §17, and 7B proves that rather than assuming it. Across all 365 real days, the # number whose DEAREST hour is priced below zero is ZERO - raw wholesale and # all-in alike - so "no discharges into a negative hour" over the real year is a # count of something that never had the chance to happen. Deleting the guard # would not change it. This section's generator drives a summer midday block # negative ON PURPOSE, and it is the only thing in this file that puts a # negative price where the strategy would otherwise sell. # # What this section establishes is the half of AC4 that does not need real # prices, and it is a property of the decision rule rather than an outcome to be # hoped for: # # every executed round trip has (sell - buy) >= required >= threshold, # so its benefit net of the threshold is >= 0, on every day, always. # # That is asserted per trade below, over 365 varied days including negative ones. # HOW OFTEN a real Belgian year clears the threshold is the other question, it # is the one the product owner's open decision turns on, and section 7B is the # only place in this file that answers it. def synth_day(n): """A synthetic all-in day-ahead day. Deterministic, seeded by day number. A seasonal base plus a two-peak diurnal shape plus a per-day amplitude, and a summer midday block that is driven NEGATIVE - the §17 case has to occur in the replay or the replay never exercises it. """ import math base = 0.22 + 0.06 * math.cos(2 * math.pi * (n - 10) / 365.0) amp = 0.05 + 0.09 * abs(math.sin(n * 1.7)) # varies day to day out = [] for h in range(24): shape = (math.sin(2 * math.pi * (h - 8) / 24.0) + 0.6 * math.sin(4 * math.pi * (h - 5) / 24.0)) p = base + amp * shape if 140 < n < 230 and 11 <= h <= 14: # summer solar glut p -= 0.45 * amp * 6 out.append(round(p, 6)) return out REPLAY_START = datetime(2026, 1, 1, 0, 0, tzinfo=UTC) 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 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 for n in range(365): day0 = REPLAY_START + timedelta(days=n) # PRICE-02: no two hours share a price. The synthetic day is # symmetric, so it prints the same maximum twice; the verdict now takes # the first one still AHEAD, where it used to take the first of the # day (a tie, graded on its own in test_price02.py part 6). So the # ties are broken by a jitter of at most 5e-5 EUR/kWh that has no # direction: not later-is-dearer, which would send every day to a # buy-then-sell and hide the selectivity this replay checks. prices = [round(p + 1e-6 * ((37 * h + 101 * n) % 50), 6) for h, p in enumerate(synth_day(n))] plan = plan_of(prices, start=day0) open_buy = None for h in range(24): d = act(plan, h, thresh=threshold, unc=unc, soc=soc, floor=floor, ceiling=ceiling, start=day0) if d.action is None: continue price = prices[h] if d.action == CHARGE: soc = min(ceiling, soc + STEP_PCT) open_buy = (price, d) else: if price < 0: neg_sells += 1 soc = max(floor, soc - STEP_PCT) if open_buy is not None: buy_price, buy_d = open_buy trades.append((buy_price, price, d)) open_buy = None else: holdings.append((n, h, price)) if d.spread_eur_kwh is None or d.required_eur_kwh is None: bad.append((n, h)) return trades, holdings, bad, neg_sells _trades, _held_over, _bad, _neg_sells = derived( lambda: replay(0.05, NO_WEATHER)) or (None, None, None, None) _kwh = MAX_W / 1000.0 check(f"the replay is 365 days long and actually traded ({len(_trades)} " f"completed round trips), or every invariant below would hold vacuously", len(_trades) > 30) 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) _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 " f"({len(_losses)} exceptions)", _losses == []) _under = [d.spread_eur_kwh for _b, _s, d in _trades if d.spread_eur_kwh < d.required_eur_kwh] check("...and each one cleared its own WIDENED requirement, which is the " "stricter statement and the one §8.3 actually asks for", _under == []) _net = sum((s - b - 0.05) * _kwh for b, s, _ in _trades) check(f"so the replayed year is net-positive BY CONSTRUCTION, not by luck: " f"{_net:+.2f} EUR of benefit net of the cycle cost over " f"{len(_trades)} trades at {_kwh:.1f} kWh each, and no single trade " f"contributes a negative term", _net > 0) check(f"§17 held for the whole year: {_neg_sells} discharges into a " f"negative-price hour", _neg_sells == 0) # ...and the replay really did contain negative hours, or the line above is # a count of something that never had the chance to happen. _neg_hours = sum(1 for n in range(365) for p in synth_day(n) if p < 0) check(f"...and it had the chance to fail: {_neg_hours} hours of the synthetic " f"year are priced below zero", _neg_hours > 100) # The shipped default, over the same year. Reported, not tuned. _sh_trades = derived(lambda: replay(_shipped, NO_WEATHER)[0]) print(f" [report] at the SHIPPED threshold {_shipped} EUR/kWh widened by " f"{widen_factor(NO_WEATHER)}x, the same synthetic year completes " f"{len(_sh_trades)} round trips; at 0.05 it completes {len(_trades)}. " f"The shipped default is deliberately conservative and the number that " f"matters is the one a REAL year would give, which this repository " f"cannot answer.") check("the shipped threshold is strictly more selective than the loose one on " "the same year - the parameter does what it says, at whatever value the " "owner sets it to", len(_sh_trades) < len(_trades)) check("...and the shipped default was NOT moved to make any of this look " "better: app/arbitrage.py reads the threshold through price_option() and " "names no number of its own", "0.10" not in open(os.path.join(HERE, "app", "arbitrage.py"), encoding="utf-8").read().split('"""', 2)[2]) # --------------------------------------------------------------------------- # print() print("7B. AC4 - twelve months of REAL EPEX BE prices, replayed") # THE DATASET, AND WHAT IT IS NOT. # # `scenarios/epex-be-dayahead-2025-09-01_2026-09-01.json` - 8760 hourly Belgian # day-ahead auction prices, 2025-09-01 to 2026-09-01 Brussels-local, fetched by # `scenarios/fetch_epex_be.py` from Fraunhofer ISE's Energy-Charts API, which # publishes the BE zone under CC BY 4.0 from Bundesnetzagentur | SMARD.de. The # licence was checked BEFORE the data was committed, because the same API serves # other zones under private-use-only terms; the script refuses to write a file # whose per-response `license_info` stops saying CC BY 4.0. Provenance, # attribution and the DST accounting live in the file's own `provenance` block # and `fetch_epex_be.py --verify` re-derives every claim in it. # # ⚠️ THE FIGURES IN THAT FILE ARE RAW WHOLESALE, NOT ALL-IN, AND THIS SECTION IS # WHERE THE DIFFERENCE IS MADE VISIBLE. §9.3's canonical point is all-in; the # committed file says `"all_in": false` and carries the markup/tax assumption as # NUMBERS TO APPLY rather than numbers already applied - TARIFF-02's # double-counting trap, avoided by never baking the guess in. `_all_in()` below # applies it in one place where a reader can watch it happen, and both replays # are run, so exactly how much of the answer rests on the guess is reported # rather than hidden. import json # noqa: E402 from collections import defaultdict # noqa: E402 _DS_PATH = os.path.join(HERE, "scenarios", "epex-be-dayahead-2025-09-01_2026-09-01.json") _DS = derived(lambda: json.load(open(_DS_PATH, encoding="utf-8"))) _ASSUME = _DS["provenance"]["all_in_assumption"] def _all_in(wholesale): """The stated assumption, applied HERE and nowhere in the committed data. all_in = (wholesale + markup + network_and_levies) * (1 + vat), with the coefficients read from the dataset's own provenance block rather than retyped - a second copy is a second thing to disagree with the file. """ return round((wholesale + _ASSUME["markup_eur_per_kwh"] + _ASSUME["network_and_levies_eur_per_kwh"]) * (1.0 + _ASSUME["vat_fraction"]), 6) def _days(price_of): """The dataset as Brussels calendar days of `PricePoint`s, in order. ⚠️ GROUPED BY THE LOCAL DATE IN THE `start` STRING, NOT BY SLICING 24 AT A TIME. Two of these days are not 24 hours long, which is the whole DST check, and a fixed stride would put October's 25th hour into the next day and misalign every day after it. """ out = defaultdict(list) for p in _DS["prices"]: out[p["start"][:10]].append( PricePoint(start=datetime.fromisoformat(p["start"]), eur_per_kwh=price_of(p["eur_per_kwh"]), provisional=bool(p["provisional"]))) return [out[k] for k in sorted(out)] _AI_DAYS = derived(lambda: _days(_all_in)) _RAW_DAYS = derived(lambda: _days(lambda w: w)) # ---- the dataset is not degenerate, asserted BEFORE anything is graded ------ _lens = [len(d) for d in _AI_DAYS] check(f"the replay has {len(_AI_DAYS)} Brussels days and {sum(_lens)} hourly " f"points, which is a full year rather than a fortnight repeated", len(_AI_DAYS) == 365 and sum(_lens) == 8760) check(f"...and the day lengths are {sorted(set(_lens))}, so BOTH DST days are " f"in it: an importer that produced 24 everywhere would have lost an hour " f"in October and invented one in March", sorted(set(_lens)) == [23, 24, 25] and _lens.count(23) == 1 and _lens.count(25) == 1) _dst_day = [d for d in _AI_DAYS if len(d) == 25][0] check("...and the 25-hour day's two 02:00 points are two different INSTANTS at " "two different offsets, which is what `at()` has to tell apart", len({p.start for p in _dst_day}) == 25 and {p.start.utcoffset() for p in _dst_day} == {timedelta(hours=1), timedelta(hours=2)}) _ai_spreads = sorted(round(max(p.eur_per_kwh for p in d) - min(p.eur_per_kwh for p in d), 6) for d in _AI_DAYS) _raw_spreads = sorted(round(max(p.eur_per_kwh for p in d) - min(p.eur_per_kwh for p in d), 6) for d in _RAW_DAYS) _req_shipped = required_spread(_shipped, NO_WEATHER) _clears = [s for s in _ai_spreads if s > _req_shipped] check(f"...and the year's daily spreads STRADDLE the shipped requirement of " f"{_req_shipped:.4f} EUR/kWh - {len(_clears)} of {len(_ai_spreads)} days " f"clear it - so the threshold is grading something. A year in which every " f"day cleared, or none did, would grade nothing at any value", 0 < len(_clears) < len(_ai_spreads)) check(f"...and the spreads are not one repeated number: median " f"{_ai_spreads[len(_ai_spreads) // 2]:.4f}, min {_ai_spreads[0]:.4f}, " f"max {_ai_spreads[-1]:.4f}", len(set(_ai_spreads)) > 300 and _ai_spreads[-1] > 4 * _ai_spreads[0]) # ---- the assumption's arithmetic, asserted rather than asserted-about ------- check("the ADDITIVE half of the all-in assumption cancels out of every daily " "spread, so the trade counts below do not depend on the markup guess at " "all - only on the VAT multiplier", all(near(a, r * (1.0 + _ASSUME["vat_fraction"]), 1e-6) for a, r in zip(_ai_spreads, _raw_spreads))) # ---- ⚠️ THE DEGENERACY THAT KEEPS SECTION 7 ALIVE -------------------------- _raw_neg_h = sum(1 for d in _RAW_DAYS for p in d if p.eur_per_kwh < 0) _ai_neg_h = sum(1 for d in _AI_DAYS for p in d if p.eur_per_kwh < 0) _raw_neg_dear = sum(1 for d in _RAW_DAYS if max(p.eur_per_kwh for p in d) < 0) _ai_neg_dear = sum(1 for d in _AI_DAYS if max(p.eur_per_kwh for p in d) < 0) check(f"the real year DOES contain negative hours - {_raw_neg_h} of them at " f"wholesale, {_ai_neg_h} still negative once the assumed markup and " f"charges are added - so §17 is a live case on this market and not a " f"hypothetical", _raw_neg_h > 100 and _ai_neg_h > 0) check(f"BUT NOT ONE OF THE 365 DAYS HAS A NEGATIVE DEAREST HOUR " f"({_raw_neg_dear} raw, {_ai_neg_dear} all-in), and the guard is on the " f"hour being SOLD INTO. So a count of zero negative-price discharges over " f"this year would be a count of something that could not happen, and " f"section 7's synthetic year stays because it is the only thing here that " f"puts a negative price where the strategy would sell", _raw_neg_dear == 0 and _ai_neg_dear == 0) def replay_real(days, threshold, unc, *, soc0=50.0, floor=20.0, ceiling=95.0): """The shipped `decide()` over the real year. Returns the ledger, nothing else. The plan is built the way production builds it: one per hour, from that hour (the refresh floors to the hour) to the end of the Brussels day, plus the next day from 13:00 Brussels, when the day-ahead prices are published. So a site sees 11 to 35 hours ahead, never a whole day behind it and never a day on its own. (An earlier version built one plan per calendar day with no tomorrow: after the cheap hour no cheap hour was left ahead, and the ledger shrank to a few trades that no real site would see.) """ soc = soc0 trades, holdings, bad, neg_sells = [], [], [], 0 for di, day in enumerate(days): nxt = days[di + 1] if di + 1 < len(days) else [] open_buy = None for i, p in enumerate(day): ahead = day[i:] + (nxt if brussels(p.start).hour >= 13 else []) plan = TariffPlan( PriceRead.series(ahead, PriceProduct.DAY_AHEAD, "generic_dayahead", p.start), None, "provider") d = decide(plan, p.start + timedelta(minutes=30), peak_guard_active=False, soc=soc, min_spread_eur_kwh=threshold, uncertainty=unc, charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=floor, soc_ceiling_pct=ceiling) if d.action is None: continue if d.spread_eur_kwh is None or d.required_eur_kwh is None: bad.append(p.start) if d.action == CHARGE: soc = min(ceiling, soc + STEP_PCT) open_buy = (p.eur_per_kwh, d) else: if p.eur_per_kwh < 0: neg_sells += 1 soc = max(floor, soc - STEP_PCT) if open_buy is not None: trades.append((open_buy[0], p.eur_per_kwh, d)) open_buy = None else: holdings.append(p.start) return trades, holdings, bad, neg_sells # ⚠️ AT THE SHIPPED THRESHOLD, UNTOUCHED, AND WIDENED BY FCST-03's NO-WEATHER # FACTOR - the harsher of the two operating states, so the figure reported is # the conservative one. _r_trades, _r_held, _r_bad, _r_neg = derived( lambda: replay_real(_AI_DAYS, _shipped, NO_WEATHER)) or (None,) * 4 _r_kwh = MAX_W / 1000.0 _r_net = round(sum((s - b - _shipped) * _r_kwh for b, s, _ in _r_trades), 2) check(f"the real year traded at the SHIPPED threshold: {len(_r_trades)} " f"completed round trips over 365 days of Belgian day-ahead prices - so " f"the criterion is being graded on trades that happened, not on an empty " f"ledger that is net-positive by vacuity", len(_r_trades) > 0) check("...and every one of them carries the spread and the widened requirement " "it cleared, so the ledger is auditable rather than a count", _r_bad == []) check(f"...and it did NOT trade every day ({len(_r_trades)} round trips over " f"365 days), so the threshold selected rather than waved everything " f"through", len(_r_trades) < 365) _r_losses = [(b, s) for b, s, _ in _r_trades if s - b < _shipped] check(f"AC4: every completed round trip on the REAL year cleared the shipped " f"{_shipped} EUR/kWh - {len(_r_losses)} exceptions of {len(_r_trades)}", _r_losses == []) _r_under = [d.spread_eur_kwh for _b, _s, d in _r_trades if d.spread_eur_kwh < d.required_eur_kwh] check(f"...and each cleared its own WIDENED requirement of " f"{_req_shipped:.4f} EUR/kWh, which is the stricter statement §8.3 asks " f"for", _r_under == []) check(f"AC4 IS MET: twelve months of REAL replayed EPEX BE prices are " f"NET-POSITIVE at the shipped default - {_r_net:+.2f} EUR of benefit net " f"of the cycle cost over {len(_r_trades)} round trips of {_r_kwh:.1f} kWh " f"each, accumulated from the trades the strategy actually chose", _r_net > 0) # ---- what the owner's open decision actually turns on ----------------------- # ⚠️ REPORTED, NOT SETTLED. `cycle_cost_threshold_eur` and FCST-03's 2.5/1.5 are # the product owner's decision. DATA-02 supplies the input and changes nothing: # every number below comes out of the same untouched `decide()`. _r_calm = derived(lambda: replay_real(_AI_DAYS, _shipped, frozenset())[0]) _r_raw = derived(lambda: replay_real(_RAW_DAYS, _shipped, NO_WEATHER)[0]) # The threshold at which the MEDIAN day of this year clears its widened # requirement. Derived from the observed spreads, not chosen. _median_spread = _ai_spreads[len(_ai_spreads) // 2] _suggested = round(_median_spread / widen_factor(NO_WEATHER), 4) _r_sugg, _, _, _ = derived( lambda: replay_real(_AI_DAYS, _suggested, NO_WEATHER)) or (None,) * 4 _sugg_net = round(sum((s - b - _suggested) * _r_kwh for b, s, _ in _r_sugg), 2) print(f" [report] SHIPPED {_shipped} EUR/kWh, widened {widen_factor(NO_WEATHER)}x " f"to {_req_shipped:.4f}: {len(_r_trades)} round trips, {_r_net:+.2f} EUR " f"over the year. Unwidened (weather present): {len(_r_calm)} trips. On " f"RAW wholesale instead of the assumed all-in: {len(_r_raw)} trips, so " f"the markup/VAT guess moves the count by " f"{len(_r_trades) - len(_r_raw):+d}.") print(f" [report] the year's MEDIAN daily all-in spread is " f"{_median_spread:.4f} EUR/kWh, so the threshold at which a median day " f"clears its widened requirement is {_suggested} - and at that value the " f"same year completes {len(_r_sugg)} round trips for {_sugg_net:+.2f} " f"EUR. THE SHIPPED DEFAULT IS NOT CHANGED BY THIS CARD; both numbers are " f"reported so the owner can decide between them.") print(f" [report] and the four-developer finding stands unchanged: a STATIC " f"day/night spread of ~0.0840 EUR/kWh still does not clear " f"{_req_shipped:.4f}, so a static-tariff site never trades at the shipped " f"default. What this year adds is that a DYNAMIC site does - on " f"{len(_clears)} of its 365 days the spread was there to be taken.") check("the loose threshold is strictly less selective than the shipped one on " "the SAME real year - the parameter does what it says at whatever value " "the owner sets, and neither run tuned anything", len(_r_sugg) > len(_r_trades) > 0) check(f"§17 held over the real year too ({_r_neg} discharges into a negative " f"hour) - but see the degeneracy check above: on this data that zero is " f"structural, and section 7's synthetic year is what actually grades the " f"guard", _r_neg == 0) check("the dataset carries its own provenance, states it is NOT all-in, and " "keeps the markup/tax figures as an assumption rather than applying " "them - the TARIFF-02 trap, in data form", _DS["provenance"]["all_in"] is False and "ASSUMPTION" in _ASSUME["status"] and _DS["provenance"]["licence"].startswith("CC BY 4.0")) # --------------------------------------------------------------------------- # print() print("8. the background refresh - the tick must never await a fetch") _MAIN_SRC = derived(lambda: open(os.path.join(HERE, "app", "main.py"), encoding="utf-8").read()) _cycle = derived(lambda: _fn(_MAIN_SRC, "cycle")) _refresh = derived(lambda: _fn(_MAIN_SRC, "run_tariff_refresh")) _read_inputs = derived(lambda: _fn(_MAIN_SRC, "read_inputs")) _FETCHERS = {"resolve_tariff", "load_prices", "https_get"} check("app/main.py has a run_tariff_refresh() coroutine at all", _refresh is not None) check("...and IT is what awaits resolve_tariff(), so the fetch has somewhere " "to live that is not the control path", isinstance(_refresh, ast.AsyncFunctionDef) and "resolve_tariff" in _awaited(_refresh)) # ⚠️ THE FORWARD HAZARD, AND IT IS THE ONE THE CARD CALLS THE MOST DANGEROUS # THING IN THIS TICKET. cycle() and read_inputs() are both on the path that # feeds the ESP32 heartbeat; layer 1 commands 0 W if that stops for ~30 s. A # supplier's TLS timeout on either of them is a SUPPLIER able to drive this # add-on into the failsafe. Nothing warns about it at runtime - the add-on would # simply be slow - so it is asserted structurally, on the shipped AST. check(f"cycle() awaits NONE of {sorted(_FETCHERS)} - a supplier's TLS timeout " f"on the tick is a supplier able to push this add-on towards layer 1's " f"30 s veto {sorted(_awaited(_cycle))}", isinstance(_cycle, ast.AsyncFunctionDef) and not (_awaited(_cycle) & _FETCHERS)) check("...and neither does read_inputs(), which is on the same path and is " "where a fetch would most naturally be put", isinstance(_read_inputs, ast.AsyncFunctionDef) and not (_awaited(_read_inputs) & _FETCHERS)) check("...and the check above can fail: the scanner really does find the " "awaits that ARE there", "resolve_tariff" in _awaited(_refresh) and len(_awaited(_cycle)) > 0) # The age bound, driven on the shipped expression rather than a copy of it. class _Stub: """Just enough Controller for fresh_tariff_plan(), which touches nothing else.""" fresh_tariff_plan = _main.Controller.fresh_tariff_plan def __init__(self, plan, at): self.tariff_plan, self.tariff_at = plan, at _NOW = datetime(2026, 6, 1, 12, 0, tzinfo=UTC) _fresh_s = _main.TARIFF_MAX_AGE_S # ⚠️ THE ABSOLUTE PROBE COMES FIRST, AND THE FIRST DRAFT SHOWED WHY. Probing at # `TARIFF_MAX_AGE_S + 1` reads the constant under test, so widening the constant # moves the probe with it and the check passes for a bound that has been made # unreachable - a mutation multiplying it by a million SURVIVED that draft. # Twelve hours is stale by any reading of a feed that publishes daily, and it is # stated here rather than derived from the module being graded. check("a plan resolved TWELVE HOURS ago is not handed to the strategy, whatever " "the module's own constant says - a stale price is not a fresh one", _Stub(PLAN, _NOW - timedelta(hours=12)).fresh_tariff_plan(_NOW) is None) check(f"...and the constant is inside the range that makes that true: " f"{_fresh_s:.0f} s is under six hours and over one refresh interval", _main.TARIFF_REFRESH_S < _fresh_s < 6 * 3600) check("a plan resolved a moment ago IS handed to the strategy, so the two " "lines above are a bound rather than a blanket refusal", _Stub(PLAN, _NOW - timedelta(seconds=1)).fresh_tariff_plan(_NOW) is PLAN) check(f"...and the bound is where the module says it is: one second past " f"{_fresh_s:.0f} s fails and one second short of it passes", _Stub(PLAN, _NOW - timedelta(seconds=_fresh_s + 1) ).fresh_tariff_plan(_NOW) is None and _Stub(PLAN, _NOW - timedelta(seconds=_fresh_s - 1) ).fresh_tariff_plan(_NOW) is PLAN) check("a plan with no timestamp is not fresh either - both terms are required, " "and 'nothing stored it' is not 'it is current'", _Stub(PLAN, None).fresh_tariff_plan(_NOW) is None and _Stub(None, _NOW).fresh_tariff_plan(_NOW) is None) check("the refresh cadence is strictly under the age bound, or the plan would " "expire between two healthy refreshes and arbitrage would flicker", _main.TARIFF_REFRESH_S < _main.TARIFF_MAX_AGE_S) # CFG-03: main.py gained no fourth copy of an option default. _names = derived(lambda: {n.id for n in ast.walk(ast.parse(_MAIN_SRC)) if isinstance(n, ast.Name)}) check("main.py reads every arbitrage option through app/arbitrage.py or " "price_option(), never through `o.get('cycle_cost_threshold_eur', )` - CFG-03 still tracks three places, not four", all(f'"{k}"' not in _MAIN_SRC and f"'{k}'" not in _MAIN_SRC for k in ("cycle_cost_threshold_eur", "arbitrage_soc_floor", "arbitrage_soc_ceiling"))) # --------------------------------------------------------------------------- # print() print("8b. TARIFF-12 - a RETURNED degradation must not clear a fresh plan") # ⚠️ THE STIMULUS IS A RETURN, NOT A RAISE, AND THAT IS THE WHOLE CHECK. # `run_tariff_refresh()` guarded the plan with `except Exception` alone, and # `load_prices()` states in its own docstring that it "RETURNS, NEVER RAISES, # for every failure a site can actually have" - so the one case the guard was # written for, a supplier outage, never reached it. Everything below drives the # SHIPPED chain and stubs nothing but the socket: the transport raises the way # aiohttp does, `load_prices()` turns that into a `no_data` read, `_degrade()` # turns THAT into a §11.4 plan, and `run_tariff_refresh()` decides what to do # with it. A check that patched `resolve_tariff` to raise would have passed # against the defective code. import asyncio # noqa: E402 from app import price as _price # noqa: E402 from app.price_history import NoHistory # noqa: E402 class ClientConnectorError(Exception): """aiohttp's own class name, so the §11.4 reason this drives is the sentence the 40 h soak recorded at refresh 56, verbatim.""" _T12_FEED_UP = {"up": True} async def _t12_transport(url, timeout_s): """The socket, and nothing else, is what this replaces.""" if not _T12_FEED_UP["up"]: raise ClientConnectorError( "Cannot connect to host api.energy-charts.info:443 ssl:default") t0 = datetime.now(UTC).replace(minute=0, second=0, microsecond=0) return json.dumps({ "unix_seconds": [int((t0 + timedelta(hours=i)).timestamp()) for i in range(48)], "price": [80.0 + 40.0 * (i % 6) for i in range(48)], "unit": "EUR / MWh"}).encode() class _T12Controller: """Just enough Controller to run the SHIPPED refresh coroutine. Both methods are bound off the shipped class rather than copied, for `fresh_tariff_plan()`'s own stated reason: a transcription drifts. """ run_tariff_refresh = _main.Controller.run_tariff_refresh fresh_tariff_plan = _main.Controller.fresh_tariff_plan def __init__(self, opts): self.o = opts self.tariff_plan = None self.tariff_at = None self.price_history = NoHistory() self.price_watch = PriceWatch() # PRICE-01: the refresh passes it on self.stop_event = asyncio.Event() self.stop_event.set() # so the inter-refresh wait returns self._ticks = 0 @property def stopping(self): self._ticks -= 1 return self._ticks < 0 def refresh_once(self): self._ticks = 1 asyncio.run(self.run_tariff_refresh()) return self def _t12_opts(profile=""): # `price_timeout_s` is read with a plain `.get()` by every plugin, so it # has to be present - an options dict that relies on price_option()'s # defaults for it fetches with `float(None)` and this section would grade # a TypeError instead of an outage. return {"price_provider": "generic_dayahead", "price_timeout_s": 10, "price_horizon_h": 48, "static_profile": profile} _T12_HORIZON_H = 48 _T12_STALE_S = _main.TARIFF_MAX_AGE_S _t12_saved_transport = _price.https_get _price.https_get = _t12_transport # -- the accepted baseline (habit 7): one good fetch, varied one thing at a # -- time from here. _T12_FEED_UP["up"] = True _t12_a = derived(lambda: _T12Controller(_t12_opts()).refresh_once()) _t12_good = derived(lambda: _t12_a.tariff_plan) check(f"BASELINE: a fetch that succeeds stores an OBSERVATION - " f"{getattr(_t12_good, 'words', _t12_good)!r}", getattr(_t12_good, "origin", None) == "provider" and _t12_good.from_feed is True and len(_t12_good.read.points) == _T12_HORIZON_H) # -- the stimulus, graded before it is used on anything (habit 6/7) ---------- _T12_FEED_UP["up"] = False _t12_now = datetime.now(UTC) _t12_deg = derived(lambda: asyncio.run(resolve_tariff( _t12_opts(), _t12_now, _t12_now + timedelta(hours=_T12_HORIZON_H), now=_t12_now, history=NoHistory()))) check("STIMULUS: a dead feed makes resolve_tariff() RETURN a degraded plan - " "it does not raise, so `except Exception` is not what was protecting " "the plan and the defect was never on that branch", isinstance(_t12_deg, TariffPlan)) check(f"...and the returned plan is the §11.4 no-profile row the soak saw - " f"{getattr(_t12_deg, 'words', _t12_deg)!r}", isinstance(_t12_deg, TariffPlan) and _t12_deg.origin == "none" and _t12_deg.from_feed is False and _t12_deg.has_prices is False and "ClientConnectorError" in _t12_deg.words) # -- AC1: the degradation must not replace a plan still inside the bound ----- # ⚠️ 1800 s IS STATED HERE, NOT DERIVED FROM THE MODULE BEING GRADED - section # 8's own lesson. A probe at `TARIFF_MAX_AGE_S - 1` moves with a mutation of # the constant; half an hour is inside any reading of a 5400 s bound, and it is # the interval the soak actually ran at. _t12_b = derived(lambda: _T12Controller(_t12_opts())) _t12_b.tariff_plan, _t12_b.tariff_at = _t12_good, datetime.now(UTC) - \ timedelta(seconds=1800) _t12_at_before = _t12_b.tariff_at check(f"PRECONDITION: the standing plan is 1800 s old and TARIFF_MAX_AGE_S is " f"{_T12_STALE_S:.0f} s, so it IS fresh when the failed refresh runs - " f"without this the next check grades an expired plan", 1800 < _T12_STALE_S and _t12_b.fresh_tariff_plan(datetime.now(UTC)) is _t12_good) _t12_b.refresh_once() check("a RETURNED degradation does not replace a plan still inside " "TARIFF_MAX_AGE_S - the 48 h series is still there, by identity, and " "not a 0-point `origin: none` read", _t12_b.tariff_plan is _t12_good and _t12_b.tariff_plan.has_prices is True and len(_t12_b.tariff_plan.read.points) == _T12_HORIZON_H) check("...and `tariff_at` did NOT move, so the kept plan still ages out from " "the fetch that produced it and is distinguishable from a freshly " "fetched one. Re-stamping it here is how a plan freezes at its last " "value and looks current forever", _t12_b.tariff_at == _t12_at_before) check("...and the two timestamps a successful refresh and a kept plan produce " "really are different values, or the check above compares a number " "against itself", _t12_a.tariff_at != _t12_at_before) # -- AC3: TARIFF_MAX_AGE_S governs - the bound is where the module says ------ _t12_c = derived(lambda: _T12Controller(_t12_opts())) _t12_c.tariff_plan, _t12_c.tariff_at = _t12_good, datetime.now(UTC) - \ timedelta(seconds=_T12_STALE_S + 5) _t12_c.refresh_once() check(f"a plan that has aged PAST {_T12_STALE_S:.0f} s IS replaced by the " f"degradation - the plan is served stale for the age bound and no " f"longer, so TARIFF_MAX_AGE_S governs something rather than being " f"unreachable", _t12_c.tariff_plan is not _t12_good and _t12_c.tariff_plan.from_feed is False and _t12_c.tariff_plan.has_prices is False) _t12_d = derived(lambda: _T12Controller(_t12_opts())) _t12_d.tariff_plan, _t12_d.tariff_at = _t12_good, datetime.now(UTC) - \ timedelta(seconds=_T12_STALE_S - 60) _t12_d.refresh_once() check("...and one minute SHORT of the bound it is still kept, so the line " "above is a bound rather than 'the degradation always wins'", _t12_d.tariff_plan is _t12_good) _t12_e = derived(lambda: _T12Controller(_t12_opts()).refresh_once()) check("a degradation with NO plan standing at all is stored - §11.4 still " "reaches an install that has never had a good fetch, and the guard is " "not 'never overwrite'", _t12_e.tariff_plan is not None and _t12_e.tariff_plan.from_feed is False and _t12_e.tariff_at is not None) # -- the §11.4 STATIC FALLBACK, which is the judgement this card had to make - _t12_f = derived(lambda: _T12Controller(_t12_opts(STATIC_BE))) _t12_fb = derived(lambda: asyncio.run(resolve_tariff( _t12_opts(STATIC_BE), _t12_now, _t12_now + timedelta(hours=_T12_HORIZON_H), now=_t12_now, history=NoHistory()))) check(f"PRECONDITION: with a static_profile configured the SAME dead feed " f"degrades to a §11.4 FALLBACK carrying real prices, not to the " f"no-profile row - {getattr(_t12_fb, 'words', _t12_fb)!r}", isinstance(_t12_fb, TariffPlan) and _t12_fb.origin == "fallback" and _t12_fb.has_prices is True and _t12_fb.from_feed is False) check("...and every one of its points is provisional, so firm_points() is " "empty, arbitrage() refuses and cheap_now() is False - it is a real " "tariff and nothing may be acted on it. That is why it does not replace " "a fresh observation", isinstance(_t12_fb, TariffPlan) and _t12_fb.provisional is True and _t12_fb.firm_points() == () and _t12_fb.arbitrage(now=LONG_AGO, peak_guard_active=False, min_spread_eur_kwh=0.0, uncertainty=NO_WEATHER).kind == "refused") _t12_f.tariff_plan, _t12_f.tariff_at = _t12_good, datetime.now(UTC) - \ timedelta(seconds=1800) _t12_f_at = _t12_f.tariff_at _t12_f.refresh_once() check("a §11.4 static FALLBACK does not replace a fresh provider plan either, " "although it has prices - a day-ahead price for an hour does not change " "once published, so the 30-minute-old observation is no less true and " "is the only one of the two that carries a firm hour", _t12_f.tariff_plan is _t12_good and _t12_f.tariff_at == _t12_f_at) # -- ...and the rule is NOT symmetrical: a degradation replaces a degradation. _t12_g = derived(lambda: _T12Controller(_t12_opts(STATIC_BE))) _t12_g.tariff_plan, _t12_g.tariff_at = _t12_fb, datetime.now(UTC) - \ timedelta(seconds=1800) _t12_g_at = _t12_g.tariff_at _t12_g.refresh_once() check("a standing plan that is ITSELF a §11.4 fallback IS replaced by the " "next one, and its timestamp moves - otherwise an outage would freeze " "the operator's static profile at the window it was first generated " "for, and the guard would be 'never overwrite' wearing a bound's name", _t12_g.tariff_plan is not _t12_fb and _t12_g.tariff_plan.origin == "fallback" and _t12_g.tariff_at != _t12_g_at) # -- ...and a recovered feed replaces a kept plan, which is the third term --- _T12_FEED_UP["up"] = True _t12_h = derived(lambda: _T12Controller(_t12_opts())) _t12_h.tariff_plan, _t12_h.tariff_at = _t12_good, datetime.now(UTC) - \ timedelta(seconds=1800) _t12_h_at = _t12_h.tariff_at _t12_h.refresh_once() check("when the feed answers again the observation replaces the kept plan and " "`tariff_at` moves - the guard keeps a plan against a DEGRADATION, not " "against a fetch that worked", _t12_h.tariff_plan is not _t12_good and _t12_h.tariff_plan.from_feed is True and _t12_h.tariff_at != _t12_h_at) # -- TARIFF-10 widened WHICH INSTALLS RUN THIS TASK, so the configurations it # -- newly keeps alive are graded here rather than reasoned about. # ⚠️ `price_provider: off` + a `static_profile` + `arbitrage_enabled: true` is a # configuration validate.py ACCEPTS and TARIFF-10's `or` exists to protect. On # it `resolve_tariff()` returns `static_plan(provisional=False)` - the # CONFIGURED product, origin "static" - so `standing.from_feed` is false on # every refresh and this card's guard must never fire. Confirmed by driving it, # not by reading the origin table: a guard that quietly latched here would # freeze a static site's plan at its first window for the life of the install. _t12_i = derived(lambda: _T12Controller( {"price_provider": "off", "price_timeout_s": 10, "price_horizon_h": 48, "static_profile": STATIC_BE}).refresh_once()) _t12_i_plan, _t12_i_at = _t12_i.tariff_plan, _t12_i.tariff_at _t12_i.refresh_once() check("PRECONDITION: a `price_provider: off` + static_profile site resolves to " "the CONFIGURED product, not to a §11.4 row - origin 'static', prices, " "and from_feed false because there is no feed to have observed", getattr(_t12_i_plan, "origin", None) == "static" and _t12_i_plan.has_prices is True and _t12_i_plan.from_feed is False) check("...and TARIFF-10 now keeps this task alive on exactly that site, where " "this card's guard must NEVER latch: the second refresh replaces the " "first and the timestamp moves, so the plan is regenerated for the " "current window every 30 minutes as it always was", _t12_i.tariff_plan is not _t12_i_plan and _t12_i.tariff_plan.origin == "static" and _t12_i.tariff_at != _t12_i_at) # ⚠️ AND THE OTHER CONFIGURATION TARIFF-10 ADDED - a provider configured with # trading DISARMED - is graded by every driven check above without a second # fixture, because nothing on this path reads `arbitrage_enabled` at all. That # is a claim about the code rather than about a behaviour, so it is asserted as # one (habit 4's exception, FCST-06's precedent): a fixture that set the option # and asserted no difference would be comparing a thing against itself. _T12_TRADING_OPTS = ("arbitrage_enabled", "cycle_cost_threshold_eur", "arbitrage_soc_floor", "arbitrage_soc_ceiling") # ⚠️ AND IT IS WHAT THE FUNCTION READS THAT IS SCANNED, NOT ITS TEXT. Two # earlier drafts of this check were red against a CORRECT tree: the first # because the docstring paragraph that SAYS `arbitrage_enabled` is not read is # itself an occurrence of the string, the second because the log line says # "arbitrage, where it is armed, stops with it" - which is a sentence, not a # read. A substring test over a function grades its prose as much as its # behaviour (habit 4). So the symbols are taken from the AST, and the string # constants are taken separately and with the docstring dropped: naming # arbitrage in a message is fine, READING a trading option is the thing claimed # not to happen. _t12_syms = derived(lambda: {getattr(n, "id", None) or getattr(n, "attr", None) for n in ast.walk(_refresh) if isinstance(n, (ast.Name, ast.Attribute))}) _t12_consts = derived(lambda: {c.value for b in _refresh.body[1:] for c in ast.walk(b) if isinstance(c, ast.Constant) and isinstance(c.value, str)}) check("run_tariff_refresh() READS no trading option and calls nothing in " "app/arbitrage.py, so keeping a plan is a decision about what the add-on " "has LOOKED UP - TARIFF-10's own distinction, and why the checks above " "cover a provider-configured site whether or not arbitrage is armed", isinstance(_t12_syms, set) and not any("arbitrage" in (n or "") for n in _t12_syms) and not (_t12_consts & set(_T12_TRADING_OPTS)) # ...and both scanners can see what IS there, or they grade nothing: the # one option this task does read, and a name it really does call. and "price_horizon_h" in _t12_consts and "fresh_tariff_plan" in _t12_syms # ...and the docstring really was dropped, or the first draft's defect is # back and the constant set is grading prose again. and not any("RETURNS, NEVER RAISES" in c for c in _t12_consts)) # -- the vocabulary, graded on its own so a mutation of it has a named home -- check("TariffPlan.from_feed is true of exactly the observation - a §11.4 " "fallback, a no-data read and a CONFIGURED static profile are all false, " "and it is not a synonym for has_prices", _t12_good.from_feed is True and _t12_fb.from_feed is False and _t12_deg.from_feed is False and static_plan({"static_profile": STATIC_BE}, D0, D0 + timedelta(days=2), now=D0).from_feed is False and static_plan({"static_profile": STATIC_BE}, D0, D0 + timedelta(days=2), now=D0).has_prices is True) # ⚠️ A SHAPE CLAIM, AND IT IS ALLOWED TO BE ONE (FCST-06's precedent). "The age # is judged in ONE place" is a claim about the code, not about a behaviour: a # transcribed copy of the same comparison behaves identically until the day the # two drift. Every other check in this section is driven. check("run_tariff_refresh() asks fresh_tariff_plan() rather than re-deriving " "the age bound - one place judges it, which is what that method's own " "docstring is for", _refresh is not None and {n.func.attr for n in ast.walk(_refresh) if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute)} >= {"fresh_tariff_plan"}) _price.https_get = _t12_saved_transport # --------------------------------------------------------------------------- # print() print("9. the startup gate - the configurations that cannot work are refused") def vbase(**over): o = {"setpoint_entity": "number.goodwe_setpoint", "max_w": 2000, "soc_entity": "sensor.soc", "batt_entity": "sensor.batt", "meter_source": "off", "maintenance_charge_w": 2000, "maintenance_discharge_w": 2000} o.update(over) return o def why(problems, needle): return any(needle in p for p in problems) P1_ON = {"meter_source": "ha_signed", "p1_net_entity": "sensor.p1"} check("the shipped configuration still starts - arbitrage_enabled ships off " "and this ticket may not have made an existing install unbootable", validate_startup_config(vbase(), -5000, 5000) == [] and enabled({}) is False) check("arbitrage on with a provider and P1 starts", validate_startup_config( vbase(arbitrage_enabled=True, price_provider="generic_dayahead", **P1_ON), -5000, 5000) == []) check("...and so does arbitrage on a static profile with P1, because §9.2 " "makes a static site first-class rather than unsupported", validate_startup_config( vbase(arbitrage_enabled=True, static_profile=STATIC_BE, **P1_ON), -5000, 5000) == []) check("arbitrage on with NO price source at all is refused - a strategy that " "must refuse on every cycle forever looks exactly like one that is " "running and finding nothing", why(validate_startup_config(vbase(arbitrage_enabled=True, **P1_ON), -5000, 5000), "the site has no prices")) check("arbitrage on with meter_source off is refused - §9.1 puts peak shaving " "above arbitrage and an unmeasured peak cannot be shaved", why(validate_startup_config( vbase(arbitrage_enabled=True, price_provider="generic_dayahead"), -5000, 5000), "meter_source")) check("a floor at or above the ceiling is refused - there is no band left " "between them and the strategy could never act", why(validate_startup_config( vbase(arbitrage_enabled=True, price_provider="generic_dayahead", arbitrage_soc_floor=90, arbitrage_soc_ceiling=90, **P1_ON), -5000, 5000), "Make the lower limit the smaller number")) check("...and the same numbers the right way round start", validate_startup_config( vbase(arbitrage_enabled=True, price_provider="generic_dayahead", arbitrage_soc_floor=25, arbitrage_soc_ceiling=90, **P1_ON), -5000, 5000) == []) check("a NEGATIVE cycle_cost_threshold_eur is refused even with arbitrage OFF " "- it is a cost, and a negative one is a threshold every spread clears", why(validate_startup_config(vbase(cycle_cost_threshold_eur=-0.01), -5000, 5000), "Set it to 0 or more") and raises(lambda: threshold_eur_kwh({"cycle_cost_threshold_eur": -1}), ValueError)) # Rule 7's wording, which OPT-04 had to update because it named what main.py # did NOT do. test_price.py section 12 pins the load_prices consumer set; this # pins the sentence that set exists to keep honest. _V = derived(lambda: open(os.path.join(HERE, "app", "validate.py"), encoding="utf-8").read()) check("rule 7's message no longer says a configured provider shapes no energy " "decision - OPT-04 made that false, and the instruction attached to the " "tripwire was to fix the wording rather than to silence the check", isinstance(_V, str) and "shape no energy decision" not in _V and "still shapes no energy decision" not in _V) check("...and it names the consumer that now exists, so it cannot decay into a " "refusal with no stated reason", isinstance(_V, str) and "app/arbitrage.py" in _V) # The option mirror, pinned to config.yaml the way TARIFF-02 pinned the rest. _CFG = derived(lambda: open(os.path.join(HERE, "config.yaml"), encoding="utf-8").read()) check("every option this card added is in config.yaml's options block AND its " "schema block, or the Supervisor drops it and the add-on silently runs " "on the mirror instead of the operator's value", isinstance(_CFG, str) and all(_CFG.count(f"\n {k}:") == 2 for k in ("arbitrage_enabled", "cycle_cost_threshold_eur", "arbitrage_soc_floor", "arbitrage_soc_ceiling"))) check("...and app/arbitrage.py reads them through price_option() only - one " "read path, one place the defaults live", "PRICE_OPTION_DEFAULTS[" not in open(os.path.join(HERE, "app", "arbitrage.py"), encoding="utf-8").read()) check("soc_bounds() returns the shipped pair, floor first", soc_bounds({}) == (float(PRICE_OPTION_DEFAULTS["arbitrage_soc_floor"]), float(PRICE_OPTION_DEFAULTS["arbitrage_soc_ceiling"]))) # --------------------------------------------------------------------------- # print() print("10. TARIFF-14 - the QUARTER-HOURLY day, end to end through decide()") # ⚠️ THIS IS THE PROPERTY THE DEFECT ACTUALLY BROKE, WHICH IS WHY IT IS GRADED # HERE AND NOT ONLY ON `at()`. `_act()` decides whether this interval is the # buy one or the sell one with `here.start == v.cheapest.start` - an IDENTITY # test between the point `_point_at()` matched and the point the verdict was # built from. `_Series.at()` used to return the first point whose start was # within one hour BEFORE the instant, in list order, so on a 900 s series that # identity almost never held: the strategy idled through exactly the interval # it exists to trade, and read `here.provisional` and §17's `price < 0` off a # point that was not the one being traded. # # Belgium moved to a 15-minute MTU on 2025-10-01 and the live day-ahead feed # returns 96 points at 900 s. `static_tariff.py` emits HOURLY points, so every # fixture above this line - and every static site - is immune; this is # reachable only down the dynamic path TARIFF-08/09 made work. QD0 = datetime(2026, 1, 15, 0, 0, tzinfo=timezone(timedelta(hours=1))) QUARTER = timedelta(minutes=15) def qplan_of(prices, *, provisional=False, start=QD0, gaps=None): """A TariffPlan over `prices` spaced by `gaps` seconds (900 s by default). `gaps` is what makes the MIXED fixture below expressible: FCST-04's carry-forward emits HOURLY carried points beside QUARTER-HOURLY firm ones, and the soak measured spacing [900, 3600] in 70 % of samples. """ g = [900] * (len(prices) - 1) if gaps is None else list(gaps) flags = ([bool(provisional)] * len(prices) if isinstance(provisional, bool) else list(provisional)) pts, t = [], start for i, (p, f) in enumerate(zip(prices, flags)): pts.append(PricePoint(start=brussels(t), eur_per_kwh=round(float(p), 6), provisional=bool(f))) if i < len(g): t += timedelta(seconds=g[i]) return TariffPlan(PriceRead.series(pts, PriceProduct.DAY_AHEAD, "generic_dayahead", start), None, "provider") def qprobe(i, *, start=QD0, gaps=None): """An instant 7 minutes into slot `i` - inside every window, never on a boundary, and NOT the midpoint of an hour either, so an implementation that answered per hour and one that answers per quarter differ here.""" g = [900] * i if gaps is None else list(gaps)[:i] return start + timedelta(seconds=sum(g)) + timedelta(minutes=7) def shipped_at(series, u): """THE RULE TARIFF-14 REPLACED, kept here as the COUNTERFACTUAL ONLY. The first point whose start is within one hour before `u`, in list order. Nothing below is graded against it; it exists so that the fixtures can be shown to SEPARATE the two rules. A check whose data cannot reach the failure it names is decoration (MUTATION.md), and on an hourly series the two rules agree at every instant - which is why 29 files stayed green. """ uu = u.astimezone(UTC) for p in series.points: s = p.start.astimezone(UTC) if s <= uu < s + timedelta(hours=1): return p return None # The hourly day interpolated across its quarters, with the two extremes # planted OFF the hour boundary on purpose. QBASE = [] for _i in range(24): _a, _b = DAY[_i], DAY[(_i + 1) % 24] for _q in range(4): QBASE.append(round(_a + (_b - _a) * _q / 4 + 0.000001 * (_i * 4 + _q), 6)) Q_CHEAP_I = 4 * 5 + 3 # 05:45 - a :45 quarter, not a whole hour Q_DEAR_I = 4 * 19 + 1 # 19:15 - a :15 quarter, not a whole hour QDAY = list(QBASE) QDAY[Q_CHEAP_I] = round(min(QBASE) - 0.030, 6) QDAY[Q_DEAR_I] = round(max(QBASE) + 0.030, 6) QPLAN = qplan_of(QDAY) QPTS = QPLAN.read.points print("10a. the fixture is not degenerate, and it SEPARATES the two rules") _qgaps = sorted({int((b.start - a.start).total_seconds()) for a, b in zip(QPTS, QPTS[1:])}) check(f"the day is 96 points at {_qgaps} s - the shape the live feed has " f"returned since Belgium's 2025-10-01 move to a 15-minute MTU", len(QPTS) == 96 and _qgaps == [900]) check(f"the cheapest quarter is slot {Q_CHEAP_I} and the dearest slot " f"{Q_DEAR_I} - neither first, neither last, {abs(Q_CHEAP_I - Q_DEAR_I)} " f"slots apart, and NEITHER ON A WHOLE HOUR, without which this section " f"would grade nothing an hourly fixture does not already grade", QDAY.index(min(QDAY)) == Q_CHEAP_I and QDAY.index(max(QDAY)) == Q_DEAR_I and Q_CHEAP_I % 4 != 0 and Q_DEAR_I % 4 != 0 and Q_CHEAP_I not in (0, 95) and Q_DEAR_I not in (0, 95) and abs(Q_CHEAP_I - Q_DEAR_I) > 1) check("...and all 96 prices are distinct, so 'the cheapest quarter' names one " "quarter and no tie-break is what any check below is really grading", len(set(QDAY)) == 96) check(f"...and its spread is {round(max(QDAY) - min(QDAY), 6):.4f} EUR/kWh, " f"clear of the widened threshold, so what follows grades the strategy " f"and not the spread rung", round(max(QDAY) - min(QDAY), 6) > required_spread(_shipped, NO_WEATHER)) # ⚠️ HABIT 4, AND THE ONLY REASON THIS SECTION IS WORTH ITS LINES. State, as a # check, that the shipped rule and the fixed one give DIFFERENT points at the # two probes that matter. Without it, every check below could be passing on a # fixture the defect could not have changed. _cf_buy = derived(lambda: shipped_at(QPLAN.read, qprobe(Q_CHEAP_I))) _cf_sell = derived(lambda: shipped_at(QPLAN.read, qprobe(Q_DEAR_I))) check(f"the shipped rule and this one give DIFFERENT points at both probes - " f"buy: {derived(lambda: _cf_buy.eur_per_kwh)} instead of " f"{QDAY[Q_CHEAP_I]}, sell: {derived(lambda: _cf_sell.eur_per_kwh)} " f"instead of {QDAY[Q_DEAR_I]} - so the fixture can reach the failure it " f"names", _cf_buy is not QPTS[Q_CHEAP_I] and _cf_sell is not QPTS[Q_DEAR_I] and _cf_buy is not None and _cf_sell is not None) print() print("10b. the buy/sell identity test matches the actual extremum interval") _qv = QPLAN.arbitrage(now=LONG_AGO, peak_guard_active=False, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER) check("the verdict's cheapest and dearest are the two QUARTERS, not the hours " "containing them - `static_tariff` reduces over every firm point and " "carries no resolution assumption of its own", derived(lambda: _qv.cheapest) is QPTS[Q_CHEAP_I] and derived(lambda: _qv.dearest) is QPTS[Q_DEAR_I]) _pa_buy = derived(lambda: _arb._point_at(QPLAN, qprobe(Q_CHEAP_I))) _pa_sell = derived(lambda: _arb._point_at(QPLAN, qprobe(Q_DEAR_I))) check("THE PROPERTY THE DEFECT BROKE: at the buy probe `_point_at()` returns " "the verdict's OWN cheapest point, by identity - which is what makes " "`_act()`'s `here.start == v.cheapest.start` true, and what made it " "false on every quarter-hourly series the add-on has ever fetched", _pa_buy is derived(lambda: _qv.cheapest)) check("...and the same at the sell probe, against the verdict's dearest", _pa_sell is derived(lambda: _qv.dearest)) _qbuy = decide(QPLAN, qprobe(Q_CHEAP_I), peak_guard_active=False, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0) _qsell = decide(QPLAN, qprobe(Q_DEAR_I), peak_guard_active=False, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0) check("...so decide() CHARGES in the cheapest quarter and DISCHARGES in the " "dearest one, which is the trade the strategy exists to make and the " "one it slept through", _qbuy.action == CHARGE and _qsell.action == DISCHARGE) check(f"...at THAT QUARTER's price and not at the price of the hour " f"containing it (buy {_qbuy.price_now_eur_kwh}, sell " f"{_qsell.price_now_eur_kwh})", near(_qbuy.price_now_eur_kwh, QDAY[Q_CHEAP_I]) and near(_qsell.price_now_eur_kwh, QDAY[Q_DEAR_I])) check("...and both reasons quote that quarter's price, so the operator's log " "names the interval that was actually traded", f"{QDAY[Q_CHEAP_I]:.4f}" in _qbuy.reason and f"{QDAY[Q_DEAR_I]:.4f}" in _qsell.reason) # The negative arm. Without it "it acted" could be "it acts everywhere". _QOFF_I = Q_CHEAP_I + 2 check(f"the off-peak probe's own price differs from its hour's :00 price " f"({QDAY[_QOFF_I]} vs {QDAY[4 * (_QOFF_I // 4)]}), so the check below " f"can tell which point the decision read", QDAY[_QOFF_I] != QDAY[4 * (_QOFF_I // 4)] and _QOFF_I not in (Q_CHEAP_I, Q_DEAR_I)) _qoff = decide(QPLAN, qprobe(_QOFF_I), peak_guard_active=False, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0) check("a quarter that is neither extremum still idles, for the reason that " "names the right rule, and reports ITS OWN price - not the price of the " "quarter the shipped scan would have matched", _qoff.action is None and _qoff.reason == _arb.IDLE_OFF_PEAK_HOUR and near(_qoff.price_now_eur_kwh, QDAY[_QOFF_I])) print() print("10c. §17 and §8.3 are evaluated on the QUARTER being traded") # ⚠️ §17 AT 900 s. The whole day is below zero and the dearest QUARTER is the # least negative one - section 2's shape, at the resolution the live feed # actually publishes. Under the shipped scan this probe never reached the # guard at all: the identity test failed first and the decision idled as # "neither the buy hour nor the sell hour", so §17 was not so much wrong as # never consulted. _shift = round(max(QDAY) + 0.020, 6) QNEG = [round(p - _shift, 6) for p in QDAY] QNEG_PLAN = qplan_of(QNEG) check(f"the negative day's dearest quarter is slot {Q_DEAR_I} and it is BELOW " f"ZERO ({max(QNEG):+.4f}), and its spread still clears the widened " f"threshold - so the refusal below is §17's and not the spread rung's", QNEG.index(max(QNEG)) == Q_DEAR_I and max(QNEG) < 0 and round(max(QNEG) - min(QNEG), 6) > required_spread(_shipped, NO_WEATHER)) _qneg_sell = decide(QNEG_PLAN, qprobe(Q_DEAR_I), peak_guard_active=False, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0) check("selling into a negative-price QUARTER is refused, and the reason names " "§17 and THAT QUARTER's price", _qneg_sell.action is None and "§17" in _qneg_sell.reason and f"{max(QNEG):+.4f}" in _qneg_sell.reason) # ...and the other side, so the refusal is about the sign and nothing else. QPOS = list(QNEG) QPOS[Q_DEAR_I] = round(-max(QNEG), 6) check("...while the SAME quarter-hourly series with that ONE price's SIGN " "FLIPPED discharges instead", decide(qplan_of(QPOS), qprobe(Q_DEAR_I), peak_guard_active=False, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0).action == DISCHARGE and sum(1 for a, b in zip(QNEG, QPOS) if a != b) == 1) # §8.3's flag, read off the covering point. `_act`'s own provisional guard # stays unreachable here for section 4's reason - the extrema are firm by # `_firm_or_die` and starts are unique - and THAT ARGUMENT IS EXACTLY WHAT THE # DEFECT BROKE at 900 s, because the point `at()` returned was not the # verdict's point at all. It is restated here at the resolution that matters. check("the point matched for the buy quarter IS the verdict's own cheapest " "point and is FIRM, so `_act`'s §8.3 guard is unreachable at 900 s for " "the same construction section 4 states at 3600 s", _pa_buy is derived(lambda: _qv.cheapest) and derived(lambda: _pa_buy.provisional) is False and len({p.start for p in QPTS}) == len(QPTS)) print() print("10d. the MIXED series FCST-04 actually emits, traded end to end") # ⚠️ 70 % OF SOAK SAMPLES, NOT AN EDGE CASE. The carry-forward emits HOURLY # carried points beside QUARTER-HOURLY firm ones, so a window derived from one # global spacing would be wrong in the NORMAL steady state. Here: 12 carried # hourly points for the morning, then firm quarters for the rest of the day. MIX_N_H, MIX_N_Q = 12, 48 MIX_GAPS = [3600] * MIX_N_H + [900] * (MIX_N_Q - 1) MIX_PRICES = [QDAY[4 * i] for i in range(MIX_N_H)] + QDAY[4 * MIX_N_H:] MIX_FLAGS = [True] * MIX_N_H + [False] * MIX_N_Q MIX_PLAN = qplan_of(MIX_PRICES, provisional=MIX_FLAGS, gaps=MIX_GAPS) MIX_PTS = MIX_PLAN.read.points _mix_gaps = sorted({int((b.start - a.start).total_seconds()) for a, b in zip(MIX_PTS, MIX_PTS[1:])}) check(f"the mixed plan really is mixed - spacing {_mix_gaps} s, " f"{MIX_N_H} carried and {MIX_N_Q} firm points, so neither half is what " f"the other half's window rule would give", _mix_gaps == [900, 3600] and sum(1 for p in MIX_PTS if p.provisional) == MIX_N_H and sum(1 for p in MIX_PTS if not p.provisional) == MIX_N_Q) _mix_cheap_i = MIX_N_H + (MIX_PRICES[MIX_N_H:].index(min(MIX_PRICES[MIX_N_H:]))) _mix_dear_i = MIX_N_H + (MIX_PRICES[MIX_N_H:].index(max(MIX_PRICES[MIX_N_H:]))) check(f"...and its firm half's extremes are quarters {_mix_cheap_i} and " f"{_mix_dear_i}, neither on a whole hour, so the identity test below is " f"about the quarter and not about the hour", (_mix_cheap_i - MIX_N_H) % 4 != 0 and (_mix_dear_i - MIX_N_H) % 4 != 0) _mv = MIX_PLAN.arbitrage(now=LONG_AGO, peak_guard_active=False, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER) _mix_buy_t = qprobe(_mix_cheap_i, gaps=MIX_GAPS) _mix_sell_t = qprobe(_mix_dear_i, gaps=MIX_GAPS) check("the verdict over a MIXED series still names firm quarters, and " "`_point_at()` matches them by identity at their own probes", derived(lambda: _mv.cheapest) is MIX_PTS[_mix_cheap_i] and derived(lambda: _mv.dearest) is MIX_PTS[_mix_dear_i] and derived(lambda: _arb._point_at(MIX_PLAN, _mix_buy_t)) is MIX_PTS[_mix_cheap_i] and derived(lambda: _arb._point_at(MIX_PLAN, _mix_sell_t)) is MIX_PTS[_mix_dear_i]) # PRICE-02: the same day twice, as a running site holds 24-48 h (see plan_of). # The probes stay in day one and keep their extremes: once the cheap quarter is # over, the verdict at the dear quarter needs a cheap one still ahead. MIX2_PLAN = qplan_of(MIX_PRICES * 2, provisional=MIX_FLAGS * 2, gaps=MIX_GAPS + [900] + MIX_GAPS) check("...so the mixed day CHARGES and DISCHARGES in its firm quarters, at " "their own prices", decide(MIX2_PLAN, _mix_buy_t, peak_guard_active=False, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0).action == CHARGE and decide(MIX2_PLAN, _mix_sell_t, peak_guard_active=False, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0).action == DISCHARGE) # An instant inside a CARRIED hour of the same series. The covering point is # the carried one; the shipped scan answered a point from the FIRM block # earlier in the list, which is §8.3's flag read off the wrong point. _mix_carried_t = QD0 + timedelta(hours=MIX_N_H - 1, minutes=37) check("an instant inside a CARRIED hour of the mixed series matches the " "CARRIED point, whose provisional flag is what §8.3 reads - and the " "strategy does nothing there, because a carried point is not in the " "firm trade set", derived(lambda: _arb._point_at(MIX_PLAN, _mix_carried_t)) is MIX_PTS[MIX_N_H - 1] and MIX_PTS[MIX_N_H - 1].provisional is True and decide(MIX_PLAN, _mix_carried_t, peak_guard_active=False, soc=50.0, min_spread_eur_kwh=_shipped, uncertainty=NO_WEATHER, charge_headroom_w=HEADROOM_CLEAR, charge_w=MAX_W, discharge_w=MAX_W, soc_floor_pct=20.0, soc_ceiling_pct=95.0).action is None) print() print("10e. the HOURLY day is unchanged, which is the regression half") # ⚠️ A REGRESSION GUARD, NOT A WITNESS. On an hourly series the shipped rule # and this one agree at every instant, which is precisely why 29 files stayed # green through the defect. It is here so that a fix which got 900 s right by # breaking 3600 s cannot pass. _h_buy = act(PLAN, CHEAP_H, thresh=_shipped, unc=NO_WEATHER) _h_sell = act(PLAN, DEAR_H, thresh=_shipped, unc=NO_WEATHER) check("the hourly fixture still charges in its cheapest hour and discharges " "in its dearest, at those hours' prices", _h_buy.action == CHARGE and _h_sell.action == DISCHARGE and near(_h_buy.price_now_eur_kwh, DAY[CHEAP_H]) and near(_h_sell.price_now_eur_kwh, DAY[DEAR_H])) check("...and a probe 59 minutes into an hourly slot still matches that slot, " "so the hourly window did not shrink under the fix", derived(lambda: _arb._point_at( PLAN, D0 + timedelta(hours=CHEAP_H, minutes=59))) is derived(lambda: PLAN.read.points[CHEAP_H])) # --------------------------------------------------------------------------- # print() if fails: print(f"{len(fails)} FAILED: " + "; ".join(fails)) sys.exit(1) print("all checks passed")