Add CONTD contract evaluator, deadline-aware buy pricing, actpkg JSON generator

- puga/contd.py, tools/contd.py: price open CONTD contract offers against real
  buy-fresh cost, flagging conditions that can't be priced from the API alone.
- tools/buyat.py: cheapest bid price likely to fill by a hard deadline
  (stockout risk), using the empirical first-passage model from history.py.
- puga/actpkg.py, tools/actpkg.py: Refined PrUn XIT ACT buy-list JSON packages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 19:03:42 +02:00
co-authored by Claude Sonnet 5
parent dbe5cea9ae
commit 6bd5d6cc69
5 changed files with 284 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
"""Refined PrUn (github.com/refined-prun/refined-prun) XIT ACT action-package JSON - the semi-auto
buy-list format: importing one pre-fills the in-game CXPO buy tile for each material, the player
still clicks the real buy button themselves. Schema confirmed from the extension's own source
(src/store/user-data.types.d.ts, src/features/XIT/ACT/actions/cx-buy/cx-buy.ts, 2026-10-17).
Gotcha (verified against the real extension, not just reading the source): `global.name` (and group/
action names) get turned directly into an in-game command string by ActionPackageList.vue - spaces
become underscores but other punctuation survives into the command literally, and APEX's command
parser then rejects it with a generic "Illegal command" error on Edit/Execute (NOT on import, so it
looks like the JSON is fine until you actually open it). sanitize_name() strips anything but
letters/digits/spaces to avoid this.
"""
import re
def sanitize_name(name: str) -> str:
return re.sub(r"[^A-Za-z0-9 ]", "", name).strip()
def build_package(name: str, materials: dict[str, float], exchange: str = "AI1") -> dict:
"""materials: {ticker: qty}. Quantities are rounded up to whole units (CXPO buy amounts aren't
fractional)."""
name = sanitize_name(name)
group = sanitize_name(name.lower()) or "group"
import math
mats = {tk.upper(): math.ceil(qty) for tk, qty in materials.items() if qty > 0}
return {
"global": {"name": name},
"groups": [
{"type": "Manual", "name": group, "materials": mats},
],
"actions": [
{
"type": "CX Buy",
"name": f"Buy {name}",
"group": group,
"exchange": exchange,
"priceLimits": {},
"buyPartial": False,
"allowUnfilled": False,
"useCXInv": True,
}
],
}
+69
View File
@@ -0,0 +1,69 @@
"""Player-to-player CONTD contracts (FIO /contract/allcontracts/{user}) - the NPC logistics-officer
offers shown in APEX's CONTD panel, not the public Local Market hauling ads in puga/contracts.py.
Only OPEN contracts are real choices - exactly one of the pool of (usually 3) offers can be accepted;
CANCELLED/REJECTED/FULFILLED/PARTIALLY_FULFILLED are history or already-committed work.
Condition vocabulary seen in this account's full history (2026-10-16): PAYMENT, REPUTATION,
PROVISION_SHIPMENT and COMEX_PURCHASE_PICKUP (a material+qty+address you must supply - these are the
only ones with a real market buy-fresh cost), PICKUP_SHIPMENT/DELIVERY_SHIPMENT (logistics legs, no
separate cost beyond the material), and open-ended ones this module deliberately does NOT price
because their real cost depends on context this API can't see (a ship already flying that route, a
planet you were about to survey anyway, a ship already damaged): EXPLORATION (Address = planet to
survey), REPAIR_SHIP, HEADQUARTERS_UPGRADE, BASE_CONSTRUCTION, CONTRIBUTION, WAIT,
INCREASE_SATISFACTION. Judging whether one of those is actually cheap right now is the caller's job.
"""
from . import fio, market
MATERIAL_TYPES = {"PROVISION_SHIPMENT", "COMEX_PURCHASE_PICKUP"}
def open_contracts():
me = fio.me()
data = fio.own(f"/contract/allcontracts/{me}", ttl=60)
return [c for c in data if c["Status"] == "OPEN"]
def evaluate(contract: dict, cx: str) -> dict:
"""Payment, priced material requirements (buy-fresh cost at `cx` - assumes none already in free
stock, check storage for materials you may already hold), and any unpriced/contextual conditions
flagged separately for the caller to judge."""
payment = 0.0
reputation = False
requirements = [] # (ticker, qty, address)
flags = [] # (type, address) for conditions this module won't price
deadline_days = 0.0
for cond in contract["Conditions"]:
t = cond["Type"]
if t == "PAYMENT" and cond.get("Amount"):
payment += cond["Amount"]
elif t == "REPUTATION":
reputation = True
elif t in MATERIAL_TYPES and cond.get("MaterialTicker"):
requirements.append((cond["MaterialTicker"], cond["MaterialAmount"], cond.get("Address")))
elif t not in MATERIAL_TYPES | {"PAYMENT", "REPUTATION", "PICKUP_SHIPMENT", "DELIVERY_SHIPMENT"}:
flags.append((t, cond.get("Address")))
dl = cond.get("DeadlineDurationMs")
if dl:
deadline_days = max(deadline_days, dl / 86400000)
material_cost = 0.0
short = False
priced = []
for tick, qty, addr in requirements:
r = market.walk(tick, cx, qty, "buy")
priced.append({"ticker": tick, "qty": qty, "address": addr, "cost": r["total"], "short": r["short"]})
material_cost += r["total"]
short = short or r["short"]
return {
"id": contract["ContractLocalId"],
"partner": contract.get("PartnerName"),
"payment": payment,
"reputation": reputation,
"deadline_days": deadline_days,
"requirements": priced,
"material_cost": material_cost,
"net": payment - material_cost,
"short": short,
"flags": flags,
}