Add buy/sell tools, library docs, and permit/HQ fixes

- 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>
This commit is contained in:
2026-09-22 17:40:17 +02:00
co-authored by Claude Sonnet 5
parent 6e0554e10a
commit e2e592cae3
9 changed files with 368 additions and 1 deletions
+5
View File
@@ -32,6 +32,9 @@ Agent-first toolkit for advising a Prosperous Universe player. The player asks q
- `puga/` library; `tools/` CLIs; `plans/examples/` generic plan specs (also test fixtures); `state/company.example.yaml` example state; `tests/`; `data/` (gitignored fetched data cache); `ref/` (gitignored PRUNplanner source).
- Rule: game facts and generic methods go in `docs/`; anything about the player's own bases, cash, decisions or preferences goes in `empire/`.
## Library use (not just CLI)
For anything the CLI tools don't expose (custom what-ifs, cost breakdowns with wages/capex, ad-hoc market checks) import `puga.*` directly in a short inline script instead of stretching a tool's flags. See `docs/library.md` for the module map and patterns; most non-trivial answers in this project came from a `python - <<EOF` block, not a CLI flag.
## Tools
Run as `puga <tool> ...` (e.g. `puga scan --min-n 3`, `puga plan pull <uuid>`); the `tools/x.py` paths below are the same programs.
- `tools/state.py sync|show` refreshes `empire/state/company.yaml` from live FIO (cash, permits, buildings, real production efficiency, storage, ships).
@@ -39,6 +42,8 @@ Run as `puga <tool> ...` (e.g. `puga scan --min-n 3`, `puga plan pull <uuid>`);
- `tools/simulate.py <spec.yaml | --uuid U> [--off EXT,SME] [--basis real|uni30|vwap30|ask|...]` replica of PRUNplanner's simulator (efficiency, workforce, material I/O, profit). Flows and efficiency verified exact vs screenshots; profit within ~2%. The NEW CAPEX line = planned minus already built (from state), the honest payback; ignore the plan-level ROI (PRUNplanner always adds a core module). `--uuid` reads the SAVED plan from the account (UI edits must be saved first).
- `tools/scan.py` DEPTH-AWARE recipe scan (use this): buildings the market could absorb, ROI for our own size, patient prices, ask-walked inputs. Fully staffed AND understaffed variants by default (`--staffing`), freight netted out (`--trip-cost`, `--cargo`), HQ/COGC/experts/faction, `--planet ID` adds that planet's extraction, fertility and COGC (a planet not in state is a new base: no HQ, permits+1), `--deprec 60` prices demolish-later. `--min-n` is ONLY a noise filter on market capacity (use 3, ideally 10); ROI is always for our own size (`--own`, default 1 building), never at the filtered scale. Below 1 lets sub-building junk in. `--json rows.json` feeds `persistence.py`. Library: `puga/saturation.py`.
- `tools/history.py TICKER` monthly margin history of the recipe producing TICKER; `tools/persistence.py rows.json` re-prices scan rows over history (mean ROI 7/14/30/90/180d, payback, net gain over 7 and 14 days, % days profitable). ALWAYS check persistence before recommending: current margins are often a spike.
- `tools/buy.py <spec.yaml|--uuid U> --days N` cash-aware shopping list: construction gap (plan vs empire state's built buildings) plus N days of the plan's NET operating stock (via `simulate()`, so self-produced inputs like AL net against consumption), priced at real fill cost, checked against cash/reserve. Pass a plan covering the WHOLE base for restocking, not just a new addition (see `docs/library.md`).
- `tools/sell.py TICKER QTY` where to post an ask (undercutting the current best) vs hitting the bids now, plus expected hours to clear from 30-day traded-volume percentiles.
- `tools/chain.py TICKER` make-vs-buy cost tree plus sourcing depth of inputs.
- `tools/price.py` prices across exchanges with VWAP, volume, fill price for a quantity; `tools/book.py` order-book ladder.
- `tools/prun_scan.py`, `tools/prun_cxarb.py` legacy top-of-book scans; overstate thin markets. Baseline only. `tools/arb.py` replacement is on the roadmap.
+110
View File
@@ -0,0 +1,110 @@
# Using `puga` as a library (not the CLI)
The `tools/*.py` scripts cover the common questions. For anything more involved — a custom
what-if, a one-off cost breakdown with wages and capex, a market check the CLI doesn't expose —
import the modules directly and write a short script inline. This is normal; most answers in
this project's history came from a `python - <<EOF ... EOF` block, not a tool flag.
```sh
. .venv/bin/activate # or use .venv/bin/python directly
python - <<'EOF'
from puga import market, econ, fio, prunplanner as pp, saturation as sat
...
EOF
```
## Modules and what they give you
- **`puga.fio`** — public FIO REST, cached. `exchange_all()`, `order_book(mat, cx)` (full book,
`BuyingOrders`/`SellingOrders`), `recipes()`, `buildings()`, `materials()`, `planets_full()`,
`workforce_needs()`. `own(path, ttl=0)` for the player's authenticated data
(`/sites/{u}`, `/storage/{u}`, `/production/{u}` — has live `Efficiency`/`Condition` per
building, `/workforce/{u}/{planet}`, `/ship/ships/{u}`, `/ship/flights/{u}`). `me()` gives the
configured username.
- **`puga.prunplanner`** — `exchanges()` (has vwap_daily/7d/30d, traded volume — richer than FIO's
book totals), `recipes()`/`buildings()` (different field names than `fio`'s: `building_ticker`,
`material_ticker`, snake_case), `_g(path, ttl)` for any other `/data/...` GET (e.g.
`_g(f"/data/planet/{id}/", 3600)` for fertility, resources, COGC, build requirements).
`request(method, path, body)` for authenticated calls (`Api-Key`) — reads are always fine;
writes must go through `tools/plan_push.py`'s guardrails, don't call `request("POST"/"PUT"/"DELETE", "/planning/plan/...")` directly.
- **`puga.market`** — `snapshot(refresh=False)` returns `{(ticker, cx): Quote}` merging FIO book
totals with PRUNplanner's VWAP/volume; this is the one-stop source for prices, use it instead
of calling `fio`/`prunplanner` separately. `walk(mat, cx, qty, side, refresh=False)` walks the
live order book for a real fill price (`side="buy"` hits asks, `"sell"` hits bids; handles
market-maker orders with `ItemCount: None` as unlimited). `uni30(snap, tk)` = PRUNplanner's
"Universe 30D" price basis (volume-weighted 30d VWAP across all CX).
- **`puga.econ`** — pure formulas, no I/O, ported from PRUNplanner and tested against its own
suite plus live FIO: `tier_efficiency`, `workforce_consumption`, `building_efficiency`,
`production_io`, `daily_extraction`/`extraction_cycle`, `optimize_habs`, `CONSUMPTION` (the
per-tier need table), `HAB_CAP`/`HAB_AREA`, `TIERS`. This is what you use for "what if I
understaff this building" or "what's the real wage bill for N heads" — the CLI tools only
expose fixed slices of this.
- **`puga.saturation`** — `tref`, `n_out`, `p_patient`, `effective_supply`, `is_thin`: the
depth/persistence primitives behind `scan.py`. Reuse these instead of re-deriving a "how many
buildings can the market take" estimate by hand.
- **`puga.simulate`** — `simulate(plan, recipes, buildings, resources, fertility, price, ...)`:
the PRUNplanner-simulator replica. Reuse this for anything plan-shaped rather than reimplementing
efficiency/workforce/flows again.
- **`puga.config`** — `get(key)` reads `.env`; `state_path()` returns the real
`empire/state/company.yaml` if synced, else the tracked example.
## Patterns that come up a lot
**Price a basket at real fill cost** (construction lists, working stock, a chain's inputs):
```python
from puga import market, fio
mat = {m["Ticker"]: m for m in fio.materials()}
def price(items, cx="AI1"):
tot = wt = 0
for t, q in items.items():
w = market.walk(t, cx, q, "buy"); tot += w["total"]; wt += q * mat[t]["Weight"]
return tot, wt
```
**Building construction cost incl. MCG**, using either `fio.buildings()` (`Ticker`,
`BuildingCosts`, `AreaCost`) or `prunplanner.buildings()` (`building_ticker`, `costs`,
`area_cost`) — pick one client and stay consistent within a script, the field names differ.
**Efficiency of a hypothetical building**, e.g. staffing/COGC/expert what-ifs:
```python
from puga import econ
eff, elements = econ.building_efficiency(
dict(pioneers=40, settlers=10), {"pioneer": 1.0, "settler": 1.0},
expertise="METALLURGY", cogc="METALLURGY", experts={"METALLURGY": 2},
faction="ANTARES", permits_used=1, permits_total=2)
```
**Live production truth** (skip the model, read the game's own number):
```python
from puga import fio
for line in fio.own(f"/production/{fio.me()}", ttl=0):
print(line["Type"], line["Efficiency"], line["Condition"])
```
**A recipe's daily flows for N buildings**:
```python
io = econ.production_io([dict(time_ms=r["TimeMs"], inputs=..., outputs=...)], eff, n_buildings)
```
## `puga.simulate.simulate()` only knows what the plan tells it
`simulate()` (and anything built on it, e.g. `tools/buy.py`) computes material flows from the
plan_data's own building list — it has no idea what else exists at the base beyond the `built`
dict used for the construction-gap check. Pass a plan covering everything whose production or
consumption matters, not just the piece you're adding, or a self-sufficient material will look
like something you need to buy (a standalone HWP plan says "buy 42.8 AL/day"; the full base+HWP
plan nets that against the smelters to ~0). This bit a first version of `tools/buy.py`; caught by
comparing it against the by-hand restock calc, not by a unit test (the pure functions were right,
the plan file chosen was wrong).
## Conventions to keep
- Cache TTLs matter: market data is cached ~15 min, static game data ~24h (`puga/cache.py`).
Pass `refresh=True` if you just changed something in-game and need it now.
- `fio` uses PascalCase ticker/field names (`MaterialTicker`, `TimeMs`); `prunplanner` uses
snake_case (`material_ticker`, `time_ms`). Don't mix them in the same dict without converting.
- Never call `prunplanner.request` with a write verb outside `tools/plan_push.py` — the `[PuGa]`
name guard and dry-run default live there, not in the client.
- One-off scripts like these are fine to throw away; if a calculation gets reused more than once
or two, promote it into a function in the relevant module instead of re-deriving it in a shell
heredoc each time.
+5
View File
@@ -58,3 +58,8 @@ Sources: Prosperous Universe dev log #239 "Enlisting Labor" (https://prosperousu
- Consequences: a habitation building alone (HB2) gets nothing: capacity without a job requiring those workers requests none. The trigger is the production building. Build the housing first (capacity must exist), then the production building: the reserve tops it up at once, up to the instant allowance; the rest arrives at the next weekly report. Buildings run at efficiency = workers present / required until then.
- Report cadence seen on one planet: weekly, Wednesdays ~08:00 UTC (report #289 on 16 Sep 2026).
- The old handoff detail "bases keep 75% of workforce between reports" is not in these sources; treat as unverified.
## HQ in game vs PRUNplanner's "HQ" flag (observed 2026-09-21)
- The HQ screen (level 1, base permits 1 / 2) shows "Efficiency gains: Electronics 20.0%", and 23.3% for the next level (permits 1 / 3). That is exactly the faction bonus formula (Antares electronics 5% x 2 x (3 - 2 x used/total)), so the HQ display is the faction bonus.
- PRUNplanner's plan flag "HQ" (x1.1 on every building) is NOT visible in live efficiencies (smelters 1.3361, HWP 1.0696 both match the model without it). Keep it off. The HQ upgrade adds a base permit and raises the faction bonus.
- FIO `/sites` MaximumPermits (3) disagreed with the HQ screen (2). The HQ screen wins; `tools/state.py` supports `permits_total_override`.
+3
View File
@@ -16,6 +16,9 @@ Legend: [ ] todo, [x] done. Build order matters; each step is usable on its own.
10. [x] `tools/state.py sync` (sites, production efficiency, storage, ships, cash, permits): inventory, production, ships into `state/`.
11. [ ] Docs to fill: `docs/apex.md`, `docs/fio.md`, `docs/playbook.md` (question type -> tool), `docs/decisions.md`.
13. [x] `tools/buy.py`: cash-aware shopping list (construction gap vs empire state's built count, plus N days of net operating stock via simulate(), priced at real fill cost, checked against cash/reserve).
14. [x] `tools/sell.py`: ask-post-price + expected-clear-time advisor from 30d traded-volume percentiles, vs hitting the bids now.
12. [ ] `tools/portfolio.py`: choose the set of 1-building opportunities for one base that maximises total profit given shared whole-building housing (HB1/HB2/...), capex, permits and (for demolish-later) 60-day value decay. Persistence check exists (`tools/persistence.py`); wire it into the ranking.
## Open questions
+39
View File
@@ -0,0 +1,39 @@
import importlib.util
from pathlib import Path
spec = importlib.util.spec_from_file_location("buy", Path(__file__).resolve().parent.parent / "tools" / "buy.py")
buy = importlib.util.module_from_spec(spec)
spec.loader.exec_module(buy)
BUILDINGS = [
{"building_ticker": "HWP", "costs": [{"material_ticker": "BBH", "material_amount": 4}, {"material_ticker": "LTA", "material_amount": 2}], "area_cost": 25},
{"building_ticker": "HB2", "costs": [{"material_ticker": "BTA", "material_amount": 2}], "area_cost": 12},
]
PLAN = {"plan_data": {"buildings": [{"name": "HWP", "amount": 1}], "infrastructure": [{"building": "HB2", "amount": 1}]}}
def test_construction_items_include_mcg_and_full_amount_when_nothing_built():
need = buy.construction_items(PLAN, BUILDINGS, built={})
assert need == {"BBH": 4, "LTA": 2, "MCG": 100 + 48, "BTA": 2}
def test_construction_items_gap_only_counts_unbuilt_units():
need = buy.construction_items(PLAN, BUILDINGS, built={"HWP": 1, "HB2": 1})
assert need == {} # already fully built: nothing to buy
def test_construction_items_partial_gap():
plan = {"plan_data": {"buildings": [{"name": "HWP", "amount": 3}], "infrastructure": []}}
need = buy.construction_items(plan, BUILDINGS, built={"HWP": 1})
assert need == {"BBH": 8, "LTA": 4, "MCG": 200} # 2 more HWP needed, not 3
def test_stock_items_only_keeps_net_consumption():
flows = {"C": {"inp": 11.13, "out": 0}, "AL": {"inp": 42.78, "out": 44.57}, "BHP": {"inp": 0, "out": 14.26}}
got = buy.stock_items(flows, days=2)
assert got == {"C": 22.26} # AL nets to +1.78/day (own smelters cover the HWP): not bought
def test_net_of_on_hand_floors_at_zero():
got = buy.net_of_on_hand({"C": 20, "O": 5}, {"C": 30, "O": 2})
assert got == {"C": 0.0, "O": 3.0}
Executable
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Cash-aware shopping list: construction gap (plan vs what's already built) plus N days of the
plan's steady-state operating stock (from the simulator), priced at real order-book fill cost,
checked against cash. Formalizes the by-hand calc used repeatedly for buildouts and restocks.
puga buy plans/examples/hwp_buildout.yaml --days 2 # nothing built yet: full construction + 2d stock
puga buy plans/examples/hwp_buildout.yaml --days 4 # once built: construction gap is empty, just restock
puga buy --uuid <uuid> --days 3 --cash 72200 --reserve 3000
Materials already sitting in the base's storage (empire state) are subtracted from both sections.
IMPORTANT for --days (stock): pass a plan that lists EVERY production building at the base, not
just a new addition, or its own production won't net against consumption (e.g. a standalone HWP
plan will say "buy 42.8 AL/day" because it can't see your smelters; the full base+HWP plan nets
that to ~0). Use the full plan for restocking; a partial plan is fine for construction-only.
Limitation: on-hand storage in empire/state/company.yaml is not scoped per base; with more than
one base this will misattribute inventory. Fine while there is only one.
"""
import argparse, math, sys
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from puga import config, fio, market, prunplanner as pp
from puga.simulate import simulate
sys.path.insert(0, str(Path(__file__).resolve().parent))
import plan_push
def construction_items(plan: dict, buildings: list, built: dict) -> dict:
"""Materials needed for the plan's buildings/infra beyond what `built` (ticker -> count) already has."""
bl = {b["building_ticker"]: b for b in buildings}
d = plan["plan_data"]
need: dict[str, float] = {}
for tk, amt in [(b["name"], b["amount"]) for b in d["buildings"]] + [(i["building"], i["amount"]) for i in d["infrastructure"]]:
gap = amt - built.get(tk, 0)
if gap <= 0:
continue
info = bl[tk]
for c in info["costs"]:
need[c["material_ticker"]] = need.get(c["material_ticker"], 0) + c["material_amount"] * gap
need["MCG"] = need.get("MCG", 0) + 4 * info["area_cost"] * gap
return need
def stock_items(flows: dict, days: float) -> dict:
"""days worth of NET shortfall (consumption minus own production) from simulate()'s flows.
A material the plan both consumes and produces (e.g. AL: SME makes it, HWP eats it) nets out;
only buy what production doesn't cover."""
return {tk: -delta * days for tk, f in flows.items() if (delta := f["out"] - f["inp"]) < 0}
def net_of_on_hand(items: dict, on_hand: dict) -> dict:
return {tk: max(0.0, q - on_hand.get(tk, 0)) for tk, q in items.items()}
def price(items: dict, cx: str):
mat = {m["Ticker"]: m for m in fio.materials()}
rows, total, weight, volume = [], 0.0, 0.0, 0.0
for tk, qty in sorted(items.items()):
qty = math.ceil(qty)
if qty <= 0:
continue
w = market.walk(tk, cx, qty, "buy")
rows.append((tk, qty, w["avg"] or 0, w["total"], w["short"]))
total += w["total"]
m = mat.get(tk)
if m:
weight += qty * m["Weight"]
volume += qty * m["Volume"]
return rows, total, weight, volume
def main():
ap = argparse.ArgumentParser()
ap.add_argument("spec", nargs="?", help="plans/*.yaml describing the target setup")
ap.add_argument("--uuid", help="read the plan from the PRUNplanner account instead")
ap.add_argument("--days", type=float, default=3, help="days of operating stock on top of construction")
ap.add_argument("--cx", default=config.DEFAULT_CX)
ap.add_argument("--cash", type=float, help="override cash (default: empire state)")
ap.add_argument("--reserve", type=float, default=0, help="AIC to keep unspent")
ap.add_argument("--cargo", type=float, default=500.0, help="ship capacity t/m3, to flag multi-trip")
a = ap.parse_args()
recipes, blds = pp.recipes(), pp.buildings()
if a.uuid:
plan = pp.request("GET", f"/planning/plan/{a.uuid}/")
elif a.spec:
plan = plan_push.build_payload(yaml.safe_load(Path(a.spec).read_text()), recipes, {b["building_ticker"] for b in blds})
else:
sys.exit("give a spec file or --uuid")
st = yaml.safe_load(config.state_path().read_text())
cash = a.cash if a.cash is not None else st.get("cash", {}).get("AIC", 0)
base = next((b for b in st.get("bases", []) if b.get("planet") == plan["planet_natural_id"]), {})
built = base.get("buildings", {})
on_hand = next((s["items"] for s in st.get("storage", []) if s.get("type") == "STORE"), {})
if not st.get("hq"):
plan["plan_corphq"] = False
faction = st.get("company", {}).get("faction")
perm = (st.get("permits", {}).get("used", 1), st.get("permits", {}).get("total", 2))
planet = pp._g(f"/data/planet/{plan['planet_natural_id']}/", 3600)
price_ask = lambda t, side="buy": (lambda q: q.ask if q else None)(market.snapshot().get((t, a.cx)))
r = simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price_ask, faction, perm)
con = net_of_on_hand(construction_items(plan, blds, built), on_hand)
stk = net_of_on_hand(stock_items(r["flows"], a.days), on_hand)
con_rows, con_total, con_w, con_v = price(con, a.cx)
stk_rows, stk_total, stk_w, stk_v = price(stk, a.cx)
def show(title, rows):
if not rows:
return
print(f"\n{title}")
for tk, qty, avg, cost, short in rows:
have = on_hand.get(tk, 0)
print(f" {tk:5} {qty:8.0f} (have {have:5.0f}) avg {avg:7.0f} = {cost:9,.0f}{' SHORT BOOK' if short else ''}")
show(f"CONSTRUCTION (plan vs built {built or 'nothing'})", con_rows)
if con_rows:
print(f" subtotal {con_total:9,.0f} ({con_w:.0f} t, {con_v:.0f} m3)")
show(f"OPERATING STOCK, {a.days:g} days", stk_rows)
if stk_rows:
print(f" subtotal {stk_total:9,.0f} ({stk_w:.0f} t, {stk_v:.0f} m3)")
total, weight, volume = con_total + stk_total, con_w + stk_w, con_v + stk_v
budget = cash - a.reserve
print(f"\nTOTAL {total:,.0f} AIC ({weight:.0f} t, {volume:.0f} m3{' -- fits one trip' if max(weight, volume) <= a.cargo else ' -- NEEDS MULTIPLE TRIPS'})")
print(f"CASH {cash:,.0f} minus reserve {a.reserve:,.0f} = budget {budget:,.0f}")
if total <= budget:
print(f"AFFORDABLE, {budget - total:,.0f} left over")
elif con_total <= budget:
lo, hi = 0.0, a.days
for _ in range(20):
mid = (lo + hi) / 2
_, t, _, _ = price(net_of_on_hand(stock_items(r["flows"], mid), on_hand), a.cx)
lo, hi = (mid, hi) if con_total + t <= budget else (lo, mid)
print(f"SHORT by {total - budget:,.0f}: construction alone fits; stock affordable at ~{lo:.2f} days instead of {a.days:g}")
else:
print(f"SHORT by {total - budget:,.0f}: even construction alone ({con_total:,.0f}) exceeds budget")
if __name__ == "__main__":
main()
Executable
+57
View File
@@ -0,0 +1,57 @@
#!/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()
+3 -1
View File
@@ -58,7 +58,9 @@ def main():
st = _y.safe_load(config.state_path().read_text())
faction = None if a.no_faction else (st.get("company") or {}).get("faction")
perm = (st.get("permits", {}).get("used", 1), st.get("permits", {}).get("total", 2))
if not a.no_hq and st.get("hq"):
if a.no_hq:
plan["plan_corphq"] = False
elif st.get("hq"):
plan["plan_corphq"] = True
built = next((b.get("buildings", {}) for b in st.get("bases", []) if b.get("planet") == plan["planet_natural_id"]), {})
if a.cm_free:
+2
View File
@@ -30,6 +30,8 @@ def sync():
stores = fio.own(f"/storage/{u}", ttl=0)
ships = fio.own(f"/ship/ships/{u}", ttl=0)
st["permits"] = {"used": sites[0]["InvestedPermits"] if sites else 0, "total": sites[0]["MaximumPermits"] if sites else 0}
if st.get("permits_total_override"): # e.g. the HQ screen (base permits x / y) disagrees with FIO's MaximumPermits
st["permits"]["total"] = st["permits_total_override"]
old = {b["planet"]: b for b in st.get("bases", [])}
bases = []
for s in sites: