Files
PuGa/docs/library.md
T
dodoxandClaude Sonnet 5 c4178fe779 Add tools/undervalued.py: trading strategy, goods priced below production cost
Finds manufactured goods currently selling below their implied
production cost (live input cost vs output price) - candidates to buy
and hold for a price-reversion bounce, the mirror of the "is this
worth building" question saturation.py already answers.

Backtested first (326 output tickers, 287 deduplicated dip-onset
episodes across 112 distinct tickers): +3mo forward return mean
+21.3%/median +8.9%/73.2% hit rate at >=10 units/day volume - real
signal, not noise, but only ~0.10-0.24%/day, far below the 14-29%/day
marginal ROI this session's production builds found. Documented
prominently in the tool's own docstring and CLAUDE.md so it doesn't
get mistaken for a primary strategy - it's for excess idle capital
once better-yielding builds are exhausted, not before.

Promoted daily_prices() out of tools/history.py into puga/history.py
so both tools share it, per the self-improve rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-09 10:28:34 +02:00

9.8 KiB

Using puga as a library (not the CLI)

The tools/*.py scripts cover the common questions. For anything more involved — a custom what-if, a one-off cost breakdown with wages and capex, a market check the CLI doesn't expose — import the modules directly and write a short script inline. This is normal; most answers in this project's history came from a python - <<EOF ... EOF block, not a tool flag.

. .venv/bin/activate   # or use .venv/bin/python directly
python - <<'EOF'
from puga import market, econ, fio, prunplanner as pp, saturation as sat
...
EOF

Modules and what they give you

  • puga.fio — public FIO REST, cached. exchange_all(), order_book(mat, cx) (full book, BuyingOrders/SellingOrders), recipes(), buildings(), materials(), planets_full(), workforce_needs(). own(path, ttl=0) for the player's authenticated data (/sites/{u}, /storage/{u}, /production/{u} — has live Efficiency/Condition per building, /workforce/{u}/{planet}, /ship/ships/{u}, /ship/flights/{u}). me() gives the configured username.
  • puga.prunplanner — exchanges() (has vwap_daily/7d/30d, traded volume — richer than FIO's book totals), recipes()/buildings() (different field names than fio's: building_ticker, material_ticker, snake_case), _g(path, ttl) for any other /data/... GET (e.g. _g(f"/data/planet/{id}/", 3600) for fertility, resources, COGC, build requirements). request(method, path, body) for authenticated calls (Api-Key) — reads are always fine; writes must go through tools/plan_push.py's guardrails, don't call request("POST"/"PUT"/"DELETE", "/planning/plan/...") directly.
  • puga.market — snapshot(refresh=False) returns {(ticker, cx): Quote} merging FIO book totals with PRUNplanner's VWAP/volume; this is the one-stop source for prices, use it instead of calling fio/prunplanner separately. walk(mat, cx, qty, side, refresh=False) walks the live order book for a real fill price (side="buy" hits asks, "sell" hits bids; handles market-maker orders with ItemCount: None as unlimited). uni30(snap, tk) = PRUNplanner's "Universe 30D" price basis (volume-weighted 30d VWAP across all CX).
  • puga.econ — pure formulas, no I/O, ported from PRUNplanner and tested against its own suite plus live FIO: tier_efficiency, workforce_consumption, building_efficiency, production_io, daily_extraction/extraction_cycle, optimize_habs, CONSUMPTION (the per-tier need table), HAB_CAP/HAB_AREA, TIERS. This is what you use for "what if I understaff this building" or "what's the real wage bill for N heads" — the CLI tools only expose fixed slices of this.
  • puga.saturation — tref, n_out, p_patient, effective_supply, is_thin: the depth/persistence primitives behind scan.py. Reuse these instead of re-deriving a "how many buildings can the market take" estimate by hand.
  • puga.simulate — simulate(plan, recipes, buildings, resources, fertility, price, ...): the PRUNplanner-simulator replica. Reuse this for anything plan-shaped rather than reimplementing efficiency/workforce/flows again.
  • puga.route — load_graph(), resolve_system(query) (planet/system natural id or name -> SystemId), shortest_path(src, dst, graph=None) (Dijkstra by straight-line ly over FIO's /systemstars jump graph; returns jumps, distance_ly, path). Not a PRUNplanner feature, built for this project since travel isn't modelled anywhere else. CLI: tools/route.py.
  • puga.undervalued — manufacturing_candidates(), current_margin(ticker, recipe, cx), scan_current(cx, min_vol=5.0): manufactured goods priced below implied production cost, for a buy-and-hold-for-reversion trade, not a build decision. Backtested expected return is ~0.10-0.24%/day - far below production's 14-29%/day, only worth excess idle capital. CLI: tools/undervalued.py. Shares puga.history.daily_prices with tools/history.py.
  • puga.planet — candidates(resource_ticker, min_factor=0.0): every planet with that resource, sorted by extraction factor, each with temperature/pressure/gravity/fertility/faction and build_requirements (FIO's own founding-cost materials for that specific planet — real data, not guessed environment thresholds). CLI: tools/planet.py, pass --from an existing base or pure-factor ranking is misleading (best-factor planets are often thousands of ly away).
  • puga.config — get(key) reads .env; state_path() returns the real empire/state/company.yaml if synced, else the tracked example.

Patterns that come up a lot

Price a basket at real fill cost (construction lists, working stock, a chain's inputs):

from puga import market, fio
mat = {m["Ticker"]: m for m in fio.materials()}
def price(items, cx="AI1"):
    tot = wt = 0
    for t, q in items.items():
        w = market.walk(t, cx, q, "buy"); tot += w["total"]; wt += q * mat[t]["Weight"]
    return tot, wt

Building construction cost incl. MCG, using either fio.buildings() (Ticker, BuildingCosts, AreaCost) or prunplanner.buildings() (building_ticker, costs, area_cost) — pick one client and stay consistent within a script, the field names differ.

Efficiency of a hypothetical building, e.g. staffing/COGC/expert what-ifs:

from puga import econ
eff, elements = econ.building_efficiency(
    dict(pioneers=40, settlers=10), {"pioneer": 1.0, "settler": 1.0},
    expertise="METALLURGY", cogc="METALLURGY", experts={"METALLURGY": 2},
    faction="ANTARES", permits_used=1, permits_total=2)

Live production truth (skip the model, read the game's own number):

from puga import fio
for line in fio.own(f"/production/{fio.me()}", ttl=0):
    print(line["Type"], line["Efficiency"], line["Condition"])

A recipe's daily flows for N buildings:

io = econ.production_io([dict(time_ms=r["TimeMs"], inputs=..., outputs=...)], eff, n_buildings)

puga.simulate.simulate() only knows what the plan tells it

simulate() (and anything built on it, e.g. tools/buy.py) computes material flows from the plan_data's own building list — it has no idea what else exists at the base beyond the built dict used for the construction-gap check. Pass a plan covering everything whose production or consumption matters, not just the piece you're adding, or a self-sufficient material will look like something you need to buy (a standalone HWP plan says "buy 42.8 AL/day"; the full base+HWP plan nets that against the smelters to ~0). This bit a first version of tools/buy.py; caught by 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.

Multi-base state: storage and ships are now tagged by location

Once a 2nd base exists, empire/state/company.yaml's storage list has multiple STORE entries (one per base) and must be filtered by planet (a field tools/state.py now adds, from FIO's AddressableId matching a site's SiteId) - summing all STORE entries blindly (as tools/buy.py did briefly) mixes bases' inventories together. Similarly each entry in ships now carries a flight dict (origin, destination, eta_ms) from live /ship/flights, keyed to the ship via ShipId (not StlFuelStoreId, despite that looking like a plausible match - verified 2026-09-28). tools/runway.py's inbound_ships()/on_hand() are the reference implementation for scoping to one base correctly; reuse them rather than re-deriving this.

Conventions to keep

  • Cache TTLs matter: market data is cached ~15 min, static game data ~24h (puga/cache.py). Pass refresh=True if you just changed something in-game and need it now.
  • fio uses PascalCase ticker/field names (MaterialTicker, TimeMs); prunplanner uses snake_case (material_ticker, time_ms). Don't mix them in the same dict without converting.
  • Never call prunplanner.request with a write verb outside tools/plan_push.py — the [PuGa] name guard and dry-run default live there, not in the client.
  • One-off scripts like these are fine to throw away; if a calculation gets reused more than once or two, promote it into a function in the relevant module instead of re-deriving it in a shell heredoc each time.