nerezta PV A pri prodeji z baterie
Some checks failed
CI and deploy / migration-check (push) Failing after 28s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-26 07:34:52 +02:00
parent 25c864db61
commit 8494ea26de
6 changed files with 166 additions and 11 deletions

View File

@@ -33,6 +33,10 @@ Příklad (čistá arbitráž, nákup = spot + fixní adder + distribuce/poplatk
--buy-distribution-kwh 1.80 --buy-other-fees-kwh 0.20 --buy-vat-multiplier 1.21 \\
... (ostatní jako výše)
Příklad (nákup jako home-01, prodej spot 0,30 dle živé DB):
python3 scripts/analysis/battery_sizing_screen.py --db \\
--buy-home-01 --sell-margin-fixed -0.30 ... (ostatní jako výše)
Vyžaduje: pip install pulp (volitelně psycopg2 pro --db).
Omezení modelu: FVE buď syntetický denní tvar (--pv-daily-kwh-*), nebo součet měsíčních
@@ -81,6 +85,24 @@ class SiteLimits:
soc_max_frac: float = 0.95
# home-01 (živá DB site_market_config + V016 tarif/HDO): ověř MCP před změnou
HOME01_BUY_MARGIN_FIXED_KWH = 0.0
HOME01_BUY_MARGIN_PCT = 9.0
HOME01_SYSTEM_SERVICES_KWH = 0.192
HOME01_OTE_FEE_KWH = 0.001
HOME01_DIST_NT_KWH = 0.2243
HOME01_DIST_VT_KWH = 0.74987
HOME01_VAT_MULTIPLIER = 1.21
HOME01_SELL_MARGIN_FIXED_KWH = -0.30
# VT okna Europe/Prague (každý den): 0910, 1213, 1617, 2021
HOME01_VT_SLOT_MINUTES: tuple[tuple[int, int], ...] = (
(9 * 60, 10 * 60),
(12 * 60, 13 * 60),
(16 * 60, 17 * 60),
(20 * 60, 21 * 60),
)
@dataclass(frozen=True)
class BuyPricingConfig:
mode: str = "flat"
@@ -210,6 +232,35 @@ def effective_buy_spot_asym_pct_kc_kwh(raw_ote: float, asym_pct: float) -> float
return raw_ote * (1.0 - asym_pct / 100.0)
def slot_is_vt_home01(slot_index: int) -> bool:
"""15min slot 0..95 od půlnoci (Europe/Prague) — VT okna jako u home-01 HDO."""
mins = (slot_index // 4) * 60 + (slot_index % 4) * 15
for start_m, end_m in HOME01_VT_SLOT_MINUTES:
if start_m <= mins < end_m:
return True
return False
def effective_buy_home01_kc_kwh(raw_ote: float, is_vt: bool) -> float:
"""Stejná struktura jako ems.fn_effective_buy_price pro home-01 (spot, buy_margin_percent=0)."""
dist = HOME01_DIST_VT_KWH if is_vt else HOME01_DIST_NT_KWH
if HOME01_BUY_MARGIN_PCT != 0.0:
if raw_ote >= 0:
energy = raw_ote * (1.0 + HOME01_BUY_MARGIN_PCT / 100.0)
else:
energy = raw_ote * (1.0 - HOME01_BUY_MARGIN_PCT / 100.0)
else:
energy = raw_ote
subtotal = (
energy
+ dist
+ HOME01_SYSTEM_SERVICES_KWH
+ HOME01_OTE_FEE_KWH
+ HOME01_BUY_MARGIN_FIXED_KWH
)
return subtotal * HOME01_VAT_MULTIPLIER
def build_buy_prices_96(raw_ote_96: Sequence[float], cfg: BuyPricingConfig) -> list[float]:
fixed_fees_kwh = cfg.distribution_kwh + cfg.other_fees_kwh
if cfg.mode == "spot_add_fixed":
@@ -240,6 +291,11 @@ def build_buy_prices_96(raw_ote_96: Sequence[float], cfg: BuyPricingConfig) -> l
cfg.nt_to_hour,
)
]
if cfg.mode == "home01":
return [
effective_buy_home01_kc_kwh(px, slot_is_vt_home01(t))
for t, px in enumerate(raw_ote_96)
]
if cfg.mode != "flat":
raise ValueError(f"Neznámý buy mode: {cfg.mode}")
return [(cfg.flat_kwh + fixed_fees_kwh) * cfg.vat_multiplier] * SLOTS_PER_DAY
@@ -597,6 +653,11 @@ def main() -> None:
default=None,
help="Základ nákupu = raw OTE × (1 + p/100) pro raw >= 0, raw OTE × (1 - p/100) pro raw < 0",
)
ap.add_argument(
"--buy-home-01",
action="store_true",
help="Nákup jako home-01 z živé DB (spot ×1.09/×0.91, dist NT/VT HDO, SS+OTE, DPH 21 %)",
)
ap.add_argument(
"--buy-distribution-kwh",
type=float,
@@ -647,9 +708,13 @@ def main() -> None:
args.buy_nt_kwh is not None,
args.buy_spot_add_fixed_kwh is not None,
args.buy_spot_asym_pct is not None,
args.buy_home_01,
]
if sum(base_buy_modes) > 1:
ap.error("Zvol jen jeden režim základu nákupu: flat, NT/VT, --buy-spot-add-fixed-kwh nebo --buy-spot-asym-pct")
ap.error(
"Zvol jen jeden režim nákupu: flat, NT/VT, --buy-spot-add-fixed-kwh, "
"--buy-spot-asym-pct nebo --buy-home-01"
)
if args.buy_vat_multiplier <= 0:
ap.error("--buy-vat-multiplier musí být > 0")
if args.solver_time_limit_sec < 0:
@@ -686,7 +751,9 @@ def main() -> None:
if args.pvgis_csv:
monthly_ed = merge_pvgis_monthly_ed_kwh([Path(p) for p in args.pvgis_csv])
if args.buy_spot_add_fixed_kwh is not None:
if args.buy_home_01:
buy_cfg = BuyPricingConfig(mode="home01")
elif args.buy_spot_add_fixed_kwh is not None:
buy_cfg = BuyPricingConfig(
mode="spot_add_fixed",
spot_add_fixed_kwh=args.buy_spot_add_fixed_kwh,
@@ -762,6 +829,15 @@ def main() -> None:
f" Nákup = raw OTE × (1 + {args.buy_spot_asym_pct}/100) pro raw >= 0, "
f"raw OTE × (1 - {args.buy_spot_asym_pct}/100) pro raw < 0"
)
elif buy_cfg.mode == "home01":
print(
" Nákup = home-01 (MCP): raw OTE ×(1+9%) / ×(1-9%) + dist NT/VT dle HDO + "
f"{HOME01_SYSTEM_SERVICES_KWH} SS + {HOME01_OTE_FEE_KWH} OTE, ×{HOME01_VAT_MULTIPLIER} DPH"
)
print(
f" distribuce NT {HOME01_DIST_NT_KWH} / VT {HOME01_DIST_VT_KWH} Kč/kWh; "
"VT 0910, 1213, 1617, 2021; prodej typicky sell_margin_fixed -0.30"
)
else:
print(f" Nákup = flat {args.buy_vat_kwh} Kč/kWh")
if args.buy_distribution_kwh or args.buy_other_fees_kwh: