"""Decimal-string discipline (docs/11 §3.3) at the skill boundary. Contracts carry money/energy/prices as fixed-point decimal *strings*. Inside a skill we compute with numpy floats, then quantize back to strings with an explicit scale — and any headline figure (revenue, energy) is re-derived with ``decimal.Decimal`` from the already-quantized curves, so what a report cites is exactly reproducible from the curves it cites. """ from __future__ import annotations from decimal import ROUND_HALF_EVEN, Decimal from typing import Iterable import numpy as np INTERVALS = 96 INTERVAL_HOURS = Decimal("0.25") SCALE_MW = 3 SCALE_MWH = 3 SCALE_PRICE = 2 SCALE_MONEY = 2 def to_array(values: Iterable[str]) -> np.ndarray: return np.array([float(Decimal(v)) for v in values], dtype=float) def quantize(x: float | Decimal, scale: int) -> str: q = Decimal(1).scaleb(-scale) d = x if isinstance(x, Decimal) else Decimal(repr(float(x))) out = d.quantize(q, rounding=ROUND_HALF_EVEN) if out == 0: out = abs(out) # normalise "-0.000" return format(out, "f") def curve(values: np.ndarray | list[str], date: str, scale: int) -> dict: if isinstance(values, np.ndarray): vals = [quantize(float(v), scale) for v in values.tolist()] else: vals = list(values) if len(vals) != INTERVALS: raise ValueError(f"curve must have {INTERVALS} values, got {len(vals)}") return {"interval_minutes": 15, "date": date, "values": vals} def dsum(values: Iterable[str]) -> Decimal: return sum((Decimal(v) for v in values), Decimal(0)) def dot(a: Iterable[str], b: Iterable[str]) -> Decimal: """Exact Σ a_i·b_i over decimal strings.""" return sum((Decimal(x) * Decimal(y) for x, y in zip(a, b, strict=True)), Decimal(0))