- tools/buy.py: cash-aware shopping list (construction gap vs empire state's built buildings, plus N days of NET operating stock via simulate() so self-produced inputs net against consumption), priced at real order-book fill cost, checked against cash/reserve, flags multi-trip, binary-searches an affordable size when short - tools/sell.py: where to post an ask vs hitting the bids now, with expected clear time from 30-day traded-volume percentiles - docs/library.md: module map and patterns for using puga.* directly instead of the CLI, plus the "simulate() only knows what the plan lists" gotcha that a first version of buy.py hit - tools/simulate.py: fix --no-hq being a no-op; tools/state.py: permits_total_override for when the in-game HQ screen disagrees with FIO's MaximumPermits - docs/mechanics.md: HQ display vs PRUNplanner's HQ flag, workforce arrival sources - CLAUDE.md, docs/roadmap.md updated Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
58 lines
2.5 KiB
Python
Executable File
58 lines
2.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Where to post a sell order and how long it should take to clear, vs hitting the bids now.
|
|
Formalizes the by-hand check done before every AL/BHP sale.
|
|
|
|
puga sell BHP 8 --cx AI1
|
|
puga sell AL 16 --undercut 5
|
|
"""
|
|
import argparse, datetime, statistics, sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
from puga import config, fio, market
|
|
|
|
|
|
def daily_traded(tk: str, cx: str, days: int = 30) -> list[float]:
|
|
rows = [(datetime.datetime.fromtimestamp(e["DateEpochMs"] / 1000, datetime.timezone.utc).date(), e["Traded"])
|
|
for e in fio.cxpc(tk, cx) if e.get("Interval") == "DAY_ONE" and e.get("Traded")]
|
|
return [t for _, t in rows[-days:]]
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("ticker")
|
|
ap.add_argument("qty", type=float)
|
|
ap.add_argument("--cx", default=config.DEFAULT_CX)
|
|
ap.add_argument("--undercut", type=float, default=10, help="AIC to undercut the current best ask by")
|
|
a = ap.parse_args()
|
|
t = a.ticker.upper()
|
|
ob = fio.order_book(t, a.cx)
|
|
asks = sorted((o["ItemCost"], o["ItemCount"] or 0) for o in ob["SellingOrders"] if o.get("ItemCost"))
|
|
best_ask = asks[0][0] if asks else None
|
|
post = round((best_ask - a.undercut) if best_ask else (ob.get("Ask") or 0))
|
|
ahead = sum(u for p, u in asks if p < post)
|
|
|
|
hit = market.walk(t, a.cx, a.qty, "sell")
|
|
trades = daily_traded(t, a.cx)
|
|
med = statistics.median(trades) if trades else 0
|
|
lo = sorted(trades)[int(0.2 * (len(trades) - 1))] if trades else 0
|
|
hi = sorted(trades)[int(0.8 * (len(trades) - 1))] if trades else 0
|
|
|
|
print(f"{t}.{a.cx} best ask {best_ask} bid {ob.get('Bid')} vwap7 {ob.get('PriceAverage')}")
|
|
print(f"\nOPTION A: post {a.qty:g} at {post:.0f} ({ahead:.0f} units ahead of you at a lower price)")
|
|
print(f" revenue if filled: {post * a.qty:,.0f}")
|
|
if med:
|
|
print(f" expected clear time (last {len(trades)}d volume): busy day {24 * a.qty / hi:.1f}h | "
|
|
f"median day {24 * a.qty / med:.1f}h | slow day {24 * a.qty / max(lo, 1):.1f}h")
|
|
else:
|
|
print(" no recent trade history to estimate clear time")
|
|
|
|
print(f"\nOPTION B: hit the bids now (instant)")
|
|
print(f" revenue: {hit['total']:,.0f} (avg {hit['avg']:.0f}, worst {hit['worst']:.0f}"
|
|
+ (f", SHORT: book only fills {hit['filled']:.0f}" if hit["short"] else "") + ")")
|
|
|
|
print(f"\nposting patiently gains {post * a.qty - hit['total']:,.0f} over hitting the bids, at the cost of waiting")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|