feat(fase1): hard/soft goal-stop in run_portfolio on accumulated ledger (F1)

This commit is contained in:
Kjell Tore Guttormsen 2026-07-07 08:11:43 +02:00
commit 16b6d80b82
4 changed files with 270 additions and 6 deletions

View file

@ -17,6 +17,7 @@ from __future__ import annotations
import pytest
from portfolio_optimiser.contracts import GoalConfig, GoalContract
from portfolio_optimiser.ledger import (
LedgerEntry,
RealizationRefused,
@ -25,6 +26,7 @@ from portfolio_optimiser.ledger import (
realize,
stamp,
)
from portfolio_optimiser.run import run_portfolio
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict
_TS = "2026-07-06T00:00:00Z"
@ -180,3 +182,53 @@ def test_realize_ore_conversion_is_exact() -> None:
timestamp=_TS,
)
assert entry.amount_ore == 1234567
# --- Step 8: goal-stop is load-bearing (SC6) -----------------------------------------------------
_PORTFOLIO_IDS = ["FV42-GSV-E1", "RV13-RAS-TP", "BRU-LAKS-REHAB"]
def _portfolio_ledger(amount_ore: int) -> SavingsLedger:
led = SavingsLedger()
led.add_realized(
LedgerEntry(
project_id="FV42-GSV-E1",
dimension="energi",
candidate_identity="prior-hitl",
amount_ore=amount_ore,
verdict_id="v-prior",
provenance=stamp(approver="ekspert", experiment="earlier", timestamp=_TS),
)
)
return led
async def test_goal_stop_is_load_bearing(make_portfolio_client_factory) -> None:
"""LOAD-BEARING (SC6): a reached HARD portfolio goal stops the pass (no project runs); a
below-goal ledger runs the FULL pass. RED if the goal-stop check is detached in run_portfolio
(a reached goal then runs past the target). The control (below goal -> full pass) proves the
stop is CAUSED by the goal being reached, not by the fixture."""
goals = GoalConfig(portfolio=GoalContract(absolute_ore=1000))
stopped = await run_portfolio(
_PORTFOLIO_IDS,
"local",
ledger=_portfolio_ledger(1000), # == goal (>=)
goals=goals,
client_factory=make_portfolio_client_factory({}),
max_rounds=1,
)
assert stopped.stopped_early is True
assert stopped.runs == () # detach -> this becomes 3 (runs past the reached goal)
below = await run_portfolio(
_PORTFOLIO_IDS,
"local",
ledger=_portfolio_ledger(999), # below the goal
goals=goals,
client_factory=make_portfolio_client_factory({}),
max_rounds=1,
)
assert below.stopped_early is False # control: goal not reached -> full pass
assert len(below.runs) == 3