TARIFF-14: the point whose own interval covers the instant
checks / unit (pull_request) Failing after 2s
checks / unit (pull_request) Failing after 2s
`_Series.at()` asked `s <= u < s + 1 h` of every point IN LIST ORDER and
returned the FIRST match. On the hourly shape every fixture in this
repository carried before TARIFF-08 that is right by accident. Belgium
moved to a 15-minute MTU on 2025-10-01 and the live day-ahead feed
returns 96 points at 900 s, where it is wrong twice over - measured
against the shipped code at 5059e28:
series: 02:00=0.10 02:15=0.11 02:30=0.12 02:45=0.13 03:00=0.14
ask +15min -> 02:00 0.10 <-- WRONG (wanted 02:15)
ask +60min -> 02:15 0.11 <-- WRONG (wanted 03:00)
ask +75min -> 02:30 0.12 <-- WRONG (wanted 03:15)
Inside the first hour the :00 point wins because its hour has not run
out. From the second hour on the answer is not even the top of the hour
- the first point still inside its own hour wins - so the error
compounds with depth into the series.
`arbitrage._act()` tests `here.start == v.cheapest.start` /
`== v.dearest.start` to decide whether this interval is the buy one or
the sell one. That identity almost never held, so the strategy idled
through exactly the interval it exists to trade, and read §8.3's
`here.provisional` and §17's `price < 0` off the same wrong point.
`static_tariff.py` emits hourly points, so static sites were never
affected; this is reachable only down the dynamic path TARIFF-08/09
made work.
THE RULE: a point covers from its own start until the next point's
start, and never more than one hour. Quarter-hourly takes 900 s,
hourly is unchanged, and a MIXED series - FCST-04's carry-forward puts
hourly carried points beside quarter-hourly firm ones, spacing
[900, 3600] in 70 % of soak samples - takes each point's OWN gap.
The one-hour cap is load-bearing: FCST-04 leaves an hour OUT when
nothing published it and yesterday has no value, and `at()` raising
Absence for it is what sends the caller to the static profile (§11.4).
Uncapped, "until the next point's start" would stretch the point before
a three-hour hole across the whole hole and answer every instant in it
with a stale price.
THE LAST POINT is given the same span as the point before it (one hour
when it is the only point). A flat one-hour tail would answer 00:30
TOMORROW with today's 23:45 price. The cost - a mixed series ending at
the firm/carried seam loses its last 45 minutes to Absence - is argued
in DECISIONS/TARIFF-14-the-window-a-point-covers.md §2 rather than
buried; the direction is the safe one.
`at()` now reads a point's NEIGHBOURS, so it would depend on
sortedness. It establishes that itself: the lookup index is sorted by
UTC start once, at construction, so no caller's list order reaches it.
A series handed its 96 points in reverse answers identically, and that
is a check. `covers()` shared the defect exactly - it has no rule of
its own, it asks `at()` - and is fixed by the same change; `window()`
did not, and its docstring now says why.
Nothing about §9.3 or a point's `start` changed. PricePoint still has
three fields; the span is derived from the neighbours, never stored.
Graded by test_price.py section 13 (900 s, 3600 s, mixed, holes, the
tail, caller order, duplicate starts, covers()/window(), the no-data
contract) and end to end by test_arbitrage.py section 10, which drives
decide() over a 96-point quarter-hourly day whose cheapest and dearest
quarters are deliberately NOT on a whole hour, plus §17, §8.3 and the
mixed series. Eight mutations, all red, including a ZERO-BYTE one on
the tail rule; byte deltas and what the arbitrage file could not see
are in the decision note §6. All 29 test files green.
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
88c59a9918
commit
7f2c4e20e1
@@ -1376,6 +1376,312 @@ check("soc_bounds() returns the shipped pair, floor first",
|
||||
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(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_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_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_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_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_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(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])
|
||||
check("...so the mixed day CHARGES and DISCHARGES in its firm quarters, at "
|
||||
"their own prices",
|
||||
decide(MIX_PLAN, _mix_buy_t, peak_guard_active=False, 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).action == CHARGE
|
||||
and decide(MIX_PLAN, _mix_sell_t, peak_guard_active=False, 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).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_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:
|
||||
|
||||
Reference in New Issue
Block a user