TARIFF-18: price_taxes_eur_kwh stops shipping at half the real figure
checks / unit (pull_request) Successful in 7m48s

`price_taxes_eur_kwh` shipped at 0.06 EUR/kWh with no recorded provenance -
no card, no date, no component list - and roughly HALF the true Flemish
all-in non-energy figure. It now ships at 0.11736, and the rows it is made
of are in the code rather than in this message:

  Fluvius distribution (Antwerpen)              5,35   c EUR/kWh
  Bijzondere accijns, "Alle verbruik"           4,876
  Bijdrage groene stroom en WKK Vlaanderen      1,51
  Energiebijdrage (0 EUR/MWh since 2026-08-01)  0
                                               ------
                                               11,736 c EUR/kWh = 0.11736

Eneco residential card for Flanders, September 2026, incl. 6 % VAT.
`app/price.py`'s FLEMISH_TAXES_COMPONENTS_EUR_KWH carries the rows and
test_price.py section 1b re-adds them - and also compares the total against
the card's own figures typed in the test file, so an edit that moved the
rows and the default together is still caught.

WHAT THIS IS NOT. It is not an arbitrage-correctness fix and it cannot be.
This scalar is a LEVEL component: an additive constant cancels exactly in
`max - min` and leaves the argmin where it was. TARIFF-17 shipped that proof
and it is unchanged - test_price.py 16b drives six flat scalars spanning and
exceeding the whole per-DSO range and gets the identical cheapest hour and
the identical spread to the last digit. DOCS and the CHANGELOG say so in
those words.

WHAT IT IS. A display and guard fix:

  * every number an operator reads, which at half the true value made the
    add-on look considerably better than it is; and
  * §17's negative-price guard in arbitrage.py - the one rung that tests an
    ABSOLUTE all-in price rather than a difference. It fires when
    `spot < -(markup + taxes)`, so its boundary moves -0.09 -> -0.14736
    EUR/kWh of spot. test_arbitrage.py section 2c drives both sides through
    the shipped decide() and pins the boundary from below as well.

The default is Antwerpen's real row, near the LOW end of the 0.117-0.131
per-DSO band, and that is deliberate: the error directions are not
symmetric. Too high carries a genuinely negative hour above zero and the
add-on exports into paid-for disposal - §17's own case. Too low only
declines a discharge that would have paid.

TARIFF-17's INTERACTION, WHICH HAD TO BE DECIDED. Nothing in that rung
hardcoded 0.06; it read PRICE_OPTION_DEFAULTS, so moving the default moves
what it refuses. The Supervisor sets `App.options = deepmerge(config.yaml
defaults, stored options)`, so a saved blob carries the key whether it was
typed or not - every stored 0.06 became "non-default" with nobody editing
anything, and a working Walloon or Brussels site with a taxes table would
have met SystemExit(78) on the upgrade over an inert number. price.py now
carries SUPERSEDED_TAXES_DEFAULTS_EUR_KWH = (0.06,) and validate.py reads a
superseded default as untouched too. The rung is NOT weakened: 0.1198 beside
a table is still refused by name, and the refusal now names both figures.

config.yaml and PRICE_OPTION_DEFAULTS moved together; CFG-03's tripwire
(test_price.py section 1, test_defaults_pin.py) stayed green by agreeing.
test_price.py section 0's fourth check was REWRITTEN rather than re-pointed:
it used to justify the file's own grading constants by observing that the
shipped pair was degenerate (0.06 is exactly twice 0.03). TARIFF-18 ends
that coincidence, so the sentence is no longer true and is not kept.

