- 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>
101 lines
4.4 KiB
Python
Executable File
101 lines
4.4 KiB
Python
Executable File
#!/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()
|