Initial PuGa toolkit: data layer, econ, depth-aware scan, state sync, plan push
- 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>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
#!/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}")
|
||||
|
||||
Reference in New Issue
Block a user