Files
PuGa/tools/sell.py
T
dodoxandClaude Sonnet 5 58db08b921 Add multi-base chain modelling, fix price-band-aware demand/supply, tranche-split sell tool
- puga/network.py + tools/network.py: model a multi-base chain (e.g. mine LST at one base,
  ship it, consume it at another). Simulates each base independently, nets a transferred
  material's producer-surplus against consumer-need, charges real freight only on what's
  moved, cm_free for a founding covered by a Core Module Kit. Worked example in plans/chains/.
- puga/saturation.py: real price-band filtering. FIO's order_book NarrowPriceBandLow/High and
  WidePriceBandLow/High match APEX's own displayed Price Band exactly (verified live) - an
  order outside it is a stale artifact, not just uncompetitive. Added in_band() and
  effective_demand(); effective_supply() gained the same hard band filter alongside its
  existing soft vwap-proximity filter (renamed that param mult to free up `band`). Wired into
  tools/scan.py stage 2 in place of the raw, unfiltered demand figure. Was flagged as an
  unimplemented refinement in saturation-design.md since the original design review.
- tools/sell.py: self-contained tranche split (aggressive tranche capped at a volume quantile,
  median normally or 80th pct with --tight when a payment is imminent and stockout risk
  outweighs margin; patient tranche priced under the next competitor tier). Instant-bid
  comparison moved behind --show-bid (off by default).
- Docs and roadmap updated accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 14:32:09 +02:00

86 lines
4.6 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, including the tranche split: an
aggressive (undercut) tranche sized to a quantile of daily volume so it's confident to clear
today, and a patient tranche (priced just under the next competitor tier) for the rest. The
quantile is the newsvendor critical fractile Cu/(Cu+Co): median (0.5) when the cost of running
out of cash (Cu) and the cost of discounting unnecessarily (Co) are about equal; higher (0.8 via
--tight) when Cu dominates, i.e. a restock/buildout payment is imminent and stockout risk (not
having cash in time) matters more than a few points of margin.
puga sell BHP 8 --cx AI1
puga sell BHP 11 --tight # cash-tight: bias the split toward the aggressive tranche
"""
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 quantile(xs: list[float], q: float) -> float:
return sorted(xs)[int(q * (len(xs) - 1))] if xs else 0.0
def tranche_split(qty: float, trades: list[float], tight: bool) -> tuple[float, float]:
"""(aggressive_qty, patient_qty). Aggressive is capped at qty and at the volume quantile."""
q = 0.8 if tight else 0.5
aggressive = min(qty, quantile(trades, q)) if trades else qty
return aggressive, qty - aggressive
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")
ap.add_argument("--tight", action="store_true", help="cash-tight: bias the tranche split toward the aggressive (undercut) tranche")
ap.add_argument("--show-bid", action="store_true", help="also show instant-bid revenue (usually worse than the tranche split; off by default)")
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)
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"\npost {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")
if a.show_bid:
hit = market.walk(t, a.cx, a.qty, "sell")
print(f"\ninstant (hit the bids now): {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" posting patiently gains {post * a.qty - hit['total']:,.0f} over this, at the cost of waiting")
aggr_qty, patient_qty = tranche_split(a.qty, trades, a.tight)
if patient_qty > 0.01:
tiers_above = sorted({p for p, u in asks if p > post})
patient_price = round(tiers_above[0] - 1) if tiers_above else round(post + 2 * a.undercut)
q_label = "80th pct (tight)" if a.tight else "median"
print(f"\nTRANCHE SPLIT ({q_label} volume quantile, {len(trades)}d):")
print(f" aggressive: {aggr_qty:.1f} @ {post:.0f} (confident to clear today)")
print(f" patient: {patient_qty:.1f} @ {patient_price:.0f} (waits behind the next tier, higher margin)")
print(f" vs posting all {a.qty:g} at {post:.0f}: gains {patient_qty*(patient_price-post):,.0f} extra if the patient tranche fills")
if __name__ == "__main__":
main()