TARIFF-18: price_taxes_eur_kwh stops shipping at half the real figure
checks / unit (pull_request) Successful in 7m48s
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:
co-authored by
Claude Opus 5
parent
7796f168a9
commit
9c65ac2cf8
@@ -527,6 +527,124 @@ check(f"...while THE SAME MAGNITUDES with the sign flipped all DISCHARGE, so "
|
||||
# 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")
|
||||
|
||||
Reference in New Issue
Block a user