#!/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()