v2: denní SoC bezpečnostní rampa — ráno dotáhnout rezervu, pak prodávat
KV1 pozorování uživatele: ráno baterie na 11 % (min 10), prodává se do sítě — nenadálý odběr/mrak by se kupoval za fixních 6.35. v1 mělo denní rampu (safety_soc_target_wh z R__063: reserve 30 % ráno → reserve+noc večer, 6-19 h, flag planner_daytime_charge_target_enabled) — v2 ji ignoroval. Mechanismus (vzor nočního polštáře): deficit pod rampou platí za KAŽDÝ slot nájem buy×faktor (V091 asset_battery.planner_safety_soc_risk_factor, default 0.05; 0=vypnuto) → ráno se nejdřív doplní rezerva (4 h deficitu 1 kWh při buy 6.35 ≈ 5.1 Kč > sell ~2.5), extrémní sell špička smí deficit racionálně podstoupit. R__039 + db_io + 2 testy (KV1 scénář, spike). Eval fixtures beze změny (sloupec v context_json fixtures není → 0); živá produkce dostane faktor přes fn_planning_site_context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,44 @@ class NightReserveTests(unittest.TestCase):
|
||||
self.assertEqual(len(results), 8)
|
||||
|
||||
|
||||
class DaytimeSafetyRampTests(unittest.TestCase):
|
||||
def test_morning_tops_up_reserve_before_selling(self) -> None:
|
||||
# KV1 scénář: ráno baterie u dna, fixní buy 6.35 >> sell 2.5, PV jede;
|
||||
# s rampou (target 30 % usable) musí nejdřív dotáhnout rezervu, ne prodávat
|
||||
bat = _battery()
|
||||
bat.planner_safety_soc_risk_factor = 0.05
|
||||
target_wh = 0.30 * bat.usable_capacity_wh
|
||||
slots = []
|
||||
for i in range(16):
|
||||
s = _slot(_BASE, i, buy=6.35, sell=2.5, pv_a=6000, load=800)
|
||||
s.safety_soc_target_wh = target_wh
|
||||
slots.append(s)
|
||||
results, _, _ = _solve(slots, battery=bat, soc0=0.11 * bat.usable_capacity_wh)
|
||||
soc_pct = [r.battery_soc_target for r in results]
|
||||
first_reach = next((i for i, v in enumerate(soc_pct) if v >= 29.5), None)
|
||||
self.assertIsNotNone(first_reach, "rampa má dotáhnout na rezervu")
|
||||
exported_before = sum(
|
||||
-r.grid_setpoint_w for r in results[:first_reach] if r.grid_setpoint_w < 0
|
||||
)
|
||||
self.assertLess(
|
||||
exported_before, 500 * max(1, first_reach),
|
||||
"před dosažením rezervy se nemá významně prodávat",
|
||||
)
|
||||
|
||||
def test_sell_spike_beats_ramp(self) -> None:
|
||||
# extrémní sell nad buy → deficit je racionální podstoupit
|
||||
bat = _battery()
|
||||
bat.planner_safety_soc_risk_factor = 0.05
|
||||
slots = []
|
||||
for i in range(16):
|
||||
s = _slot(_BASE, i, buy=2.0, sell=14.0, pv_a=2000, load=300)
|
||||
s.safety_soc_target_wh = 0.5 * bat.usable_capacity_wh
|
||||
slots.append(s)
|
||||
results, _, _ = _solve(slots, battery=bat, soc0=0.45 * bat.usable_capacity_wh)
|
||||
total_export = sum(-r.grid_setpoint_w for r in results if r.grid_setpoint_w < 0)
|
||||
self.assertGreater(total_export, 5000, "spike má vyprodat i pod target")
|
||||
|
||||
|
||||
class PvRiskFrontloadTests(unittest.TestCase):
|
||||
def test_neg_window_charges_asap(self) -> None:
|
||||
# sell<0 okno, PV >> load, prázdnější baterie: s frontload prémií musí
|
||||
|
||||
Reference in New Issue
Block a user