MAINT-20 part 1: custom_formula.py comments cut; new TARIFF-16 note
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
a3f725e8a4
commit
eda6d0aed1
@@ -0,0 +1,102 @@
|
||||
# TARIFF-16 - the injection leg of a dynamic contract (`price_export_formula`)
|
||||
|
||||
**Status: SHIPPED.** Tests: `test_price.py` (17 series). Written down by
|
||||
MAINT-20 from `app/custom_formula.py` and `app/price.py`. Follows [[TARIFF-15]],
|
||||
whose parser it reuses.
|
||||
|
||||
## Why a formula, not the scalar
|
||||
|
||||
`price.py` modelled injection as one scalar. The market publishes a line a
|
||||
scalar cannot hold:
|
||||
|
||||
Eneco Zon & Wind Dynamisch 0,1 x BELPEX-H - 1,188 c EUR/kWh
|
||||
ENGIE Dynamic (B x eSpot_15) - A
|
||||
Aspiravi (non-dynamic) 0,07 x Belpex - 2
|
||||
|
||||
1. Its own factor (Eneco 0,102 on consumption, 0,100 on injection; Aspiravi 0,07
|
||||
against about 0,10): not the consumption `B` with a sign flip.
|
||||
2. A negative offset (`- 1,188`, `- 2`); `price_markup_eur_kwh: float(0,1)`
|
||||
cannot express one.
|
||||
3. VAT-exempt ("Injectietarieven zijn vrijgesteld van btw") against 6 % on
|
||||
consumption: representable only because `vat_pct` is per spec.
|
||||
4. It goes negative with no floor: Eneco's is below zero for any BELPEX-H under
|
||||
11,88 EUR/MWh, the hours a PV household exports in. You can be charged to
|
||||
export.
|
||||
|
||||
`price_export_formula` is `custom_formula`'s parser with one argument moved
|
||||
(`option`, a row of `_LEGS`), as TARIFF-17 did for `parse_static_profile()`. A
|
||||
second parser would be a second `_finite()`, unknown-key rule, unit table and
|
||||
`to_resolution()`, diverging first on the 23- and 25-hour days. Every earlier
|
||||
call site passes no `option` and reads byte-identical sentences. The flat scalar
|
||||
`export_price_eur_kwh()` is untouched and still answers when the option is "".
|
||||
|
||||
## What it does not affect
|
||||
|
||||
`arbitrage.py` does not read the injection price at all (9.3: near-zero Belgian
|
||||
injection; it prices the sell side as displaced import), so no trade today turns
|
||||
on this number and it is not a wrong-trade fix. It changes what an operator is
|
||||
told and what a future export-aware strategy (OPT-06) rests on.
|
||||
|
||||
## The card cannot always grade it
|
||||
|
||||
Eneco's injection formula is printed with no price beside it. The `card` block
|
||||
is required on the consumption leg and optional here (`FormulaSpec.card_graded`
|
||||
is how callers ask; `card_discrepancy()` raises on an ungraded spec rather than
|
||||
returning 0.0). The self-check runs wherever a printed estimate exists, and
|
||||
validate.py says at startup that a leg is ungraded. Inventing an estimate would
|
||||
grade the arithmetic against itself (MUTATION.md habit 4).
|
||||
|
||||
## `implausible_injection()`: bounds on the coefficients, not the price
|
||||
|
||||
The consumption band (`implausible_all_in()`) cannot be reused: it adds
|
||||
`price_taxes_eur_kwh`, a consumption component, and its floor `energy >= 0.02`
|
||||
would refuse the real market's negative hours. But that floor catches a whole
|
||||
card read 100x too small, which the card check cannot see (it cancels). A band
|
||||
spanning zero cannot catch 0.00132 EUR/kWh, so the bound moves onto the
|
||||
coefficients, index-free and sign-free:
|
||||
|
||||
- pass-through `factor x UNITS[units] / 0.001` (one EUR/MWh is 0.001 EUR/kWh):
|
||||
1.00 for Eneco's injection leg, 0.70 for Aspiravi's; 100x off reads 0.01 or 100;
|
||||
- offset magnitude, on the absolute value (1,188 c = 0.01188; a hundredfold error
|
||||
is 1.188 EUR/kWh).
|
||||
|
||||
Two rungs can both answer one stimulus, so they are graded on their reasons and
|
||||
mutated together (MUTATION.md, TEST-16); test_price.py 17d builds fixtures so
|
||||
each is the only one firing. It bounds the shape of the coefficients, never
|
||||
their truth.
|
||||
|
||||
## The consumption band's asymmetry (`implausible_all_in()`)
|
||||
|
||||
The low end is graded on the energy component and the high end on the all-in
|
||||
figure. `price_taxes_eur_kwh` ships at 0.11736 (TARIFF-18; 0.06 before), nearly
|
||||
six times the low bound, so a card read 100x too small (0.0016 EUR/kWh) is
|
||||
carried over the floor by the flat tax addend alone and would pass a band applied
|
||||
to the all-in total. Raising the default made the trap wider. The ceiling is on
|
||||
the all-in figure because that is what the site is billed. It grades the card's
|
||||
own point, not every hour: live EPEX hours may fall outside (a negative hour is
|
||||
the best charging opportunity of the year, 17 keeps a regression case). It is
|
||||
called from validate.py only, since it reads the framework-owned taxes number.
|
||||
|
||||
## Smaller rules
|
||||
|
||||
- `CARD_TOLERANCE` is 0.01 in the card's units, the last digit both real cards
|
||||
print (Eneco 15,55 against 15.5471; ENGIE 7,522 against 7.5218). Every error
|
||||
class it exists for is at least two orders of magnitude bigger.
|
||||
- The card age warning (about 6 months) is never a refusal: ENGIE freezes a
|
||||
contract's coefficients, so an old card can be exactly right.
|
||||
- `units` is required with no default (a default is the factor-1000 class).
|
||||
A zero factor is a fixed tariff in a dynamic one's clothes (every spread 0,
|
||||
arbitrage never fires; 14's warning box), so the operator is pointed at the
|
||||
first-class way to say one price. A negative factor inverts the market (charge
|
||||
into the evening peak). `vat_pct: 0` is legitimate (injection is exempt; some
|
||||
cards are quoted VAT-inclusive); the bound exists for the 106 / 1.06 / 6
|
||||
confusion. An unknown key is refused, not ignored, so a user who types "expr"
|
||||
is not priced on the fields they also typed. A non-finite factor would make
|
||||
every price NaN; `True` is an `int` and would price the day at the index.
|
||||
- `to_resolution()`: the bucket is a UTC hour, exact for Belgium (CET and CEST
|
||||
are whole-hour offsets, so keying by UTC cannot collide the two 02:00s). It
|
||||
decides how many points are emitted, not how long one covers ([[TARIFF-14]]). An
|
||||
incomplete hour is averaged over what is there, not dropped or zero-filled.
|
||||
- The startup preview prints the index that went in beside the price that came
|
||||
out (`61.96 -> 0.13522`): every error this module refuses is invisible on the
|
||||
status page, and a factor of 1000 is one glance there.
|
||||
Reference in New Issue
Block a user