Compare commits

...
2 Commits
Author SHA1 Message Date
glenn schrooyenandClaude Opus 5 2b8b8cb97f SAFETY-26: pin the negative-price refusal from below, not only from above
checks / unit (pull_request) Successful in 3m15s
OPT-04's reviewer weakened `if price < 0` to `if price < -0.001` and every
suite stayed green while a -0.0005 EUR/kWh sell hour discharged. The shipped
code was right the whole time; the CHECK could not tell correct from
nearly-correct. The only probe near the boundary sat at 0.0000, which catches a
`<=` and says nothing about a tolerance invented on the other side - and a
tolerance on the other side is §17's named regression walking through with the
suite passing. A dearest hour at -0.0005 is an ordinary Belgian day-ahead
print, not a fuzz value.

`app/` IS NOT TOUCHED. `if price < 0` is already correct and changing it would
be the defect; this card closes the check, not the behaviour. The 0.0000 probe
stays and is now labelled as the half that pins the boundary from above.

The new probe is a SWEEP rather than a point, because one probe at -0.0005
closes `-0.001` and nothing else - a mutation reaching for `-0.01` would be the
identical defect one decimal further out. Five prices span four orders of
magnitude down to -0.000001, the last place the canonical six-decimal format
can express; each is asserted to survive that rounding as strictly negative,
to still be the DEAREST hour, and to still clear the widened threshold, so §17
is the only rung that can refuse. The same five magnitudes with the sign
flipped all discharge, so this is a boundary at exactly zero rather than a
refusal of small numbers.

AC4, the look: every price/quantity boundary in arbitrage.py and
static_tariff.py was given the same tolerance mutation, both directions where
both exist. Two survive, both in static_tariff.py and both NAMED RATHER THAN
FIXED - they are TARIFF-03's rungs and want probes in its own file. `spread ==
0.0` loosened to `<= 0.001` flips a real 0.0005 spread from allowed to refused
at min_spread_eur_kwh=0.0, a documented supported value, and otherwise prints a
false reason about a tariff that is not flat. `len(firm) < 2` loosened to `< 1`
survives at +0 bytes - TEST-15's risky delta - and is masked only by the flat
rung, so the two mask each other and a later edit to either opens a hole
nothing reports.

Mutation verified under `python -B` with a recursive __pycache__ purge, byte
delta printed (+5 B), the mutated line asserted changed, and - per the
reviewer's addition to TEST-15 - the tree re-read from disk and asserted to be
the tree that was mutated before each run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VXjA3LWccN1rJsk3okyPC
2026-08-31 02:07:56 +02:00
admin 8e589fee3f OPT-04: price arbitrage, at priority 3, shipped switched off (#22)
app/arbitrage.py consumes TARIFF-02s canonical prices, TARIFF-03s Arbitrage verdict and FCST-03s firm/provisional split rather than re-implementing them. An idle decision files NO claim - a 0 W set at P_ARBITRAGE would out-rank the loop and kill self-consumption. Peak shaving is honoured twice: TARIFF-03s required peak_guard_active interlock, and P_PEAK 50 clamping P_ARBITRAGE 30 in the arbiter. The fetch lives in run_tariff_refresh(), asserted on the shipped AST to be absent from cycle() and read_inputs(). Selling into a negative-price hour is refused. arbitrage_enabled ships false.
2026-08-31 01:53:31 +02:00
+118 -1
View File
@@ -378,7 +378,11 @@ check("BUYING in a negative-price hour is still permitted - §17 forbids "
"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.
# 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 "
@@ -387,6 +391,119 @@ check("a sell hour priced at exactly ZERO is not a negative one, so the guard "
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.
# --------------------------------------------------------------------------- #
print()