TARIFF-24 round 2: a usable solar forecast replaces the "no weather" x1.5 (AUDIT-02 F1)
With battery modes on, the price gap a mode needs was always widened x1.5 by Uncertainty.NO_WEATHER, even with a solar forecast read, so a 0.08 day/night gap never traded at the shipped cycle cost. The doubt is now added only when no usable forecast is read (solar_fc is None, the day plan's own test), in the run plan (_price_runs), the charge gate (_mode_roles) and Home's mirror (homeview.trade_verdict). The rule is one helper, arbitrage.weather_uncertainty(); the modes-off trade path (main.py cycle) keeps its literal NO_WEATHER. DOCS "Battery modes" sentence, CHANGELOG 0.19.1 line and the DECISIONS note follow. Tests: test_tariff24_weather.py. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
230ba0b57a
commit
382b0ea411
@@ -147,7 +147,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from .arbiter import ACTUATOR_SITE, P_ARBITRAGE, Claim
|
||||
from .price import Absence, price_option
|
||||
from .price import Absence, Uncertainty, price_option
|
||||
|
||||
_LOG = logging.getLogger("goodwe.arbitrage")
|
||||
|
||||
@@ -267,6 +267,19 @@ def threshold_eur_kwh(opts: dict) -> float:
|
||||
return v
|
||||
|
||||
|
||||
def weather_uncertainty(modes_on: bool, solar_fc) -> frozenset:
|
||||
"""The "no weather" doubt (x1.5) a trade must clear. TARIFF-24.
|
||||
|
||||
Battery modes read a solar forecast; with one read (`solar_fc` not None) the
|
||||
doubt is gone. Everything else, and the modes without a forecast, keep it.
|
||||
One rule for the run plan, the charge gate and Home's mirror of it. See
|
||||
DECISIONS/TARIFF-24-modes-need-the-tariff-task.md.
|
||||
"""
|
||||
if modes_on and solar_fc is not None:
|
||||
return frozenset()
|
||||
return frozenset({Uncertainty.NO_WEATHER})
|
||||
|
||||
|
||||
def soc_bounds(opts: dict) -> tuple:
|
||||
"""(floor, ceiling) in percent. The reserve arbitrage may not spend."""
|
||||
return (float(price_option(opts, "arbitrage_soc_floor")),
|
||||
|
||||
@@ -22,7 +22,7 @@ from . import arbitrage, messages, peakguard, uitext
|
||||
from .maintenance import CHARGE as M_CHARGE, DRAIN as M_DRAIN, HOLD as M_HOLD
|
||||
from .maintenance import IDLE as M_IDLE, next_slot_start
|
||||
from .packmode import CHEAP_HOURS, GRID_CHARGE_MODES, LEFTOVER_SOLAR, SOLAR_FIRST
|
||||
from .price import Absence, Uncertainty, brussels, _cover_table
|
||||
from .price import Absence, brussels, _cover_table
|
||||
from .static_tariff import static_plan
|
||||
from .tariff import BILLING_TZ
|
||||
from .uimodel import REGION_PRESETS, options_to_ui
|
||||
@@ -277,7 +277,9 @@ def trade_verdict(c, plan, now):
|
||||
return None, "no_prices"
|
||||
verdict = plan.arbitrage(peak_guard_active=guard,
|
||||
min_spread_eur_kwh=arbitrage.threshold_eur_kwh(c.o),
|
||||
uncertainty=frozenset({Uncertainty.NO_WEATHER}),
|
||||
uncertainty=arbitrage.weather_uncertainty(
|
||||
bool(getattr(c, "modes_on", False)),
|
||||
getattr(c, "solar_fc", None)),
|
||||
now=now)
|
||||
return verdict.match(
|
||||
allowed=lambda v: (v, "allowed"),
|
||||
|
||||
@@ -2250,7 +2250,7 @@ class Controller:
|
||||
required = price_required_spread(
|
||||
arbitrage.threshold_eur_kwh(self.o),
|
||||
price_uncertainty_of(plan.read.product)
|
||||
| {Uncertainty.NO_WEATHER})
|
||||
| arbitrage.weather_uncertainty(True, self.solar_fc))
|
||||
ref = self._ref_cheap
|
||||
if ref is not None and now - ref.end > REF_CHEAP_KEEP:
|
||||
ref = self._ref_cheap = None
|
||||
@@ -2329,7 +2329,7 @@ class Controller:
|
||||
return plan.arbitrage(
|
||||
peak_guard_active=active,
|
||||
min_spread_eur_kwh=arbitrage.threshold_eur_kwh(self.o),
|
||||
uncertainty=frozenset({Uncertainty.NO_WEATHER}),
|
||||
uncertainty=arbitrage.weather_uncertainty(True, self.solar_fc),
|
||||
now=now).match(
|
||||
allowed=lambda _v: True, refused=lambda _v: False)
|
||||
if guard is not None and plan is not None and runs is not None:
|
||||
|
||||
Reference in New Issue
Block a user