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:
@@ -97,6 +97,27 @@ plan nets that against the smelters to ~0). This bit a first version of `tools/b
|
||||
comparing it against the by-hand restock calc, not by a unit test (the pure functions were right,
|
||||
the plan file chosen was wrong).
|
||||
|
||||
## Multi-base chains: `puga.network`
|
||||
|
||||
For a decision that spans two or more bases (mine LST at Nike, ship it, turn it into BSE at
|
||||
Deimos), don't price the transferred material at market on both ends — that double counts a trade
|
||||
that never happens. `puga.network.combine()` simulates each base independently (each base's own
|
||||
`profit` is still correct market-priced), then nets any transferred material between a producer's
|
||||
surplus and a consumer's need, charging only the real freight for what's actually moved; leftover
|
||||
surplus is still validly sold at market by the source, leftover deficit still validly bought by the
|
||||
destination — those are already right in each base's own numbers, so `combine()` only adds the one
|
||||
correction neither side has. CLI: `tools/network.py <chain.yaml>`, spec format and a worked example
|
||||
(Nike LST -> Deimos PP2/BSE) in `plans/chains/`.
|
||||
|
||||
Two gotchas specific to this: (1) each base's plan must be self-contained (see above) - Nike's plan
|
||||
needs its OWN housing even though Deimos's doesn't compete for it; (2) a NEW base's plan always
|
||||
prices a fresh core module unless the founding source (e.g. a Core Module Kit) actually covers it -
|
||||
mark it with `cm_free: true` on that base in the chain spec (mirrors `tools/simulate.py --cm-free`),
|
||||
or the phantom ~200k core-module cost craters an otherwise-fine ROI. (3) each base's reported
|
||||
profit/day is for its WHOLE plan, not the marginal addition - for "is this new building worth it",
|
||||
also simulate the base without the addition and diff, same as `tools/simulate.py`'s built-vs-planned
|
||||
new_capex logic but applied to profit instead of capex.
|
||||
|
||||
## Conventions to keep
|
||||
|
||||
- Cache TTLs matter: market data is cached ~15 min, static game data ~24h (`puga/cache.py`).
|
||||
|
||||
@@ -11,6 +11,7 @@ Legend: [ ] todo, [x] done. Build order matters; each step is usable on its own.
|
||||
6. [x] `tools/chain.py`: full bill of materials, make-vs-buy per node.
|
||||
7. [ ] `tools/whatif.py`: marginal ROI of adding a building to the actual state (uses `state/company.yaml`, efficiency stack, imports).
|
||||
8. [ ] `tools/planet.py`, `tools/found.py`: rank planets for a resource; founding cost including environment extras.
|
||||
13b. [x] `puga/network.py` + `tools/network.py`: multi-base chain modelling (per-base simulate, net transfers against real freight, `cm_free` for kits). Worked example: `plans/chains/nike_deimos_lst.yaml` (Nike LST -> Deimos PP2/BSE). Known gap: reports whole-plan profit per base, not the marginal addition; caller must diff manually.
|
||||
9. [ ] `tools/route.py`, `tools/haul.py`: jump graph, fuel, profit per trip.
|
||||
9b. [x] (dry-run default, --apply after user yes) `tools/plan_push.py`: create/update `[PuGa]` plans in PRUNplanner from YAML (Api-Key auth; see decisions.md for guardrails).
|
||||
10. [x] `tools/state.py sync` (sites, production efficiency, storage, ships, cash, permits): inventory, production, ships into `state/`.
|
||||
@@ -25,3 +26,4 @@ Legend: [ ] todo, [x] done. Build order matters; each step is usable on its own.
|
||||
- PRUNplanner auth is known (`Authorization: Api-Key <key>`). FIO REST and FIO API are separate services with separate keys (separate services with separate keys; an earlier "one API" reading was wrong). Keys are in `.env`. Verified 2026-09-18: FIO_REST_KEY works on rest.fnar.net as `Authorization: <key>` (own /sites, /storage etc. readable); FIO_API_KEY gives 401 there, host unknown. PRUNplanner key works (plans 0, empires 1, cx prefs 1 on his account). Still unknown: the OpenAPI spec at https://doc.fnar.net/api.json has no securitySchemes. Probe with read-only calls.
|
||||
- (resolved) PRUNplanner `exchanges/` has vwap_daily/7d/30d and traded volume; merged into `puga/market.py`. Trend history via cxpc still todo.
|
||||
- Extraction: concentration to daily rate formula.
|
||||
- (resolved 2026-09-25) Real price-band filtering (`NarrowPriceBandLow/High`, `WidePriceBandLow/High`) implemented in `puga/saturation.py` (`in_band`, `effective_demand`, `effective_supply` band param) and wired into `tools/scan.py` stage 2. Was flagged as an unimplemented refinement in saturation-design.md since day one; the player noticed a stale out-of-band bid in a live screenshot and asked the right question.
|
||||
|
||||
@@ -48,3 +48,24 @@ Later refinements (not built): supply-feedback fixpoint (our N raises supply), d
|
||||
|
||||
## Addendum: effective supply (implemented)
|
||||
Stage 1 of `tools/scan.py` uses flow + demand only; stage 2 (shortlist) fetches the ask book and counts only standing sell orders priced <= 1.25 x vwap7 (`saturation.effective_supply`). Reason: DEC at AI1 showed 2,013 units 'queue' of which 1,501 sat at 50,000 (1.5x vwap), which made the raw queue penalty call the market saturated (N=0.38) when the effective queue gives about 1.7.
|
||||
|
||||
|
||||
## Addendum: real price-band filtering (implemented 2026-09-25)
|
||||
Verified live: FIO's order_book `NarrowPriceBandLow/High` and `WidePriceBandLow/High` fields match
|
||||
APEX's own displayed "Price Band" exactly (BHP.AI1: 663.00 - 16,575.00 both narrow and wide at the
|
||||
time). This is the exchange's actual tradeable range (dev log #191: unrated companies +/-25%, rated
|
||||
+/-75% of a 3-day average, though observed bounds here are wider than that 2019 formula predicts -
|
||||
the live numbers win per source-of-truth rules). An order outside this band isn't just
|
||||
uncompetitive, it's a stale artifact that cannot be freshly placed and will never fill (e.g. a
|
||||
leftover 334 AIC bid on BHP.AI1 when the floor is 663 - only 19 of 1,060 standing "demand" units
|
||||
were like this, so the raw demand figure was mostly fine here, but the filter is a hard correctness
|
||||
fix, not a heuristic).
|
||||
- `saturation.in_band(price, (lo, hi))`: the band test.
|
||||
- `saturation.effective_demand(bids, band)`: hard-filters demand to in-band bids. Now used in
|
||||
`tools/scan.py` stage 2 (`demand_of()`) in place of the raw, unfiltered `Quote.demand`.
|
||||
- `effective_supply()` gained the same hard band pre-filter (`band=` param) alongside its existing
|
||||
soft `mult x vwap` "active competition" filter - these are different concepts: band = literally
|
||||
invalid/stale, mult x vwap = valid but not realistically competing right now (e.g. OCK's far top
|
||||
tier). Renamed the old positional `band` (vwap multiplier) param to `mult` to free up the name.
|
||||
- `p_patient()` already accepted `wide_high` as a price ceiling; `scan.py` wasn't passing it before,
|
||||
now does via `band_of(t)[1]`.
|
||||
|
||||
Reference in New Issue
Block a user