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