Initial PuGa toolkit: data layer, econ, depth-aware scan, state sync, plan push
- puga/: cached FIO + PRUNplanner clients, market view with order-book walk, econ formulas ported from PRUNplanner (tested against its suite and live FIO), saturation model v1 (reviewed by Opus) - tools/: scan (depth-aware), price, book, chain, state sync, plan_push (dry run default, [PuGa]-prefixed plans only), legacy prun_scan/prun_cxarb - docs/: mechanics (PRUNplanner is source of truth), roadmap, decisions, saturation design, archived handoff - secrets stay in .env (gitignored); ref/ holds PRUNplanner source (ignored) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Make-vs-buy cost tree for a material at one exchange, plus sourcing depth for every raw input.
|
||||
Cost to make = inputs at min(buy, make) per output unit; wages/capex excluded unless --wages.
|
||||
tools/chain.py DEC # AI1
|
||||
tools/chain.py BHP --depth 3 --qty 13
|
||||
"""
|
||||
import argparse, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from puga import config, econ, fio, market, saturation as sat
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("ticker")
|
||||
ap.add_argument("--cx", default=config.DEFAULT_CX)
|
||||
ap.add_argument("--depth", type=int, default=2, help="levels of make-vs-buy expansion")
|
||||
ap.add_argument("--qty", type=float, default=1.0, help="units/day you would make (sourcing check)")
|
||||
ap.add_argument("--wages", action="store_true", help="add wage cost per output unit (both luxuries at ask)")
|
||||
ap.add_argument("--cogc")
|
||||
a = ap.parse_args()
|
||||
|
||||
snap = market.snapshot()
|
||||
Q = lambda t: snap.get((t, a.cx))
|
||||
blds = {b["Ticker"]: b for b in fio.buildings()}
|
||||
by_out = {}
|
||||
for r in fio.recipes():
|
||||
for o in r["Outputs"]:
|
||||
by_out.setdefault(o["Ticker"], []).append(r)
|
||||
|
||||
def wage_per_day(b):
|
||||
tot = 0.0
|
||||
for t in econ.TIERS:
|
||||
n = b[t.capitalize() + "s"]
|
||||
if n:
|
||||
tot += n * sum(need / 100 * (Q(tk).ask if Q(tk) and Q(tk).ask else 0) for tk, need, _, _ in econ.CONSUMPTION[t])
|
||||
return tot
|
||||
|
||||
def buy(t):
|
||||
q = Q(t)
|
||||
return q.ask if q and q.ask else None
|
||||
|
||||
memo = {}
|
||||
|
||||
def make_cost(t, depth, stack=()):
|
||||
"""(best unit cost, how). Compares buy vs best recipe."""
|
||||
key = (t, depth)
|
||||
if key in memo:
|
||||
return memo[key]
|
||||
b = buy(t)
|
||||
best = (b if b is not None else float("inf"), "buy" if b is not None else "n/a", None)
|
||||
if depth > 0 and t not in stack:
|
||||
for r in by_out.get(t, []):
|
||||
bld = blds.get(r["BuildingTicker"])
|
||||
if not bld:
|
||||
continue
|
||||
cost = 0.0
|
||||
for i in r["Inputs"]:
|
||||
c, _, _ = make_cost(i["Ticker"], depth - 1, stack + (t,))
|
||||
cost += i["Amount"] * c
|
||||
outs = sum(o["Amount"] for o in r["Outputs"] if o["Ticker"] == t)
|
||||
if outs == 0:
|
||||
continue
|
||||
if a.wages:
|
||||
eff, _ = econ.building_efficiency({x.lower() + "s": bld[x + "s"] for x in ("Pioneer", "Settler", "Technician", "Engineer", "Scientist")},
|
||||
{x: 1.0 for x in econ.TIERS}, expertise=bld["Expertise"] or None, cogc=a.cogc)
|
||||
cost += wage_per_day(bld) * (r["TimeMs"] / 86400e3) / eff
|
||||
unit = cost / outs
|
||||
if unit < best[0]:
|
||||
best = (unit, f"make {r['BuildingTicker']} {r['TimeMs']/3.6e6:.1f}h", r)
|
||||
memo[key] = best
|
||||
return best
|
||||
|
||||
def show(t, depth, qty, indent=0):
|
||||
c, how, r = make_cost(t, depth)
|
||||
q = Q(t)
|
||||
pad = " " * indent
|
||||
mk = f"ask {q.ask:.0f} bid {q.bid:.0f} sup {q.supply:.0f} dem {q.demand:.0f} flow {sat.tref(q.traded7, q.traded30):.1f}/d" if q and q.ask and q.bid else "no market"
|
||||
print(f"{pad}{t:5} x{qty:<8.1f} best {c:9.0f}/u via {how:18} | {mk}")
|
||||
if r and how.startswith("make") and depth > 0:
|
||||
outs = sum(o["Amount"] for o in r["Outputs"] if o["Ticker"] == t)
|
||||
for i in r["Inputs"]:
|
||||
show(i["Ticker"], depth - 1, qty * i["Amount"] / outs, indent + 1)
|
||||
|
||||
show(a.ticker.upper(), a.depth, a.qty)
|
||||
print("\n# sourcing check for buy-leaves at", a.qty, "output units/day: ask-walk price for one day of inputs")
|
||||
q0 = Q(a.ticker.upper())
|
||||
_, _, r = make_cost(a.ticker.upper(), a.depth)
|
||||
if r:
|
||||
outs = sum(o["Amount"] for o in r["Outputs"] if o["Ticker"] == a.ticker.upper())
|
||||
for i in r["Inputs"]:
|
||||
need = a.qty * i["Amount"] / outs
|
||||
w = market.walk(i["Ticker"], a.cx, need, "buy")
|
||||
x = Q(i["Ticker"])
|
||||
days = x.supply / need if x and need else 0
|
||||
print(f" {i['Ticker']:5} need {need:8.1f}/d ask {x.ask if x else None} walked avg {w['avg'] and round(w['avg'])} short={w['short']} standing supply = {days:.0f} days of your use, market flow {sat.tref(x.traded7, x.traded30):.1f}/d")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user