From 59245ee54db7d26796cd49730889a57404623f98 Mon Sep 17 00:00:00 2001 From: Dominik Roth Date: Fri, 9 Oct 2026 10:30:46 +0200 Subject: [PATCH] whatif.py: add --remove for teardown/demolish scenarios Mirrors --add - removes AMOUNT (default: all) of a building from the copied spec. Used this to check whether BHP's HWP is still worth running after its price drop (still +19.8k-28.4k/day marginal, nowhere near a teardown call) without hand-writing two spec files. Co-Authored-By: Claude Sonnet 5 --- tests/test_whatif.py | 12 ++++++++++++ tools/whatif.py | 21 +++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/test_whatif.py b/tests/test_whatif.py index b58f9c2..6f9b0b3 100644 --- a/tests/test_whatif.py +++ b/tests/test_whatif.py @@ -23,6 +23,18 @@ def test_apply_add_appends_new_building_with_recipe(): assert spec["buildings"] == [{"building": "CHP", "amount": 1, "recipes": ["MG,S,CU=>IND"]}] +def test_apply_remove_drops_building_entirely_by_default(): + spec = {"buildings": [{"building": "HWP", "amount": 1, "recipes": ["AL,STL,HE=>BHP"]}]} + whatif.apply_remove(spec, "HWP") + assert spec["buildings"] == [] + + +def test_apply_remove_partial_amount(): + spec = {"buildings": [{"building": "CHP", "amount": 3, "recipes": ["MG,S,CU=>IND"]}]} + whatif.apply_remove(spec, "CHP:1") + assert spec["buildings"] == [{"building": "CHP", "amount": 2, "recipes": ["MG,S,CU=>IND"]}] + + def test_apply_infra_adds_delta(): spec = {"infrastructure": {"HB1": 4}} whatif.apply_infra(spec, "HB1:+1,HB2:+2") diff --git a/tools/whatif.py b/tools/whatif.py index 788ce84..dacf0cc 100644 --- a/tools/whatif.py +++ b/tools/whatif.py @@ -37,6 +37,20 @@ def apply_add(spec: dict, add: str): spec["buildings"].append({"building": ticker, "amount": amount, "recipes": recipes}) +def apply_remove(spec: dict, remove: str): + """BUILDING[:AMOUNT] - a teardown scenario: removes AMOUNT (default: all) of that building + from the plan. For "should we demolish this" questions - pass the current full base as + --after and the --remove result as the plain positional arg (or vice versa; whatif always + reports AFTER minus BEFORE, so order which one is which by what you want the sign to mean).""" + parts = remove.split(":", 1) + ticker = parts[0] + amount = int(parts[1]) if len(parts) == 2 else None + for b in spec.get("buildings", []): + if b["building"] == ticker: + b["amount"] = max(0, b["amount"] - amount) if amount else 0 + spec["buildings"] = [b for b in spec.get("buildings", []) if b["amount"] > 0] + + def apply_infra(spec: dict, infra: str): """HB1:+1,HB2:+2 - deltas added to spec['infrastructure'].""" inf = spec.setdefault("infrastructure", {}) @@ -56,6 +70,7 @@ def main(): ap.add_argument("before", help="full-base plan spec as it stands (or as a baseline you want to compare from)") ap.add_argument("--after", help="full-base plan spec with the addition already in it; skips --add/--infra") ap.add_argument("--add", action="append", default=[], help="BUILDING:AMOUNT[:RECIPE[|RECIPE]], repeatable") + ap.add_argument("--remove", action="append", default=[], help="BUILDING[:AMOUNT] - teardown scenario, removes AMOUNT (default: all), repeatable") ap.add_argument("--infra", action="append", default=[], help="HB1:+1,HB2:+2, repeatable") ap.add_argument("--cx", default=config.DEFAULT_CX) ap.add_argument("--basis", default="uni30", choices=["real", "uni30", "vwap30", "vwap7", "ask", "bid", "mid"]) @@ -66,11 +81,13 @@ def main(): if a.after: after_spec = yaml.safe_load(Path(a.after).read_text()) else: - if not a.add and not a.infra: - sys.exit("give --after, or at least one --add/--infra") + if not a.add and not a.remove and not a.infra: + sys.exit("give --after, or at least one --add/--remove/--infra") after_spec = copy.deepcopy(before_spec) for add in a.add: apply_add(after_spec, add) + for remove in a.remove: + apply_remove(after_spec, remove) for infra in a.infra: apply_infra(after_spec, infra)