- puga/: cached FIO + PRUNplanner clients, market view with order-book walk, econ formulas ported from PRUNplanner (tested against its suite and live FIO), saturation model v1 (reviewed by Opus) - tools/: scan (depth-aware), price, book, chain, state sync, plan_push (dry run default, [PuGa]-prefixed plans only), legacy prun_scan/prun_cxarb - docs/: mechanics (PRUNplanner is source of truth), roadmap, decisions, saturation design, archived handoff - secrets stay in .env (gitignored); ref/ holds PRUNplanner source (ignored) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
40 lines
2.3 KiB
Python
40 lines
2.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Pure exchange arbitrage: for each material, cheapest ask at any CX vs best bid at any other CX.
|
|
Reports spread per unit, per ton and per m3 of cargo, and how many units the thinner side can absorb.
|
|
python prun_cxarb.py --from AI1 # only routes buying at AI1
|
|
python prun_cxarb.py --to AI1 # only routes selling at AI1
|
|
python prun_cxarb.py --minqty 200 --sort ton
|
|
"""
|
|
import argparse, json, urllib.request
|
|
FIO="https://rest.fnar.net"
|
|
def get(p):
|
|
with urllib.request.urlopen(FIO+p,timeout=60) as r: return json.load(r)
|
|
ap=argparse.ArgumentParser()
|
|
ap.add_argument("--from",dest="src",default=None); ap.add_argument("--to",dest="dst",default=None)
|
|
ap.add_argument("--minqty",type=float,default=100,help="min units both sides can absorb")
|
|
ap.add_argument("--sort",default="ton",choices=["unit","ton","m3","total","pct"]); ap.add_argument("--top",type=int,default=30)
|
|
a=ap.parse_args()
|
|
mats={m["Ticker"]:m for m in get("/material/allmaterials")}
|
|
px={}
|
|
for e in get("/exchange/all"): px.setdefault(e["MaterialTicker"],{})[e["ExchangeCode"]]=e
|
|
rows=[]
|
|
for t,cxs in px.items():
|
|
for s,es in cxs.items():
|
|
if a.src and s!=a.src: continue
|
|
if not es.get("Ask"): continue
|
|
for d,ed in cxs.items():
|
|
if d==s or (a.dst and d!=a.dst) or not ed.get("Bid"): continue
|
|
# depth: units available at ask side / wanted at bid side (order book totals)
|
|
# /exchange/all has only book totals; use min(supply at source, demand at destination) as depth proxy
|
|
qty=min(es.get("Supply") or 0, ed.get("Demand") or 0)
|
|
if qty<a.minqty: continue
|
|
spread=ed["Bid"]-es["Ask"]
|
|
if spread<=0: continue
|
|
w=mats[t]["Weight"]; v=mats[t]["Volume"]
|
|
rows.append(dict(t=t,src=s,dst=d,ask=es["Ask"],bid=ed["Bid"],unit=spread,pct=100*spread/es["Ask"],ton=spread/w if w else 0,m3=spread/v if v else 0,qty=qty,total=spread*qty))
|
|
rows.sort(key=lambda r:r[a.sort],reverse=True)
|
|
print(f"{'mat':4} {'buy@':4} {'sell@':5} {'ask':>8} {'bid':>8} {'spread':>7} {'%':>5} {'/ton':>7} {'/m3':>7} {'qty':>7} {'total':>9}")
|
|
for r in rows[:a.top]:
|
|
print(f"{r['t']:4} {r['src']:4} {r['dst']:5} {r['ask']:8.0f} {r['bid']:8.0f} {r['unit']:7.0f} {r['pct']:5.0f} {r['ton']:7.0f} {r['m3']:7.0f} {r['qty']:7.0f} {r['total']:9.0f}")
|
|
|