29/29 test files green. Mutation ledger on the PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
glenn schrooyen
2026-09-13 16:24:56 +02:00
co-authored by Claude Opus 5
parent 7796f168a9
commit 9c65ac2cf8
15 changed files with 671 additions and 58 deletions
+63 -4
View File
@@ -18,6 +18,8 @@ import asyncio
import sys
from app.maintenance import MAINT_OPTION_DEFAULTS, MaintConfig
from app.price import (
PRICE_OPTION_DEFAULTS, SUPERSEDED_TAXES_DEFAULTS_EUR_KWH)
from app.control import LOOP_TICK_S, PLANT_SETTLE_S, SLEW_RAIL_FRAC
from app.validate import (
ACTUATOR_WRITE_KEYS, FIRMWARE_INGEST_VETO_S, ConfigRefused,
@@ -1378,6 +1380,8 @@ print("TARIFF-17 - the taxes period table is refused HERE, at startup, and not "
# healthy fixed-tariff day. That is `static_profile`'s argument, one option
# over, and this is the same rule.
_T17_DAG, _T17_NACHT = 0.1327, 0.0739
_T18_SHIPPED_TAXES = PRICE_OPTION_DEFAULTS["price_taxes_eur_kwh"]
_T18_SUPERSEDED = tuple(SUPERSEDED_TAXES_DEFAULTS_EUR_KWH)
_T17_OK = ('{"periods":['
'{"name":"dag","from":"07:00","to":"22:00","days":"mon-fri",'
f'"eur_per_kwh":{_T17_DAG}}},'
@@ -1427,11 +1431,66 @@ check("the table AND a non-default price_taxes_eur_kwh is refused, and the "
and why(_T17_P_BOTH, "THE TABLE WINS"))
check("...and the SHIPPED scalar beside the table is NOT refused, or every "
"Walloon site would have to clear a key it never touched. The refusal is "
"about a value the operator chose, and 0.06 is the one they did not",
validate_startup_config(base(price_taxes_profile=_T17_OK,
price_taxes_eur_kwh=0.06),
-5000, 5000) == []
"about a value the operator chose, and the shipped default is the one "
"they did not",
validate_startup_config(
base(price_taxes_profile=_T17_OK,
price_taxes_eur_kwh=_T18_SHIPPED_TAXES),
-5000, 5000) == []
and len(_T17_P_BOTH) == 1)
# ⚠️ TARIFF-18 - "UNTOUCHED" IS A SET, AND THIS IS THE HALF THAT DECIDES
# WHETHER AN UPGRADE STOPS A RUNNING SITE. The Supervisor sets `App.options =
# deepmerge(config.yaml defaults, stored options)`, so a saved blob carries
# every schema key whether the operator typed it or not - the reference
# install's export is 70 of 71 (CHANGELOG 0.9.0). When TARIFF-18 moved this
# default 0.06 -> 0.11736, every such blob went from "equal to the shipped
# default" to "not equal to it" WITHOUT ANYBODY EDITING ANYTHING. A site that
# also carries a taxes table would have met `SystemExit(78)` on the upgrade,
# over a number it never typed and that decides nothing. So a SUPERSEDED
# default still reads as untouched.
#
# ⚠️ AND THE PRECONDITION FIRST (MUTATION.md habit 1): if the shipped default
# and the superseded one were ever the same number, the two checks below would
# be one check written twice, and dropping the superseded set entirely would
# leave both green.
check(f"the shipped taxes default {_T18_SHIPPED_TAXES} and the superseded "
f"{_T18_SUPERSEDED} are DIFFERENT numbers, so 'the shipped one is "
f"accepted' and 'the superseded one is accepted' are two claims",
_T18_SHIPPED_TAXES not in _T18_SUPERSEDED and len(_T18_SUPERSEDED) >= 1)
check(f"...and the superseded set is non-empty ({_T18_SUPERSEDED}), or the "
f"loop below quantifies over nothing and passes on a tree that dropped "
f"the set entirely - UX-01 AC 4's vacuous clause, one file over",
len(_T18_SUPERSEDED) >= 1)
for _old in _T18_SUPERSEDED:
check(f"...and a blob that still carries the SUPERSEDED default {_old} "
f"beside the table STARTS. That value was written into the options "
f"file by this add-on, not typed by the operator, so refusing it "
f"would stop a working site on an upgrade over an inert number",
validate_startup_config(base(price_taxes_profile=_T17_OK,
price_taxes_eur_kwh=_old),
-5000, 5000) == [])
# ⚠️ AND THE RUNG IS STILL A RUNG. A superseded default is not a licence for
# any value: 0.1198 is ORES's enkelvoudig rate, a number an operator plausibly
# types, and it is still refused beside the table. Without this line the set
# above could be widened to "anything" and nothing would report.
check("...while a value that is NEITHER the shipped default NOR a superseded "
"one is still REFUSED beside the table, naming it - the set of "
"'untouched' figures widened by exactly the old defaults and by nothing "
"else",
why(_T17_P_BOTH, "price_taxes_profile is set AND price_taxes_eur_kwh is "
"0.1198")
and 0.1198 not in _T18_SUPERSEDED
and abs(0.1198 - _T18_SHIPPED_TAXES) > 1e-9)
# ⚠️ AND THE REFUSAL SAYS SO, so an operator who HAS typed a number is told
# both which figure to return to and that the old default is not the problem.
check("...and the refusal names the SHIPPED default to return to AND says the "
"superseded default is accepted, so the sentence is actionable on both "
"sides of the upgrade",
bool(_T18_SUPERSEDED)
and why(_T17_P_BOTH, f"back to {_T18_SHIPPED_TAXES}")
and why(_T17_P_BOTH, "SUPERSEDED")
and all(why(_T17_P_BOTH, str(u)) for u in _T18_SUPERSEDED))
check("...and the same non-default scalar WITHOUT the table still starts, "
"which is what makes the line above about the PAIR rather than about "
"0.1198 being disliked",