CI / test (push) Failing after 45m4s
Implements the package from PLAN.md: config/data loading, the LP-first MILP-fallback rolling-horizon optimiser, battery degradation tracking, independent schedule validation, metrics, plots and the CLI, plus the matching test suite. Adds a Gitea Actions workflow (lint + tests) that posts a pass/fail notification to ntfy on every run, and a .gitignore for build/cache artefacts that had been tracked by mistake. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
56 lines
1.8 KiB
Python
56 lines
1.8 KiB
Python
"""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])
|