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
+71
@@ -0,0 +1,71 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user