"""Market definitions. Both markets are expressed on the common half-hourly grid. A market whose trading block is longer than a half-hour (Market 2, hourly) is described by ``block_half_hours``; the optimiser creates one decision variable per *block* and references it from every half-hour inside that block, so the commitment rule cannot be violated by construction. """ from __future__ import annotations from dataclasses import dataclass import numpy as np MARKET_1 = "market_1" MARKET_2 = "market_2" @dataclass(frozen=True) class Market: """A market the battery can trade in. Attributes: name: Identifier used in output column names. block_half_hours: Number of half-hours a single commitment must span. prices: One price per half-hour, broadcast from the block price. """ name: str block_half_hours: int prices: np.ndarray def __post_init__(self) -> None: if self.block_half_hours < 1: raise ValueError("block_half_hours must be >= 1") if self.prices.ndim != 1: raise ValueError("prices must be one-dimensional") def block_of(self, half_hour: int) -> int: """Index of the trading block containing ``half_hour``.""" return half_hour // self.block_half_hours def slice(self, start: int, stop: int) -> "Market": """A view of this market over half-hours ``[start, stop)``. ``start`` must fall on a block boundary, otherwise the window would cut a trading block in half and the commitment rule would be meaningless. """ if start % self.block_half_hours: raise ValueError( f"{self.name}: window start {start} is not aligned to a " f"{self.block_half_hours}-half-hour block boundary" ) return Market(self.name, self.block_half_hours, self.prices[start:stop])