Add simulator replica, persistence, planet scan, README; split empire state out of the repo
- tools/simulate.py + puga/simulate.py: replica of PRUNplanner's simulator (flows and efficiency verified against screenshots), reports real new capex (planned minus built) - tools/scan.py: staffing variants, freight, HQ/experts, --planet mode, demolish-later, --min-n as a pure market-size filter, --json output - tools/history.py, tools/persistence.py: margin history and short-horizon payback checks - tools/plan_push.py: guarded delete; tools/state.py: syncs to empire/ - README with features and setup; CLAUDE.md made generic - Own-empire material (profile, state, plans, notes) moved to gitignored empire/; generic examples in plans/examples and state/company.example.yaml Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Margin history of a recipe from daily exchange candles (FIO cxpc DAY_ONE): output value minus input cost per batch, per month.
|
||||
Daily price = value traded / units traded (VWAP), forward-filled over days without trades.
|
||||
tools/history.py KV # AI1, the recipe producing KV
|
||||
tools/history.py BHP --cx AI1 --eff 1.18 --overhead 3200 --capex 94000 --months 12
|
||||
Building/day = batches/day at --eff; profit/day = batches/day * margin_per_batch - overhead (wages etc, --overhead)."""
|
||||
import argparse, datetime, statistics, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from puga import config, fio
|
||||
|
||||
|
||||
def daily_prices(tk, cx):
|
||||
out = {}
|
||||
for e in fio.cxpc(tk, cx):
|
||||
if e.get("Interval") == "DAY_ONE" and e.get("Traded"):
|
||||
out[datetime.datetime.fromtimestamp(e["DateEpochMs"] / 1000, datetime.timezone.utc).date()] = (e["Volume"] / e["Traded"], e["Traded"])
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("ticker")
|
||||
ap.add_argument("--cx", default=config.DEFAULT_CX)
|
||||
ap.add_argument("--recipe", type=int, default=0, help="index if several recipes produce it")
|
||||
ap.add_argument("--eff", type=float, default=1.0)
|
||||
ap.add_argument("--overhead", type=float, default=0.0, help="daily wages/other per building")
|
||||
ap.add_argument("--capex", type=float, help="per building; gives ROI/day")
|
||||
ap.add_argument("--months", type=int, default=14)
|
||||
a = ap.parse_args()
|
||||
t = a.ticker.upper()
|
||||
recs = [r for r in fio.recipes() if any(o["Ticker"] == t for o in r["Outputs"])]
|
||||
r = recs[a.recipe]
|
||||
ins = {i["Ticker"]: i["Amount"] for i in r["Inputs"]}
|
||||
outs = {o["Ticker"]: o["Amount"] for o in r["Outputs"]}
|
||||
per_day = 24 / (r["TimeMs"] / 3.6e6) * a.eff
|
||||
print(f"{r['BuildingTicker']}: {ins} -> {outs}, {r['TimeMs']/3.6e6:.1f}h, eff {a.eff}: {per_day:.3f} batches/day; recipe {a.recipe + 1} of {len(recs)}")
|
||||
series = {m: daily_prices(m, a.cx) for m in list(ins) + list(outs)}
|
||||
days = sorted(set.intersection(*[set(s) for s in series.values()])) if False else sorted(set().union(*[set(s) for s in series.values()]))
|
||||
last = {}
|
||||
rows = []
|
||||
for d in days:
|
||||
for m, s in series.items():
|
||||
if d in s:
|
||||
last[m] = s[d][0]
|
||||
if len(last) == len(series):
|
||||
rev = sum(last[m] * n for m, n in outs.items())
|
||||
cost = sum(last[m] * n for m, n in ins.items())
|
||||
rows.append((d, rev, cost, series[t].get(d, (0, 0))[1]))
|
||||
if not rows:
|
||||
sys.exit("no overlapping history")
|
||||
by = {}
|
||||
for d, rev, cost, tr in rows:
|
||||
by.setdefault((d.year, d.month), []).append((rev, cost, tr, per_day * (rev - cost) - a.overhead))
|
||||
print(f"\n{'month':8} {'out/batch':>10} {'in/batch':>10} {'margin':>9} {'margin%':>7} {'profit/d':>9} {'ROI/d%':>7} {'units/d':>8} {'days>0':>7}")
|
||||
for (y, m), v in list(sorted(by.items()))[-a.months:]:
|
||||
rev = statistics.mean(x[0] for x in v); cost = statistics.mean(x[1] for x in v); pr = statistics.mean(x[3] for x in v)
|
||||
roi = f"{100 * pr / a.capex:7.1f}" if a.capex else f"{'-':>7}"
|
||||
print(f"{y}-{m:02d} {rev:10.0f} {cost:10.0f} {rev-cost:9.0f} {100*(rev-cost)/cost if cost else 0:7.1f} {pr:9.0f} {roi} "
|
||||
f"{statistics.mean(x[2] for x in v):8.1f} {100*sum(1 for x in v if x[3] > 0)/len(v):6.0f}%")
|
||||
prof = [per_day * (rev - cost) - a.overhead for _, rev, cost, _ in rows]
|
||||
for label, n in (("last 30d", 30), ("last 90d", 90), ("last 180d", 180), ("all", len(prof))):
|
||||
p = prof[-n:]
|
||||
print(f"{label:9} profit/d mean {statistics.mean(p):8.0f} min {min(p):8.0f} max {max(p):8.0f} days>0 {100*sum(1 for x in p if x > 0)/len(p):4.0f}%"
|
||||
+ (f" ROI/d {100*statistics.mean(p)/a.capex:5.1f}%" if a.capex else ""))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user