Files
PuGa/tools/persistence.py
dodoxandClaude Sonnet 5 3bbf524ebb 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>
2026-09-19 00:11:02 +02:00

72 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""How stable are scan opportunities? Re-prices each scan row over its exchange history (daily VWAP candles) and reports
mean ROI/day over 30/90/180 days and all history, plus the share of days with positive profit.
tools/scan.py --min-n 3 --json /tmp/rows.json --top 20 ; tools/persistence.py /tmp/rows.json [--only KV,BHP]
Method: profit(period) = profit_now + batches/day * (margin_period - margin_now), margin = output value - input cost per batch at daily VWAP,
margin_now = mean of the last 7 days. So wages/freight/efficiency in the scan row carry over; only prices move."""
import argparse, json, re, statistics, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
sys.path.insert(0, str(Path(__file__).resolve().parent))
from puga import config
from history import daily_prices
def parse(side):
return {m.group(2): float(m.group(1)) for m in re.finditer(r"([\d.]+)([A-Z0-9]+)", side)}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("rows")
ap.add_argument("--cx", default=config.DEFAULT_CX)
ap.add_argument("--only", help="comma list of output tickers")
ap.add_argument("--top", type=int, default=25)
ap.add_argument("--lag", type=float, default=3.0, help="days from decision to first output (build, hauling, workers)")
ap.add_argument("--min-mkt", type=float, default=0.0)
a = ap.parse_args()
only = {x.strip().upper() for x in a.only.split(",")} if a.only else None
rows = json.loads(Path(a.rows).read_text())
cache = {}
out = []
seen = set()
for r in rows:
left, right = r["rec"].replace(" [thin]", "").split("->")
ins, outs = parse(left), parse(right)
if only and not (set(outs) & only):
continue
key = (r["rec"], r["bld"], r["staff"])
if key in seen:
continue
seen.add(key)
toks = list(ins) + list(outs)
for t in toks:
if t not in cache:
cache[t] = daily_prices(t, a.cx)
last, series = {}, []
for d in sorted(set().union(*[set(cache[t]) for t in toks])):
for t in toks:
if d in cache[t]:
last[t] = cache[t][d][0]
if len(last) == len(toks):
series.append(sum(last[t] * n for t, n in outs.items()) - sum(last[t] * n for t, n in ins.items()))
if len(series) < 60:
continue
per_day = 24 / r["h"] * r["eff"]
now = statistics.mean(series[-7:])
prof = [r["total"] / r["n"] + per_day * (m - now) for m in series] # per building
capex = r["capex"] / r["n"]
f = lambda n: 100 * statistics.mean(prof[-n:]) / capex
p14 = statistics.mean(prof[-14:])
out.append((f(14), dict(pb=capex / p14 if p14 > 0 else 999, net7=p14 * (7 - a.lag) - capex, net14=p14 * (14 - a.lag) - capex, d7=f(7), d14=f(14), capex=capex, profit_now=r["total"] / r["n"], rec=r["rec"], bld=r["bld"], staff=r["staff"], now=100 * (r["total"] / r["n"]) / capex, d30=f(30), d90=f(90), d180=f(180), all=f(len(prof)),
pos=100 * sum(1 for x in prof[-180:] if x > 0) / len(prof[-180:]), days=len(prof), market=r["n_lim"])))
out.sort(key=lambda x: -x[0]) # ranked by 14-day mean ROI
out = [x for x in out if x[1]["market"] >= a.min_mkt]
print(f"{'now':>5} {'7d':>5} {'14d':>5} {'30d':>5} {'payback':>7} {'net@7d':>8} {'net@14d':>8} {'90d':>5} {'180d':>5} {'pos%':>5} {'days':>5} {'mkt':>5} bld staff recipe (ROI/day % per building; pos% = days profitable in last 180d)")
for _, o in out[:a.top]:
print(f"{o['now']:5.1f} {o['d7']:5.1f} {o['d14']:5.1f} {o['d30']:5.1f} {o['pb']:6.1f}d {o['net7']:8.0f} {o['net14']:8.0f} {o['d90']:5.1f} {o['d180']:5.1f} {o['pos']:5.0f} {o['days']:5d} {o['market']:5.1f} {o['bld']:4} {o['staff']:5} {o['rec']}")
if __name__ == "__main__":
main()