Add multi-base chain modelling, fix price-band-aware demand/supply, tranche-split sell tool

- puga/network.py + tools/network.py: model a multi-base chain (e.g. mine LST at one base,
  ship it, consume it at another). Simulates each base independently, nets a transferred
  material's producer-surplus against consumer-need, charges real freight only on what's
  moved, cm_free for a founding covered by a Core Module Kit. Worked example in plans/chains/.
- puga/saturation.py: real price-band filtering. FIO's order_book NarrowPriceBandLow/High and
  WidePriceBandLow/High match APEX's own displayed Price Band exactly (verified live) - an
  order outside it is a stale artifact, not just uncompetitive. Added in_band() and
  effective_demand(); effective_supply() gained the same hard band filter alongside its
  existing soft vwap-proximity filter (renamed that param mult to free up `band`). Wired into
  tools/scan.py stage 2 in place of the raw, unfiltered demand figure. Was flagged as an
  unimplemented refinement in saturation-design.md since the original design review.
- tools/sell.py: self-contained tranche split (aggressive tranche capped at a volume quantile,
  median normally or 80th pct with --tight when a payment is imminent and stockout risk
  outweighs margin; patient tranche priced under the next competitor tier). Instant-bid
  comparison moved behind --show-bid (off by default).
- Docs and roadmap updated accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 14:32:09 +02:00
co-authored by Claude Sonnet 5
parent e2e592cae3
commit 58db08b921
15 changed files with 434 additions and 21 deletions
+62
View File
@@ -0,0 +1,62 @@
"""puga.network.combine(): the transfer/freight logic, with fake simulate() results (no network calls)."""
import pytest
from puga import network
def fake_results(monkeypatch, per_base):
"""per_base: {name: flows dict} -> patches simulate() to return {"flows": ..., "profit": sum handled by caller}."""
def fake_simulate(plan, recipes, buildings, resources, fertility, price, faction=None, permits=(1, 2), built=None):
return {"flows": per_base[plan["_name"]], "profit": per_base[plan["_name"]].pop("_profit")}
monkeypatch.setattr(network, "simulate", fake_simulate)
def node(name):
return network.BaseNode(name, {"_name": name}, [], [], [], 0.0, lambda t, side="both": 100.0)
def test_transfer_fully_matched_only_charges_freight(monkeypatch):
fake_results(monkeypatch, {
"a": {"LST": {"out": 20.0, "inp": 0.0}, "_profit": 1000.0},
"b": {"LST": {"out": 0.0, "inp": 20.0}, "_profit": 500.0},
})
monkeypatch.setattr(network.fio, "materials", lambda: [{"Ticker": "LST", "Weight": 2.73, "Volume": 1.0}])
r = network.combine({"a": node("a"), "b": node("b")}, [network.Transfer("LST", "a", "b", trip_cost=1000, cargo=500)])
t = r["ledger"][0]
assert t["moved"] == 20.0 and t["shortfall"] == 0 and t["surplus"] == 0
assert r["total_profit"] == pytest.approx(1000 + 500 - t["freight"])
def test_transfer_shortfall_is_flagged_not_double_charged(monkeypatch):
fake_results(monkeypatch, {
"a": {"LST": {"out": 5.0, "inp": 0.0}, "_profit": 1000.0},
"b": {"LST": {"out": 0.0, "inp": 20.0}, "_profit": 500.0}, # needs 20, source only makes 5
})
monkeypatch.setattr(network.fio, "materials", lambda: [{"Ticker": "LST", "Weight": 2.73, "Volume": 1.0}])
r = network.combine({"a": node("a"), "b": node("b")}, [network.Transfer("LST", "a", "b", trip_cost=1000, cargo=500)])
t = r["ledger"][0]
assert t["moved"] == 5.0 and t["shortfall"] == 15.0 and t["surplus"] == 0
# b's own 'profit' already priced the missing 15 units at market (simulate()'s job) - combine() must not touch that
def test_transfer_surplus_is_flagged_and_sold_by_source(monkeypatch):
fake_results(monkeypatch, {
"a": {"LST": {"out": 40.0, "inp": 0.0}, "_profit": 1000.0},
"b": {"LST": {"out": 0.0, "inp": 9.0}, "_profit": 500.0},
})
monkeypatch.setattr(network.fio, "materials", lambda: [{"Ticker": "LST", "Weight": 2.73, "Volume": 1.0}])
r = network.combine({"a": node("a"), "b": node("b")}, [network.Transfer("LST", "a", "b", trip_cost=1000, cargo=500)])
t = r["ledger"][0]
assert t["moved"] == 9.0 and t["surplus"] == 31.0 and t["shortfall"] == 0
def test_freight_scales_with_weight_and_cargo_cap(monkeypatch):
fake_results(monkeypatch, {
"a": {"LST": {"out": 100.0, "inp": 0.0}, "_profit": 0.0},
"b": {"LST": {"out": 0.0, "inp": 100.0}, "_profit": 0.0},
})
monkeypatch.setattr(network.fio, "materials", lambda: [{"Ticker": "LST", "Weight": 2.73, "Volume": 1.0}])
r = network.combine({"a": node("a"), "b": node("b")}, [network.Transfer("LST", "a", "b", trip_cost=1000, cargo=100)])
t = r["ledger"][0]
assert t["weight"] == pytest.approx(273.0)
assert t["trips"] == pytest.approx(2.73) # 273 t / 100 t cargo cap
assert t["freight"] == pytest.approx(2730.0)
+19
View File
@@ -38,3 +38,22 @@ def test_effective_supply_ignores_stale_far_asks():
asks = [(100, 10), (110, 20), (150, 1500)]
assert s.effective_supply(asks, 100) == 30 # 1500 units at 1.5x are not competition
assert s.effective_supply(asks, None) == 1530
def test_effective_supply_hard_filters_out_of_band_asks():
asks = [(100, 10), (110, 20), (150, 1500)]
# band excludes the 150 tier outright, even though ref_price=None would otherwise count it
assert s.effective_supply(asks, None, band=(50, 120)) == 30
def test_in_band():
assert s.in_band(700, (663, 16575)) is True
assert s.in_band(334, (663, 16575)) is False
assert s.in_band(20000, (663, 16575)) is False
assert s.in_band(1, None) is True # no band given: nothing excluded
def test_effective_demand_excludes_stale_bids_below_the_band():
bids = [(4550, 8), (4200, 250), (2810, 8), (334, 19)]
assert s.effective_demand(bids, band=(663, 16575)) == 8 + 250 + 8 # the 334 bid is stale, excluded
assert s.effective_demand(bids, band=None) == 8 + 250 + 8 + 19
+34
View File
@@ -0,0 +1,34 @@
import importlib.util
from pathlib import Path
spec = importlib.util.spec_from_file_location("sell", Path(__file__).resolve().parent.parent / "tools" / "sell.py")
sell = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sell)
TRADES = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50] # evenly spaced: median 25, 80th pct 40
def test_quantile_median_and_80th():
assert sell.quantile(TRADES, 0.5) == 25
assert sell.quantile(TRADES, 0.8) == 40
assert sell.quantile([], 0.5) == 0.0
def test_tranche_split_caps_aggressive_at_quantile_not_full_stock():
aggr, patient = sell.tranche_split(60, TRADES, tight=False)
assert aggr == 25 and patient == 35 # median cap
def test_tranche_split_tight_uses_higher_quantile():
aggr, patient = sell.tranche_split(60, TRADES, tight=True)
assert aggr == 40 and patient == 20 # 80th pct cap, bigger aggressive tranche than non-tight
def test_tranche_split_no_split_when_stock_below_quantile():
aggr, patient = sell.tranche_split(11, TRADES, tight=False)
assert aggr == 11 and patient == 0 # whole stock fits under the median, nothing held back
def test_tranche_split_falls_back_to_full_qty_with_no_history():
aggr, patient = sell.tranche_split(8, [], tight=False)
assert aggr == 8 and patient == 0