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
+60
View File
@@ -0,0 +1,60 @@
"""Multi-base chains: combine several independently-simulated plans into one balance sheet.
Each base is simulated on its own (puga.simulate.simulate), which prices every flow at market —
that already gives each base's own correct profit whether or not a material actually leaves the
base. For a material transferred between two bases in the SAME chain (e.g. LST mined at Nike,
consumed at Deimos), the two bases' market-priced flows cancel out in the combined total to within
the transferred quantity (base A "sells" it, base B "buys" it, at the same price basis) — so the
combined total needs only ONE correction: the real freight cost of physically moving it, which
isn't in either base's isolated numbers. Leftover surplus (more produced than a partner needs) is
still validly sold at market by the producing base; leftover deficit is still validly bought.
"""
from dataclasses import dataclass, field
from . import fio
from .simulate import simulate
@dataclass
class BaseNode:
name: str
plan: dict
recipes: list
buildings: list
resources: list
fertility: float
price: callable
faction: str | None = None
permits: tuple[float, float] = (1, 2)
built: dict = field(default_factory=dict)
@dataclass
class Transfer:
material: str
src: str # producing base's name (key in the `bases` dict)
dst: str # consuming base's name
trip_cost: float = 9250.0 # AIC per round trip; PLACEHOLDER until a real route/fuel model exists
cargo: float = 500.0 # t or m3 per trip
def combine(bases: dict[str, BaseNode], transfers: list[Transfer]) -> dict:
mat = {m["Ticker"]: m for m in fio.materials()}
results = {name: simulate(b.plan, b.recipes, b.buildings, b.resources, b.fertility, b.price, b.faction, b.permits, built=b.built)
for name, b in bases.items()}
ledger = []
total_profit = sum(r["profit"] for r in results.values())
for t in transfers:
prod = results[t.src]["flows"].get(t.material, {"out": 0.0, "inp": 0.0})
cons = results[t.dst]["flows"].get(t.material, {"out": 0.0, "inp": 0.0})
avail = prod["out"] - prod["inp"] # net surplus at the source (0 if it's a net consumer)
need = cons["inp"] - cons["out"] # net requirement at the destination
moved = max(0.0, min(avail, need))
m = mat.get(t.material)
weight, volume = moved * (m["Weight"] if m else 0), moved * (m["Volume"] if m else 0)
trips = max(weight, volume) / t.cargo if t.cargo else 0.0
freight = trips * t.trip_cost
total_profit -= freight
ledger.append(dict(material=t.material, src=t.src, dst=t.dst, moved=moved, avail=avail, need=need,
shortfall=max(0.0, need - avail), surplus=max(0.0, avail - need),
weight=weight, volume=volume, trips=trips, freight=freight))
return dict(results=results, ledger=ledger, total_profit=total_profit)
+25 -4
View File
@@ -48,9 +48,30 @@ def p_patient(asks: list[tuple[float, float]], tr: float, produced_per_day: floa
return max(bid, min(p, hi))
def effective_supply(asks: list[tuple[float, float]], ref_price: float | None, band: float = 1.25) -> float:
"""Units of standing sell orders that actually compete: price <= band x reference (vwap7 or ask).
Stale asks far above market (e.g. 1500 units at 1.5x vwap) are not competition. asks = [(price, units)]."""
def in_band(price: float, band: tuple[float | None, float | None] | None) -> bool:
"""band = (low, high), the exchange's tradeable Price Band (FIO order_book's Narrow/WidePriceBandLow/High;
verified live 2026-09-25 to match APEX's own displayed 'Price Band' field exactly). An order outside it is
not just uncompetitive, it is not currently placeable at all, and existing ones there are stale artifacts
(e.g. a leftover 334 AIC bid sitting under a 663 floor) that will never fill; not just a heuristic cutoff."""
if not band:
return True
lo, hi = band
return (lo is None or price >= lo) and (hi is None or price <= hi)
def effective_supply(asks: list[tuple[float, float]], ref_price: float | None, mult: float = 1.25,
band: tuple[float | None, float | None] | None = None) -> float:
"""Units of standing sell orders that actually compete: in the tradeable price band (hard filter, if given)
AND price <= mult x reference (vwap7 or ask; soft filter for "active competition" vs merely valid-but-stale
high asks like OCK's top tier). asks = [(price, units)]."""
asks = [(p, u) for p, u in asks if in_band(p, band)]
if not ref_price:
return sum(u for _, u in asks)
return sum(u for p, u in asks if p <= band * ref_price)
return sum(u for p, u in asks if p <= mult * ref_price)
def effective_demand(bids: list[tuple[float, float]], band: tuple[float | None, float | None] | None) -> float:
"""Units of standing buy orders that are actually tradeable: within the exchange's price band. Filters out
stale/abandoned bids placed under a different band or long since left behind by the market (e.g. a 334 AIC
bid when the floor is 663) rather than genuine, fillable demand. bids = [(price, units)]."""
return sum(u for p, u in bids if in_band(p, band))