Files
PuGa/puga/market.py
T
dodoxandClaude Sonnet 5 3bbf524ebb 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>
2026-09-19 00:11:02 +02:00

65 lines
2.8 KiB
Python

"""Unified market view: FIO book totals merged with PRUNplanner VWAP/volume, plus order-book walks."""
from dataclasses import dataclass
from . import fio, prunplanner as pp
CXS = ["AI1", "NC1", "CI1", "IC1", "NC2", "CI2"]
@dataclass
class Quote:
tk: str
cx: str
ask: float | None = None
bid: float | None = None
supply: float = 0 # units on sell side (order-book total)
demand: float = 0 # units on buy side
vwap7: float | None = None
vwap30: float | None = None
traded7: float = 0 # avg units/day over 7d
traded30: float = 0 # avg units/day over 30d
mm_buy: float | None = None
mm_sell: float | None = None
def snapshot(refresh=False) -> dict[tuple[str, str], Quote]:
q: dict[tuple[str, str], Quote] = {}
for e in fio.exchange_all(refresh):
k = (e["MaterialTicker"], e["ExchangeCode"])
q[k] = Quote(k[0], k[1], e.get("Ask"), e.get("Bid"), e.get("Supply") or 0, e.get("Demand") or 0,
mm_buy=e.get("MMBuy"), mm_sell=e.get("MMSell"))
for e in pp.exchanges(refresh):
k = (e["ticker"], e["exchange_code"])
x = q.setdefault(k, Quote(k[0], k[1], e.get("ask"), e.get("bid"), e.get("supply") or 0, e.get("demand") or 0))
x.vwap7, x.vwap30 = e.get("vwap_7d"), e.get("vwap_30d")
x.traded7, x.traded30 = e.get("avg_traded_7d") or 0, e.get("avg_traded_30d") or 0
return q
def walk(mat: str, cx: str, qty: float, side: str, refresh=False) -> dict:
"""Walk the live order book. side='buy' takes sell orders (you pay asks); side='sell' hits buy orders (you get bids).
Returns avg price, units filled, worst price reached, total cost/proceeds. Shallow books give filled < qty."""
ob = fio.order_book(mat, cx, refresh)
orders = ob["SellingOrders"] if side == "buy" else ob["BuyingOrders"]
# MM orders come with ItemCount None = unlimited depth; treat as infinite
orders = sorted((o for o in orders if o.get("ItemCount") != 0), key=lambda o: o["ItemCost"], reverse=(side == "sell"))
left, total, worst = qty, 0.0, None
for o in orders:
take = min(left, float("inf") if o.get("ItemCount") is None else o["ItemCount"])
total += take * o["ItemCost"]
worst, left = o["ItemCost"], left - take
if left <= 0:
break
filled = qty - left
return dict(avg=total / filled if filled else None, filled=filled, worst=worst, total=total, short=left > 0)
def uni30(snap: dict, tk: str) -> float | None:
"""PRUNplanner's 'Universe 30D' price basis: volume-weighted 30d VWAP across all exchanges (verified against its BHP price)."""
num = den = 0.0
for cx in CXS:
q = snap.get((tk, cx))
if q and q.vwap30 and q.traded30:
num += q.vwap30 * q.traded30
den += q.traded30
return num / den if den else None