fix(flip-candidates): GE asset accounting + offer lifetime per governor 10390 (bounce-fix 6)

- Buy-limit reset NEVER cancels an offer: quote_age_policy now aborts
  only on a stale scanned edge or explicit strategy reallocation; the
  4h window is a capacity clock (limit_ledger), not an offer deadline.
  Removed every lapse-as-cancel trigger from best_practice, whens,
  quote_age_policy, a5/a8, worked example, risk, and the a6 verdict.
- Completed offers pass through the collection box: new canonical
  sub-states buy-filled-awaiting-collect / sell-filled-awaiting-collect;
  a full buy cannot sell and a full sell cannot close before ge_collect
  (scheduler dispatches collect for all four *-awaiting-collect states).
- Works the arrowtips example side-aware: a 4,000/10,000 partial BUY
  cancel returns 4,000 units + refunded CASH for the unfilled 6,000 —
  never 'refunded units'; the phantom 10,000-unit withdrawal is gone.
- conservation_violations() is now side-aware (buy: quoted =
  filled + still_open + cancelled_unfilled with acquired items = filled
  only, cash = spent + still_committed + refunded; sell: quoted =
  sold + still_open + returned_unsold) with slot_phase_violations()
  phase gates and committed negative fixtures: phantom refunded buy
  units, a full buy sold before collection, a full sell closed before
  proceeds collection. Existing tax/scheduler/limit fixtures preserved.
- grammar (ge_cancel/ge_collect), schema sub_states minItems, entry
  version 5 + sources updated accordingly.
This commit is contained in:
Claude 2026-08-28 14:48:55 -05:00
parent 02b366c7ef
commit 76f8e5ad27
4 changed files with 482 additions and 252 deletions

View File

@ -3,7 +3,7 @@ title: GE Flipping (margin trading)
domain: economy
tags: [flipping, merchanting, ge, money-making, hybrid]
status: verified
version: 4 # 4: bounce-fix 5 2026-08-28 (governor comment 10323) — per-slot asynchronous sub-state machine + global scheduler pass; ge_cancel moves everything to the GE collection box (ge_collect + ledger reconciliation before reuse); 4h buy-limit ledger distinct from offer expiry (cancellation never renews the limit); typed ceiling_gp_per_hour vs documented realized scenarios; conservation invariants; breaks with persisted per-slot state. Capped/floored tax model and its 4 selftest fixtures preserved (owner decision 2026-08-28).
version: 5 # 5: bounce-fix 6 2026-08-28 (governor comment 10390) — the 4h buy-limit reset NEVER cancels an offer (offer lifetime = independent quote_age_policy: stale edge or strategy reallocation only); explicit buy-filled-awaiting-collect / sell-filled-awaiting-collect states so completed offers must pass through the GE collection box (ge_collect) before reuse or closing; SIDE-AWARE per-slot ledgers (buy: quoted units = filled + still_open + cancelled_unfilled, acquired items = filled ONLY, cash = spent + still_committed + refunded; sell: quoted = sold + still_open + returned_unsold) with committed negative fixtures for phantom refunded buy units, a full buy sold before collection, and a full sell closed before proceeds collection. Capped/floored tax model + its 4 selftest fixtures and the asynchronous scheduler behavior preserved (owner decisions 2026-08-28).
updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
summary: >-
@ -13,18 +13,23 @@ summary: >-
quest requirements — capital breadth and slot discipline are the whole
game. Members accounts run 8 GE slots over the full candidate set; F2P
accounts get 3 slots and F2P-tradeable rows only (separate F2P path in
profit_drivers). Model (governor 10323, 2026-08-28): every GE slot is an
INDEPENDENT asynchronous sub-state machine (free / buy-open /
buy-cancelled-awaiting-collect / ready-to-sell / sell-open /
profit_drivers). Model (governor 10323 + 10390, 2026-08-28): every GE
slot is an INDEPENDENT asynchronous sub-state machine (free / buy-open /
buy-filled-awaiting-collect / buy-cancelled-awaiting-collect /
ready-to-sell / sell-open / sell-filled-awaiting-collect /
sell-cancelled-awaiting-collect / ready-to-requote / closed) driven by a
global scheduler pass that acts only on dispatchable slots and never
closes or advances unrelated ones; the 4h buy-limit ledger (limit_used /
first_fill_ts / limit_resets_at / remaining) is DISTINCT from offer
expiry and cancellation NEVER renews the limit; ge_cancel moves filled
items, refunds, unsold units and unspent coins to the GE collection box,
and a separate ge_collect withdraws and reconciles the per-slot ledger
before any reuse; every snapshot satisfies per-slot conservation
(quoted == still_offered + collected + refunded + sold; cash likewise);
closes or advances unrelated ones; completed offers (full buys and full
sells) land in the GE COLLECTION BOX and must pass through ge_collect +
ledger reconciliation before assets are reused or the slot closes; the
4h buy-limit ledger (limit_used / first_fill_ts / limit_resets_at /
remaining) is DISTINCT from offer expiry — a limit-window reset NEVER
cancels an offer and cancellation NEVER renews the limit; ge_cancel
moves filled items, refunded CASH, unsold units and proceeds to the box,
and every snapshot satisfies SIDE-AWARE per-slot conservation (buy:
quoted units == filled + still_open + cancelled_unfilled with acquired
items == filled only, cash == spent + still_committed + refunded; sell:
quoted == sold + still_open + returned_unsold);
economy.ceiling_gp_per_hour is the TYPED economic ceiling and
gp_per_hour_low/high are documented realized scenario bounds. Pinned
worked example + committed validator fixtures prove the model.
@ -66,8 +71,9 @@ best_practice:
why: "The buying rate is restricted to a set quantity every 4 hours per player, and
some items share a connected limit with related items — treat those as one bucket
when filling slots. The window starts when the FIRST unit of the window is bought
(first_fill_ts) and resets 4h later; cancelling an offer does NOT renew or restart
it."
(first_fill_ts) and resets 4h later. The reset ONLY restores buying capacity —
it never cancels, expires, or otherwise affects a resting offer — and cancelling
an offer does NOT renew or restart the window."
source: "OSRS wiki — Grand Exchange (Buy limits; Grand Exchange/Buying limits)"
- name: run the path that matches the account's slot set
why: "Members get eight GE slots and may trade any row; F2P accounts get three slots
@ -80,17 +86,22 @@ best_practice:
global scheduler pass acts only on dispatchable slots (free -> quote,
*-awaiting-collect -> ge_collect, ready-to-sell -> sell, ready-to-requote ->
requote) and leaves resting buy-open / sell-open / closed slots untouched — one
slot's cancel or close never advances an unrelated slot."
source: "governor review comment 10323 (2026-08-28), finding 3; docs/action-grammar.md"
slot's cancel or close never advances an unrelated slot. A FULLY FILLED offer
never sells or closes directly: it first enters an explicit
buy-/sell-filled-awaiting-collect state because its assets sit in the GE
collection box until ge_collect (finding 2, governor 10390)."
source: "governor review comment 10323 + 10390 (2026-08-28); OSRS wiki — Grand Exchange / Bank (collection box); docs/action-grammar.md"
- name: keep a per-slot offer ledger; ge_cancel -> collection box -> ge_collect -> reconcile, never strand units
why: "Every slot tracks quoted qty -> filled qty -> sold qty -> collected proceeds.
ge_cancel moves filled items, refunds, unsold units and unspent coins to the GE
COLLECTION BOX (never directly to inventory); a separate ge_collect withdraws
them, and the ledger is reconciled (conservation: quoted == still_offered +
collected + refunded + sold, cash likewise) before the slot is reused. The 4h
window lapse or a stale price triggers ge_cancel; the fresh-edge requote happens
only after collection. No items, coins, or GE slots are ever stranded."
source: "OSRS wiki — Grand Exchange (offer mechanics; 4h window); governor review comment 10323 finding 2; docs/action-grammar.md (ge_cancel / ge_collect)"
why: "Every slot tracks its side-aware ledger: a BUY quote partitions units into
filled + still_open + cancelled_unfilled with acquired items = filled ONLY (the
unfilled/cancelled quantity is CASH: spent / still_committed / refunded), and a
SELL quote partitions sold + still_open + returned_unsold. ge_cancel moves
filled items, refunded CASH, unsold units and sale proceeds to the GE COLLECTION
BOX (never directly to inventory); a separate ge_collect withdraws them and the
ledger is reconciled before the slot is reused. The ONLY cancel triggers are a
stale scanned edge (margin no longer positive) or an explicit strategy
reallocation — never the 4h buy-limit reset, which merely restores capacity."
source: "OSRS wiki — Grand Exchange (offer mechanics; collection); Bank (collection interface); governor review comment 10390 findings 1-4; docs/action-grammar.md (ge_cancel / ge_collect)"
economy:
# Model (documented, reproducible from the snapshot):
@ -118,9 +129,10 @@ economy:
# full-fill at their quoted edges; each 40-min rotation is 37 min of
# resting waits + 3 min of scan/place/collect actions.
# low 74,750 = 149,500 x 0.50 — 'requote-heavy' scenario: every 4h
# window's volume completes only after one ge_cancel + requote cycle
# (the no-fill remainder is refunded to the collection box and
# re-quoted on the next window), halving the window's filled volume.
# window's volume completes only after one stale-edge ge_cancel +
# requote cycle (the no-fill remainder is refunded to the collection
# box; the slot re-quotes when the margin clears again or capacity
# allows), halving the window's filled volume.
# Bounds nest: 0 <= 74,750 <= 138,000 <= 149,500 (validator-enforced).
item: Rune arrowtips # anchor: top candidate by modeled 4h/slot (members)
item_id: 44
@ -150,29 +162,34 @@ economy:
- "Members path — snapshot 2026-08-27 top 8 by modeled 4h/slot (floored+capped 2% tax): Rune arrowtips 170,000 (M, buy 223 / sell 244 / limit 10000); Iron bar 90,000 (F2P, 145/157/10000); Cosmic rune 72,000 (F2P, 122/128/18000); Magic logs 72,000 (M, 772/793/12000); Silver ore 65,000 (F2P, 73/79/13000); Mud rune 54,000 (M, 96/101/18000); Raw swordfish 45,000 (F2P, 206/213/15000); Raw lobster 30,000 (F2P, 106/110/15000) — total 598,000 per 4h = 149,500 gp/h CEILING on 8 slots"
- "F2P path — 3 slots, F2P-tradeable rows only: Iron bar 90,000 + Cosmic rune 72,000 + Silver ore 65,000 = 227,000 per 4h = 56,750 gp/h ceiling (alternates: Raw swordfish 45,000, Raw lobster 30,000)"
- "Post-2%-fee reality: 11 of 21 classic staples are net-positive at band edges for members; sub-2%-spread items (law/chaos/blood runes, dragon bones, adamantite/mithril bars) need sub-low buys or above-high sells"
- "Realized bounds are DOCUMENTED SCENARIOS with explicit arithmetic (governor 10323 finding 5 — no uncited fill/duty multipliers): HIGH 'resting-fill' 138,000 gp/h is the ceiling scaled by the rotation duty of 37 resting minutes per 40-minute rotation (149,500 x 37/40 = 138,287.5 -> 138,000): scan/place/collect actions cost ~3 min per rotation at the 600s quote-check cadence; LOW 'requote-heavy' 74,750 gp/h assumes every 4h window completes only after one ge_cancel + requote cycle, so the no-fill remainder (refunded to the collection box and re-quoted on the next window) halves the filled volume (149,500 x 0.50). The ceiling 149,500 itself is an economic bound, never an achieved rate."
- "Realized bounds are DOCUMENTED SCENARIOS with explicit arithmetic (governor 10323 finding 5 — no uncited fill/duty multipliers): HIGH 'resting-fill' 138,000 gp/h is the ceiling scaled by the rotation duty of 37 resting minutes per 40-minute rotation (149,500 x 37/40 = 138,287.5 -> 138,000): scan/place/collect actions cost ~3 min per rotation at the 600s quote-check cadence; LOW 'requote-heavy' 74,750 gp/h assumes every 4h window's volume completes only after one stale-edge ge_cancel + requote cycle, so the no-fill remainder (refunded to the collection box and re-quoted when the margin clears or capacity allows) halves the filled volume (149,500 x 0.50). The ceiling 149,500 itself is an economic bound, never an achieved rate. Cancelling never renews the buy limit and the limit reset never cancels an offer (governor 10390 finding 1)."
slot_state_machine:
sub_states: [free, buy-open, buy-cancelled-awaiting-collect, ready-to-sell, sell-open, sell-cancelled-awaiting-collect, ready-to-requote, closed]
sub_states: [free, buy-open, buy-filled-awaiting-collect, buy-cancelled-awaiting-collect, ready-to-sell, sell-open, sell-filled-awaiting-collect, sell-cancelled-awaiting-collect, ready-to-requote, closed]
slot_transitions:
free: [buy-open]
buy-open: [ready-to-sell, buy-cancelled-awaiting-collect]
buy-open: [buy-filled-awaiting-collect, buy-cancelled-awaiting-collect]
buy-filled-awaiting-collect: [ready-to-sell, free]
buy-cancelled-awaiting-collect: [ready-to-sell, ready-to-requote, free]
ready-to-sell: [sell-open]
sell-open: [closed, sell-cancelled-awaiting-collect]
sell-open: [sell-filled-awaiting-collect, sell-cancelled-awaiting-collect]
sell-filled-awaiting-collect: [closed]
sell-cancelled-awaiting-collect: [ready-to-requote, closed]
ready-to-requote: [buy-open, sell-open, free]
closed: []
slot_transition_whens:
- "free -> buy-open: scheduler pass quotes a resting buy at the low edge (qty = min(remaining 4h limit, slot capital, capital available))"
- "buy-open -> ready-to-sell: offer FULLY filled (filled_qty == quoted qty) — units in hand, dispatch sells this pass"
- "buy-open -> buy-cancelled-awaiting-collect: 4h window lapse, or scanned edge stale (margin no longer positive) — ge_cancel: filled items + refunded units + unspent coins ALL move to the GE collection box; the buy limit is NOT renewed by the cancel"
- "buy-cancelled-awaiting-collect -> ready-to-sell: ge_collect withdrawn units to inventory; sell edge positive — sell the units in hand"
- "buy-cancelled-awaiting-collect -> ready-to-requote: ge_collect withdrawn; buy margin still positive AND remaining limit > 0 — requote the unfilled balance at the fresh low edge"
- "buy-cancelled-awaiting-collect -> free: ge_collect withdrawn; slot released (margin gone or capital reallocated)"
- "buy-open -> buy-filled-awaiting-collect: offer FULLY filled (filled == quoted) — the units sit in the GE COLLECTION BOX until ge_collect; the slot may NOT sell them directly (governor 10390 finding 2)"
- "buy-open -> buy-cancelled-awaiting-collect: quote_age_policy abort — scanned edge stale (margin no longer positive) or explicit strategy reallocation; NEVER the 4h limit reset. ge_cancel moves filled items + refunded CASH (the unspent remainder) to the collection box; the buy limit is NOT renewed"
- "buy-filled-awaiting-collect -> ready-to-sell: ge_collect withdrawn the units; sell edge positive — dispatch sells"
- "buy-filled-awaiting-collect -> free: ge_collect withdrawn; margin gone or capital reallocated — bank the units, release the slot"
- "buy-cancelled-awaiting-collect -> ready-to-sell: ge_collect withdrawn the bought units; sell edge positive — sell the units in hand"
- "buy-cancelled-awaiting-collect -> ready-to-requote: ge_collect withdrawn; buy margin still positive AND remaining limit > 0 — requote at the fresh low edge"
- "buy-cancelled-awaiting-collect -> free: ge_collect withdrawn (full cash refund); slot released (margin gone or capital reallocated)"
- "ready-to-sell -> sell-open: scheduler pass quotes a resting sell at the high edge for the units in hand"
- "sell-open -> closed: sell offer fully filled, proceeds collected, ledger reconciled — slot freed"
- "sell-open -> sell-cancelled-awaiting-collect: 4h window lapse or stale edge — ge_cancel: unsold units return to the GE collection box (never to inventory)"
- "sell-open -> sell-filled-awaiting-collect: sell offer FULLY filled (sold == quoted) — the proceeds sit in the GE COLLECTION BOX until ge_collect; the slot may NOT close directly (governor 10390 finding 2)"
- "sell-open -> sell-cancelled-awaiting-collect: quote_age_policy abort — scanned edge stale or strategy reallocation; ge_cancel: unsold units + proceeds received so far move to the collection box (never to inventory)"
- "sell-filled-awaiting-collect -> closed: ge_collect withdrawn the proceeds; ledger reconciled — slot freed"
- "sell-cancelled-awaiting-collect -> ready-to-requote: ge_collect withdrawn; requote the sell at the fresh high edge or the current band"
- "sell-cancelled-awaiting-collect -> closed: ge_collect withdrawn; slot closed (exit taken)"
- "ready-to-requote -> buy-open: requoted buy placed (buy_capacity_ok: limit_used + qty <= limit until limit_resets_at)"
@ -180,53 +197,71 @@ slot_state_machine:
- "ready-to-requote -> free: slot abandoned and released"
scheduler_iteration: >-
One pass over every slot exactly once, at the 600 s quote-check cadence.
Only dispatchable slots act: free -> quote (ge_buy), buy-cancelled-
awaiting-collect / sell-cancelled-awaiting-collect -> ge_collect,
ready-to-sell -> sell (ge_sell), ready-to-requote -> requote. Resting
buy-open / sell-open / closed slots produce NO action and are never
closed, advanced, re-quoted or touched by the pass. A pass therefore
advances only the slots whose sub-state is dispatchable.
Only dispatchable slots act: free -> quote (ge_buy), buy-filled-
awaiting-collect / buy-cancelled-awaiting-collect /
sell-filled-awaiting-collect / sell-cancelled-awaiting-collect ->
ge_collect, ready-to-sell -> sell (ge_sell), ready-to-requote ->
requote. Resting buy-open / sell-open / closed slots produce NO action
and are never closed, advanced, re-quoted or touched by the pass. A
pass therefore advances only the slots whose sub-state is dispatchable,
and a completed offer always passes through its *-filled-awaiting-
collect state (ge_collect) before its assets are reused.
quote_age_policy: >-
INDEPENDENT of the 4h buy-limit ledger: resting offers are re-checked at
the 600 s cadence; an offer is aborted ONLY on a stale band edge
(scanned margin no longer positive) or the 4h window lapse — never
merely because it has been resting. An offer that still clears the
scanned margin keeps resting until it fills or the window lapses.
Offer lifetime is INDEPENDENT of the 4h buy-limit ledger. Resting
offers are re-checked at the 600 s cadence and kept resting while the
scanned margin stays positive — a profitable resting offer remains
open regardless of limit_resets_at (a limit-window reset only restores
buy capacity; it never cancels, expires, or implies cancelling an
offer). An offer is aborted ONLY on a stale band edge (scanned margin
no longer positive) or an explicit strategy reallocation (slot freed
for a better candidate / stop condition). No-fill alone never aborts an
offer whose edge still clears the margin.
persisted_state: >-
Per-slot record persisted between passes and across breaks: sub-state,
item, quoted qty, still_offered qty, collected qty, refunded qty, sold
qty, cash_quoted, still_offered_cash, collected_cash, refunded_cash,
limit_used, first_fill_ts, limit_resets_at, quote placed_at, last
check_ts. The GE offers themselves persist server-side; the record is
the agent's copy of truth for the scheduler and conservation checks.
item, quoted qty, and SIDE-AWARE buckets — buy side: filled, still_open,
cancelled_unfilled, cash_quoted, spent, still_committed, refunded;
sell side: sold, still_open, returned_unsold, proceeds_in_box,
cash_collected; plus limit_used, first_fill_ts, limit_resets_at,
quote placed_at, last check_ts. The GE offers themselves persist
server-side; the record is the agent's copy of truth for the scheduler
and conservation checks.
limit_ledger: >-
Per item: limit_used (units bought so far in the current 4h window),
first_fill_ts (when the FIRST unit of the window was bought),
limit_resets_at = first_fill_ts + 4h, remaining = limit - limit_used.
The ledger is DISTINCT from offer expiry: offers rest independently of
the window, and cancelling never renews or restarts it — a requote
The ledger controls ONLY the buying rate: it is DISTINCT from offer
expiry (offers rest independently of the window), cancelling never
renews or restarts it, and the reset never cancels an offer — a requote
before limit_resets_at may buy at most remaining (buy_capacity_ok:
limit_used + qty <= limit). A slot with limit_used == limit cannot
requote until limit_resets_at.
place a new or requoted buy until limit_resets_at; its resting offer
is unaffected.
conservation: >-
Per slot at every snapshot: quoted == still_offered + collected_items +
refunded + sold (the buckets partition the quote's units — no losses,
no duplication, no negatives); cash_quoted == still_offered_cash +
collected_cash + refunded_cash. Impossible pairs (units in a live offer
AND a terminal bucket) are violations: still_offered + sold, and
still_offered + refunded (cancel is atomic). ge_cancel moves every
filled / refunded / unsold unit and coin to the GE collection box;
ge_collect withdraws them and reconciles the ledger BEFORE any slot
reuse. Committed validator fixtures prove the invariants and the
scheduler dispatch.
SIDE-AWARE per-slot invariants at every snapshot (governor 10390
finding 4). BUY side: quoted units == filled + still_open +
cancelled_unfilled per item, and ACQUIRED ITEMS == filled ONLY — the
unfilled/cancelled quantity is a CASH refund, never refunded units;
cash_quoted == spent + still_committed + refunded. SELL side: quoted
units == sold + still_open + returned_unsold per item (a partial sell
legitimately spans sold + still_open). No negative or duplicated
quantities; impossible pairs (still_open + cancelled_unfilled on a
buy; still_open + returned_unsold on a sell) are violations. Phase
gates: a buy-filled-awaiting-collect slot has NOTHING sold yet (a full
buy cannot sell before ge_collect) and a sell-filled-awaiting-collect
slot holds its proceeds in the box; a 'closed' slot has no uncollected
proceeds or cancelled/returned units. Committed validator fixtures —
including the negative fixtures (phantom refunded buy units; a full
buy sold before collection; a full sell closed before proceeds
collection) — prove the invariants and the scheduler dispatch.
break_resume: >-
Breaks are taken only when every slot is resting (buy-open / sell-open)
or collected (closed) — never mid-collect or mid-place. Full state
Breaks are taken only when no slot is dispatchable: every slot is
resting (buy-open / sell-open), free, or collected (closed) — never
mid-collect, mid-place, or in an *-awaiting-collect state. Full state
(offers server-side + the persisted per-slot record) survives the
pause; resume by re-opening the GE, re-checking offers, re-running the
scheduler pass and continuing each slot from its persisted sub-state.
Slots and slots only: the 8-slot members / 3-slot F2P activity is never
downgraded to single-slot work.
Slots and slots only: the 8-slot members / 3-slot F2P activity is
never downgraded to single-slot work.
state_machine:
initial: idle
@ -342,7 +377,7 @@ actions:
verb: sleep
params:
delay: [6000, 12000]
until: "ge_offers_done changes or a fill event or the 600s cadence or 4h window lapse"
until: "ge_offers_done changes or a fill event or the 600s cadence (a resting offer keeps resting; the 4h limit reset plays no role in offer lifetime)"
on_fail: retry
anti_ban:
delay: [300, 900]
@ -353,8 +388,8 @@ actions:
target: "all quoted slots"
params:
phase: "per-slot: buy-open (buy outstanding) | sell-open (sell outstanding)"
per_slot: "still_offered vs quoted for every slot (partial fills stay attached to the resting offer)"
verdict: "full | partial | none | lapse | stale-edge"
per_slot: "side-aware buckets per slot — buy: filled (units in the collection box) / still_open (resting offer) / cancelled_unfilled; sell: sold (proceeds in the box) / still_open / returned_unsold — partial fills keep the offer resting with filled units held in the box"
verdict: "full | partial | none | stale-edge # 'lapse' is NOT a verdict: the 4h window is a capacity clock, not an offer deadline"
on_fail: retry
- id: a7
@ -362,7 +397,7 @@ actions:
verb: ge_sell
target: grand-exchange
params:
item: "per ready-to-sell slot (units in hand after full fill or ge_collect)"
item: "per ready-to-sell slot (units in hand AFTER ge_collect — a fully filled buy must first pass through buy-filled-awaiting-collect -> ge_collect)"
qty: "units in hand for that slot"
price: "high edge (rounded up)"
ledger: "record sold qty per slot; sub-state -> sell-open"
@ -373,11 +408,11 @@ actions:
- id: a8
step: cancel-to-box
verb: ge_cancel
target: "lapsed or stale slot offers (current phase)"
target: "slot offers aborted by quote_age_policy (stale edge / strategy reallocation)"
params:
offers: "the slots that hit the 4h window lapse or a stale edge"
moved: "filled items + refunded units + unsold units + unspent coins ALL move to the GE COLLECTION BOX (never directly to inventory)"
limit: "the 4h buy limit is NOT renewed or restarted by the cancel (limit_used stays)"
offers: "slots whose scanned band edge no longer clears the margin (stale), or slots explicitly reallocated by the strategy — NEVER offers whose 4h limit-window merely reset (a reset restores buy capacity only; quote_age_policy)"
moved: "BUY cancel: filled items + refunded CASH (the unspent remainder) -> GE COLLECTION BOX; SELL cancel: unsold units + proceeds received so far -> GE COLLECTION BOX (never directly to inventory)"
limit: "the 4h buy limit is NOT renewed or restarted by the cancel (limit_used stays); the next reset does not cancel anything either"
sub_state: "buy-open -> buy-cancelled-awaiting-collect; sell-open -> sell-cancelled-awaiting-collect"
on_fail: retry
@ -386,9 +421,9 @@ actions:
verb: ge_collect
target: grand-exchange
params:
withdraw: "from the GE collection box: filled items, refunds, unsold units, sale proceeds"
reconcile: "per-slot ledger conservation check (quoted == still_offered + collected + refunded + sold; cash likewise) BEFORE any slot reuse"
decide: "ready-to-sell (sell edge positive) | ready-to-requote (buy margin positive and remaining limit > 0) | free (release)"
withdraw: "from the GE collection box: bought (filled) items, refunded CASH, unsold units, sale proceeds — per slot"
reconcile: "SIDE-AWARE per-slot ledger conservation check BEFORE any slot reuse — buy: quoted == filled + still_open + cancelled_unfilled and cash_quoted == spent + still_committed + refunded; sell: quoted == sold + still_open + returned_unsold"
decide: "buy-filled/buy-cancelled -> ready-to-sell (sell edge positive) | ready-to-requote (buy margin positive and remaining limit > 0) | free (release); sell-filled/sell-cancelled -> closed | ready-to-requote"
log: "RESULT item, qty, net gp per slot (floored+capped tax)"
on_fail: alert
anti_ban:
@ -416,80 +451,100 @@ run_control:
- "no net-positive candidates on 3 consecutive scans"
- "3 consecutive failed/aborted passes (client issue — do not loop)"
# ================= WORKED EXAMPLE (pinned, deterministic — owner addendum 2026-08-28; slot-machine walk per governor 10323) =================
# ================= WORKED EXAMPLE (pinned, deterministic — owner addendum 2026-08-28; slot-machine walk per governor 10323, corrected per 10390) =================
# Members account, 8 GE slots, capital 22,500,000 gp. Snapshot prices 2026-08-27
# (buy/sell = low/high edge; tax = min(floor(0.02 x sell), 5,000,000)).
# The run below walks THREE slots in detail (arrowtips / iron / cosmic) exactly as
# the committed scheduler-dispatch + conservation fixtures; the other five slots
# behave identically to the iron slot and are omitted for brevity.
# Ledgers are SIDE-AWARE throughout: a BUY's acquired items are `filled` ONLY;
# unfilled/cancelled quantity is CASH (spent / still_committed / refunded).
#
# t0 idle -> scan -> schedule (pass 1): all slots free. Scheduler dispatch:
# free -> quote. Slot ledger after the pass (conservation holds):
# arrowtips: buy-open, quoted 10,000 @ 223; cash_quoted 2,230,000
# iron: buy-open, quoted 10,000 @ 145; cash_quoted 1,450,000 (limit_ledger entry: limit_used 0, first_fill_ts -, limit_resets_at -)
# = spent 0 + still_committed 2,230,000 + refunded 0
# iron: buy-open, quoted 10,000 @ 145; cash_quoted 1,450,000
# cosmic: buy-open, quoted 18,000 @ 122; cash_quoted 2,196,000
# Buckets: still_offered == quoted for every slot; collected/refunded/sold = 0.
# Unit buckets: quoted == still_open for every slot; filled/cancelled = 0.
#
# t1 wait, ~18 min: arrowtips PARTIAL fill 4,000/10,000 (a6 verdict PARTIAL).
# Ledger: arrowtips quoted 10,000 == still_offered 6,000 + collected_items 4,000
# (the 4,000 units sit in the player's possession — GE box — attached to the
# resting offer; nothing is stranded). limit_ledger: limit_used 4,000,
# first_fill_ts now, limit_resets_at = first_fill_ts + 4h; remaining 6,000.
# Cosmic: no fill; iron: FULL fill 10,000 -> ready-to-sell.
# BUY ledger: quoted 10,000 == filled 4,000 + still_open 6,000 + cancelled 0.
# Acquired items = 4,000 ONLY; they sit in the GE collection box (collectible
# at any bank), while the offer keeps resting with 6,000 still open. Cash:
# cash_quoted 2,230,000 == spent 892,000 + still_committed 1,338,000.
# limit_ledger: first_fill_ts now, limit_used 4,000, limit_resets_at = +4h,
# remaining 6,000 (window CAPACITY — not an offer deadline).
# Iron: FULL fill 10,000/10,000 -> buy-filled-awaiting-collect (units sit in
# the box until ge_collect; the slot cannot sell them directly — finding 2).
# Iron cash: spent 1,450,000. Cosmic: no fill; offer keeps resting (edge
# positive, quote age within policy — finding 1: no-fill alone never aborts).
#
# t2 cadence -> schedule (pass 2): dispatch ONLY the dispatchable slot:
# iron ready-to-sell -> sell-open (a7): 10,000 @ 157 (high edge).
# arrowtips buy-open -> NO ACTION (resting, quote age within policy).
# cosmic buy-open -> NO ACTION (resting, no fill, quote age within policy).
# iron buy-filled-awaiting-collect -> ge_collect (a9): withdraw 10,000 units;
# ledger reconciled (quoted 10,000 == filled 10,000; cash spent 1,450,000,
# no refund); sell edge positive -> ready-to-sell.
# arrowtips buy-open -> NO ACTION (resting, partial fill still open).
# cosmic buy-open -> NO ACTION (resting, no fill, edge positive).
# => the pass never closes or advances unrelated slots (regression fixture).
# Iron ledger: quoted 10,000 == still_offered 0 + sold 10,000 (buckets move on fill).
#
# t3 iron sell-open FULL fill -> collect: proceeds 1,570,000; tax floor(157 x 0.02)
# = 3/item -> net = (157 - 3 - 145) x 10,000 = 90,000. ge_collect (a9):
# cash withdrawn, ledger reconciled, sub-state -> closed, slot freed.
# Conservation: quoted 10,000 == sold 10,000 (all other buckets 0).
# t3 pass 3: iron ready-to-sell -> sell-open (a7): 10,000 @ 157 (high edge).
#
# t4 LAPSE / stale-edge handling (a8): at the 4h window edge, cosmic still has NO
# fill -> ge_cancel: the offer's cash 2,196,000 moves to the COLLECTION BOX as a
# refund. Ledger: cash_quoted 2,196,000 == still_offered_cash 0 + refunded_cash
# 2,196,000 (collected_cash 0). Sub-state -> buy-cancelled-awaiting-collect.
# arrowtips reaches limit_resets_at with 6,000 still offered -> ge_cancel:
# filled 4,000 (already in player possession) + refunded 6,000 units + unspent
# cash 6,000 x 223 = 1,338,000 ALL move to the collection box. Ledger:
# quoted 10,000 == collected_items 4,000 + refunded 6,000; limit_used STAYS
# 4,000 (the cancel does NOT renew the limit; remaining 6,000 stands).
# Sub-state -> buy-cancelled-awaiting-collect for both slots.
# t4 iron sell-open FULL fill -> sell-filled-awaiting-collect: proceeds
# 1,570,000 sit in the GE collection box until ge_collect. SELL ledger:
# quoted 10,000 == sold 10,000 + still_open 0 + returned_unsold 0.
#
# t5 cadence -> schedule (pass 3): dispatch the awaiting-collect slots -> ge_collect
# (a9). Withdraw + reconcile:
# arrowtips: units 10,000 (4,000 filled + 6,000 refund) -> inventory.
# Ledger: quoted 10,000 == collected_items 10,000 (refunded absorbed into
# collected on withdrawal). Buy margin still positive AND remaining limit
# 6,000 > 0. DECIDE (a9): ready-to-requote -> requote buy 6,000 @ 223
# (buy_capacity_ok: limit_used 4,000 + 6,000 = 10,000 <= 10,000 — allowed;
# a 6,001-unit requote would be BLOCKED until limit_resets_at — fixture).
# -> buy-open on the fresh edge.
# cosmic: cash 2,196,000 withdrawn (refunded_cash -> collected_cash 2,196,000).
# Margin still positive, limit_used 0 -> ready-to-requote -> buy-open 18,000
# @ 122 (full capacity available).
# iron: already closed (free).
# => ONLY the awaiting-collect slots acted; resting/closed slots untouched.
# t5 pass 4: iron sell-filled-awaiting-collect -> ge_collect (a9): withdraw
# 1,570,000; tax floor(157 x 0.02) = 3/item -> net (157 - 3 - 145) x 10,000
# = 90,000. proceeds_in_box -> cash_collected; ledger reconciled;
# sub-state -> closed, slot freed. Conservation: quoted 10,000 == sold 10,000.
#
# t6 Scheduler continues; arrowtips requote buy 6,000 fills -> ready-to-sell ->
# sell-open 10,000 @ 244 (4,000 in hand from the t1 fill + 6,000 from the
# requote) -> fills -> collect (a9): tax floor(244 x 0.02) = 4/item ->
# net = (244 - 4 - 223) x 10,000 = 170,000 for the window (matches the
# 170,000 4h/slot table row; limit_used 10,000 == limit -> no further
# requotes until limit_resets_at).
# t6 LIMIT RESET, NOT AN OFFER EVENT (finding 1): arrowtips reaches
# limit_resets_at with 6,000 still open on the resting offer. The window
# reset restores buying capacity ONLY — the offer KEEPS RESTING at its edge;
# nothing is cancelled and the 4,000 filled units stay collectible in the
# box. Cosmic likewise unaffected.
#
# t7 FIRST SCHEDULED BREAK after 6-10 passes (midpoint 8): every slot is resting
# (buy-open) or closed; per-slot records persisted (slot_state_machine.
# break_resume); take-break 60-240 s (a10); RESUME at a2 (re-open GE) -> a6
# (check-fills) -> schedule (next pass) -> continue per-slot from persisted
# sub-state. Breaks never strand an offer and never interrupt a collect/place.
# t7 arrowtips offer FULLY fills the remaining 6,000 -> buy-filled-awaiting-
# collect -> pass 5 ge_collect: withdraw 10,000 units (4,000 from the t1
# partial + 6,000 from the t7 fills — the units are what the offer actually
# bought; NO phantom "refunded units"). BUY ledger: quoted 10,000 == filled
# 10,000; cash 2,230,000 == spent 2,230,000 (still_committed 0 — a full fill
# spends its whole quote). Sell edge positive -> ready-to-sell.
# limit_ledger: window 1 used 4,000 (t1); window 2 (post-reset) used 6,000.
#
# t8 STOP: session target (e.g. 900,000 net) -> collect -> complete; capital fell
# below floor -> idle -> complete; 3 consecutive failed passes -> failed.
# t8 pass 6: arrowtips ready-to-sell -> sell-open 10,000 @ 244 -> full fill ->
# sell-filled-awaiting-collect -> pass 7 ge_collect: proceeds 2,440,000;
# tax floor(244 x 0.02) = 4/item -> net (244 - 4 - 223) x 10,000 = 170,000
# (matches the 170,000 4h/slot table row) -> closed.
#
# t9 STALE-EDGE recovery (cosmic, finding 1 cancel trigger): at a cadence check
# the cosmic LOW edge has risen 122 -> 126: net 128 - floor(128 x 0.02)=2 -
# 126 = 0 -> margin no longer positive -> quote_age_policy aborts the offer
# (ge_cancel, a8): ZERO units were filled, so the collection box receives
# refunded CASH 2,196,000 and NO items. BUY ledger: quoted 18,000 == filled 0
# + still_open 0 + cancelled_unfilled 18,000; cash 2,196,000 == spent 0 +
# still_committed 0 + refunded 2,196,000. Sub-state -> buy-cancelled-
# awaiting-collect. (The 18,000 never-purchased units are cash, never
# "refunded units" — the phantom-items fixture.)
#
# t10 pass 8: cosmic buy-cancelled-awaiting-collect -> ge_collect: withdraw
# refunded CASH 2,196,000; ledger reconciled (refunded -> collected). The
# fresh-edge margin is still 0, so the slot is NOT re-quoted: ready-to-
# requote requires a positive margin — decide free, capital reallocated to a
# candidate that still clears (e.g. magic logs); a requote in a later window
# is governed by buy_capacity_ok as usual (the cancel did NOT renew the
# window, and the next reset does not cancel anything).
#
# t11 FIRST SCHEDULED BREAK after 6-10 passes (midpoint 8): every slot is
# resting (buy-open / sell-open), free, or collected (closed) — nothing
# dispatchable; per-slot records persisted (slot_state_machine.break_resume);
# take-break 60-240 s (a10); RESUME at a2 (re-open GE) -> a6 (check-fills)
# -> schedule (next pass) -> continue per-slot from persisted sub-state.
# Breaks never strand an offer and never interrupt a collect/place.
#
# t12 STOP: session target (e.g. 900,000 net) -> collect -> complete; capital
# fell below floor -> idle -> complete; 3 consecutive failed passes -> failed.
#
# Rate qualification (typed ceiling vs realized scenarios — finding 5):
# ceiling 149,500 = 598,000 per 4h / 4h — ideal full fills of all 8 slots,
@ -506,20 +561,23 @@ risk:
notes:
- "Flipping is standard gameplay; the detectable signal is quote churn — keep per-offer pacing humanised, never instant-buy/sell loops."
- "The 2% fee (since 2025-05-29) and 5M/item cap make sub-2%-spread staples unprofitable at band edges; re-scan before every rotation."
- "Thin/volatile items: a resting offer can sit for hours — quote lifetime is bounded by the 4h window (lapse -> ge_cancel -> collection box -> ge_collect -> requote at a fresh edge), never by chasing."
- "Partial fills are normal; never cancel-and-replace a partially-filled offer mid-window just to churn — the ledger keeps the filled qty attached to the resting offer until full/lapse."
- "ge_cancel NEVER returns items/coins directly to inventory: everything lands in the GE collection box. Reuse a slot only after ge_collect + ledger reconciliation (conservation invariants)."
- "The 4h buy limit is independent of offer life: cancelling does not renew it. A slot at limit_used == limit cannot requote until limit_resets_at (first_fill_ts + 4h)."
- "Breaks are safe only at resting-offer or post-collect points (see a10); state = GE offers + the persisted per-slot record, which survive the pause."
- "Thin/volatile items: a resting offer can sit for hours — offer lifetime is governed by quote_age_policy (abort only on a stale scanned edge or strategy reallocation), NEVER by the 4h buy-limit window. A limit reset merely restores buy capacity; it cancels nothing."
- "Partial fills are normal: filled units of a partial buy sit in the GE collection box (collectible at any bank) while the offer keeps resting; never cancel-and-replace a partially-filled offer mid-window just to churn — the side-aware ledger keeps the filled qty attached until the offer fully fills or is aborted per quote_age_policy."
- "ge_cancel NEVER returns items/coins directly to inventory: everything lands in the GE collection box. BUY cancels return filled items + refunded CASH (the unfilled quantity is cash, never 'refunded units'); SELL cancels return unsold units + proceeds received. Reuse a slot only after ge_collect + side-aware ledger reconciliation (conservation invariants)."
- "The 4h buy limit is independent of offer life: cancelling does not renew it and the reset does not cancel offers. A slot at limit_used == limit cannot place a new/requoted BUY until limit_resets_at (first_fill_ts + 4h); its resting offer is unaffected."
- "Breaks are safe only when no slot is dispatchable (resting offers or post-collect points, see a10 and break_resume); state = GE offers + the persisted per-slot record, which survive the pause."
sources:
- "OSRS wiki — Grand Exchange (2% convenience fee since 2025-05-29, capped 5M/item, sub-50gp effectively untaxed, tax-exempt low-value items; buying rate restricted per 4h; connected limits; verified 2026-08-27)"
- "OSRS wiki — Grand Exchange/Buying limits (4h window mechanics; window starts on first purchase)"
- "OSRS wiki — Grand Exchange (2% convenience fee since 2025-05-29, capped 5M/item, sub-50gp effectively untaxed, tax-exempt low-value items; buying rate restricted per 4h window starting on the FIRST purchase of the window; connected limits; verified 2026-08-27)"
- "OSRS wiki — Grand Exchange/Buying limits (4h window mechanics; the limit controls how many units may be bought and resets 4h after the first unit is bought; outstanding offers remain active until filled or explicitly aborted — a limit reset is NOT offer expiry)"
- "OSRS wiki — Grand Exchange + Bank (collection interface): items/coins from full or partial trades — including aborted offers — remain in the collection box until collected; partially completed trades can be collected at any bank"
- "OSRS wiki — Merchanting (flipping = spread exploitation; high trade volume = safety; bulk flipping needs ~20M+ cash; avoid static-price items; offer matching at or better than your price)"
- "prices.runescape.wiki/api/v1/osrs/mapping (item ids, members flags, limits — limits cross-checked against Module:Exchange/<Item>, all agree)"
- "prices.runescape.wiki/api/v1/osrs/latest (high/low price band snapshot 2026-08-27)"
- "governor review comment 10323 on PR #7 (2026-08-28): per-slot asynchronous sub-state machine + scheduler pass (finding 3); ge_cancel -> collection box + ge_collect + reconciliation (finding 2); buy-limit ledger distinct from offer expiry, cancellation never renews (finding 1); conservation invariants (finding 4); typed ceiling vs documented realized scenarios (finding 5); explicit scheduler iteration + persisted break/resume, no slot downgrade (finding 6); owner decision 2026-08-28 (preserve capped/floored tax + 4 fixtures)"
- "docs/action-grammar.md (ge_cancel amended + ge_collect verb, 2026-08-28) + schemas/entry.schema.json (slot_state_machine, ceiling_gp_per_hour)"
- "governor review comment 10323 on PR #7 (2026-08-28): per-slot asynchronous sub-state machine + scheduler pass (finding 3); ge_cancel -> collection box + ge_collect + reconciliation (finding 2); buy-limit ledger distinct from offer expiry, cancellation never renews (finding 1); conservation invariants (finding 4); typed ceiling vs documented realized scenarios (finding 5); explicit scheduler iteration + persisted break/resume, no slot downgrade (finding 6)"
- "governor review comment 10390 on PR #7 (2026-08-28): the 4h limit reset never cancels offers — resting offers remain open while the margin is positive (finding 1); filled offers must pass through explicit *-filled-awaiting-collect states and ge_collect before reuse/close (finding 2); side-aware ledgers kill the phantom-units error (finding 3); side-aware conservation with negative fixtures, including sale-before-collection and close-before-proceeds-collection (finding 4)"
- "owner decisions 2026-08-28 (guidance comments 10264, 10271, 10334): capped/floored global tax formula; complete lifecycle contract; authorisation to keep repairing this same branch/PR"
- "docs/action-grammar.md (ge_cancel/ge_collect rows, 2026-08-28) + schemas/entry.schema.json (slot_state_machine, ceiling_gp_per_hour)"
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27
related:

View File

@ -68,9 +68,9 @@ Legend for the mapping table:
| `ge_open` | open GE at a bank | — | — | — |
| `ge_buy` | buy `qty` of `item` at offer price, paced | — | — | — |
| `ge_sell` | sell `qty` of `item`, confirm | — | — | — |
| `ge_cancel` | cancel a pending offer: filled items, refunds, unsold units and unspent coins ALL move to the GE COLLECTION BOX (never directly to inventory); the 4h buy limit is NOT renewed | — | — | — |
| `ge_cancel` | abort an offer (quote_age_policy only — stale edge or strategy reallocation, never a 4h limit-window reset): BUY — filled items + refunded CASH (unspent remainder) enter the GE COLLECTION BOX; SELL — unsold units + proceeds received so far enter the box; NEVER directly to inventory; the 4h buy limit is NOT renewed | — | — | — |
| `ge_offers_done` | predicate: no pending buy/sell offers | — | — | — |
| `ge_collect` | withdraw from the GE collection box to inventory: filled items, refunds, unsold units, sale proceeds; reconcile the per-slot ledger before any slot reuse | — | — | — |
| `ge_collect` | withdraw from the GE collection box: bought (filled) items, refunded CASH, unsold units, sale proceeds; reconcile the SIDE-AWARE per-slot ledger (buy: quoted = filled + still_open + cancelled_unfilled; sell: quoted = sold + still_open + returned_unsold) before any slot reuse | — | — | — |
| `ge_check_price` | read current buy/sell price of an item | — | — | — |
## F. Combat

View File

@ -72,7 +72,7 @@
"properties": {
"sub_states": {
"type": "array",
"minItems": 8,
"minItems": 10,
"items": { "type": "string" }
},
"slot_transitions": {

View File

@ -23,15 +23,21 @@ Runs:
`ge` while its income/cost model has nothing to do with flip tax.
- GE-flip lifecycle (flipping/merchanting entries): the 4h buy-limit
ledger (limit_used / limit_resets_at) is DISTINCT from offer
expiry — cancelling never renews the limit; every slot is an
independent asynchronous sub-state machine (canonical set:
free / buy-open / buy-cancelled-awaiting-collect / ready-to-sell /
sell-open / sell-cancelled-awaiting-collect / ready-to-requote /
closed) driven by a global scheduler pass that never touches
unrelated slots; ge_cancel moves filled items/refunds/proceeds/
unsold units to the GE collection box for a separate ge_collect;
per-slot conservation (quoted == still_offered + collected +
refunded + sold; no negative/duplicated quantities) is provable;
expiry — a limit-window reset never cancels an offer and cancelling
never renews the limit; every slot is an independent asynchronous
sub-state machine (canonical set: free / buy-open /
buy-filled-awaiting-collect / buy-cancelled-awaiting-collect /
ready-to-sell / sell-open / sell-filled-awaiting-collect /
sell-cancelled-awaiting-collect / ready-to-requote / closed) driven
by a global scheduler pass that never touches unrelated slots;
ge_cancel moves filled items, refunded CASH, unsold units and
proceeds to the GE collection box for a separate ge_collect;
per-slot conservation is SIDE-AWARE (buy: quoted units =
filled + still_open + cancelled_unfilled and acquired items =
filled ONLY — the unfilled/cancelled quantity is cash, never
refunded units; sell: quoted = sold + still_open + returned_unsold)
with phase gates (a full buy cannot sell before collection, a full
sell cannot close before proceeds collection);
economy.ceiling_gp_per_hour is the TYPED economic ceiling and
0 <= gp_per_hour_low <= gp_per_hour_high <= ceiling.
- Regression fixtures (--selftest): a fractional-tax flipping
@ -111,16 +117,20 @@ def ge_floor_tax_error(rel, data):
return None
# --- GE-flip lifecycle gates (owner guidance 2026-08-28, governor 10323) ---
# --- GE-flip lifecycle gates (owner guidance 2026-08-28, governor 10323 + 10390) ---
# Canonical per-slot sub-states: each GE slot is an INDEPENDENT asynchronous
# machine driven by a global scheduler pass; a pass never closes or advances
# a slot it did not touch.
# a slot it did not touch. A COMPLETED offer leaves its assets in the GE
# collection box until ge_collect: fully filled buys and sells each get an
# explicit *-filled-awaiting-collect state (governor 10390 finding 2).
SLOT_SUB_STATES = [
"free", # no offer, nothing held
"buy-open", # resting buy offer (partial fills stay attached)
"buy-cancelled-awaiting-collect", # aborted buy: assets sit in the GE collection box
"ready-to-sell", # units in hand, sell edge positive
"buy-open", # resting buy offer (partial fills keep resting)
"buy-filled-awaiting-collect", # buy FULLY filled — units sit in the GE collection box until ge_collect
"buy-cancelled-awaiting-collect", # aborted buy: bought units + refunded CASH in the box
"ready-to-sell", # units in hand (ge_collect'ed), sell edge positive
"sell-open", # resting sell offer (partial sells tracked)
"sell-filled-awaiting-collect", # sell FULLY filled — proceeds sit in the box until ge_collect
"sell-cancelled-awaiting-collect", # aborted sell: proceeds + unsold units in the box
"ready-to-requote", # collected units/refunds; requote or abandon
"closed", # slot finished, ledger reconciled, nothing held
@ -129,9 +139,11 @@ SLOT_SUB_STATES = [
SLOT_DISPATCH = {
"free": "quote",
"buy-open": None, # resting; fill/abort decided by policies
"buy-filled-awaiting-collect": "collect",
"buy-cancelled-awaiting-collect": "collect",
"ready-to-sell": "sell",
"sell-open": None, # resting; fill/abort decided by policies
"sell-filled-awaiting-collect": "collect",
"sell-cancelled-awaiting-collect": "collect",
"ready-to-requote": "requote",
"closed": None,
@ -148,28 +160,76 @@ def scheduler_dispatch(sub_state):
return SLOT_DISPATCH.get(sub_state)
def conservation_violations(record):
"""Per-slot conservation invariants (governor 10323 finding 4).
# Side-aware GE bucket names (governor 10390 finding 4).
# BUY: the player commits CASH; acquired items are `filled` ONLY — the
# unfilled/cancelled quantity is an unspent-CASH refund, never "refunded
# units". SELL: the player holds units; sold units yield proceeds.
BUY_UNIT_BUCKETS = ["filled", "still_open", "cancelled_unfilled"]
SELL_UNIT_BUCKETS = ["sold", "still_open", "returned_unsold"]
# Physically impossible co-occurrences per side (cancel is atomic: a live
# offer has no remainder once cancelled).
IMPOSSIBLE_UNIT_PAIRS = {
"buy": [("still_open", "cancelled_unfilled")],
"sell": [("still_open", "returned_unsold")],
}
record: dict with item buckets (still_offered, collected_items,
refunded, sold — each a {item: qty} map), quoted ({item: qty}), and cash
buckets (cash_quoted, still_offered_cash, collected_cash,
refunded_cash). Invariants:
- every quantity >= 0;
- per item: quoted == still_offered + collected_items + refunded + sold
(the buckets are a partition of the quote's units — no losses, no
duplicated, no negative quantities);
- physically impossible co-occurrences: units cannot sit in a live
offer AND already be sold or cancelled-refunded (cancel is atomic),
so an item may not appear in both {still_offered, sold} or both
{still_offered, refunded} — partial fills legitimately span
{still_offered, collected_items} until the offer is closed;
- cash: cash_quoted == still_offered_cash + collected_cash +
refunded_cash.
Returns a list of violation strings (empty = conserved).
def conservation_violations(record):
"""Side-aware per-slot GE conservation (governor 10390 finding 4).
record: {
'side': 'buy'|'sell', # REQUIRED — the ledger is
'quoted': {item: qty}, # side-aware
**unit buckets (side-specific): # {item: qty}
'cash_quoted': int, # BUY: cash committed
'spent'/'still_committed'/'refunded': int, # BUY cash partition
'proceeds_in_box'/'cash_collected': int, # SELL proceeds
'sub_state': str, # phase gate input
}
BUY invariants:
- unit partition: quoted == filled + still_open + cancelled_unfilled
per item;
- ACQUIRED ITEMS == filled ONLY: any sell-side unit bucket (sold /
returned_unsold) or a 'refunded' unit bucket on a buy record is a
violation — the unfilled/cancelled quantity is CASH (phantom
"refunded units" guard);
- cash partition: cash_quoted == spent + still_committed + refunded
(the unspent remainder of a buy is the cash refund);
- no item may be both still_open and cancelled_unfilled (cancel is
atomic).
SELL invariants:
- unit partition: quoted == sold + still_open + returned_unsold per
item;
- no item may be both still_open and returned_unsold (cancel is
atomic); sold + still_open IS legal (partial sell rest).
Shared: no negative quantities.
"""
out = []
buckets = ["still_offered", "collected_items", "refunded", "sold"]
side = record.get("side", "buy")
if side not in ("buy", "sell"):
out.append(f"record side {side!r} must be 'buy' or 'sell'")
return out
buckets = BUY_UNIT_BUCKETS if side == "buy" else SELL_UNIT_BUCKETS
foreign = {"filled", "still_open", "cancelled_unfilled", "sold",
"returned_unsold"} - set(buckets)
for b in sorted(foreign):
if record.get(b):
out.append(
f"{side} ledger uses {b} units — invalid for side {side}: "
f"buy-side unfilled/cancelled quantity is CASH "
f"(refunded), never {b} units; sell-side units partition "
f"into sold/still_open/returned_unsold")
if side == "buy":
# A unit map under a CASH field is the phantom-items guard: a buy's
# unfilled/cancelled quantity is an unspent-CASH refund, never
# "refunded units" (governor 10390 finding 4).
for c in ("cash_quoted", "spent", "still_committed", "refunded"):
if isinstance(record.get(c), dict):
out.append(
f"buy ledger carries {c} as units — the unfilled/"
f"cancelled quantity of a buy is CASH (refunded), "
f"never {c} units (phantom items)")
for b in buckets:
for item in record.get(b, {}):
q = record[b][item]
@ -182,34 +242,86 @@ def conservation_violations(record):
total = sum(record.get(b, {}).get(item, 0) for b in buckets)
if total != qq:
out.append(
f"item '{item}' quoted {qq} != still_offered + collected + "
f"refunded + sold ({total})")
# Physically impossible co-occurrences (the buckets are a
# partition of the quote's units; cancel is atomic and units in
# a live offer cannot already be sold/cancelled):
# still_offered + sold — units cannot be both in a live
# offer and already sold
# still_offered + refunded — a cancelled offer has no live
# remainder left in the offer
f"item '{item}' quoted {qq} != "
f"{' + '.join(buckets)} ({total})")
in_b = {b for b in buckets if item in record.get(b, {})}
for pair in (("still_offered", "sold"), ("still_offered", "refunded")):
if pair[0] in in_b and pair[1] in in_b:
for a, b in IMPOSSIBLE_UNIT_PAIRS[side]:
if (a in in_b and b in in_b
and record.get(a, {}).get(item)
and record.get(b, {}).get(item)):
out.append(
f"item '{item}' appears in multiple buckets "
f"({pair[0]} + {pair[1]} — physically impossible: "
f"units cannot be in a live offer and a terminal "
f"bucket at the same time)")
for c in ("cash_quoted", "still_offered_cash", "collected_cash", "refunded_cash"):
if record.get(c, 0) < 0:
out.append(f"{c} is negative ({record[c]})")
if record.get("cash_quoted", 0) != (
record.get("still_offered_cash", 0)
+ record.get("collected_cash", 0)
+ record.get("refunded_cash", 0)):
f"item '{item}' appears in both {a} and {b} — "
f"physically impossible (a cancelled offer has no "
f"live remainder)")
if side == "buy":
cash_fields = ["cash_quoted", "spent", "still_committed", "refunded"]
for c in cash_fields:
if isinstance(record.get(c), (int, float)) and record[c] < 0:
out.append(f"{c} is negative ({record[c]})")
cash_vals = {c: record.get(c) for c in cash_fields}
if all(isinstance(v, (int, float)) for v in cash_vals.values()):
if cash_vals["cash_quoted"] != (
cash_vals["spent"] + cash_vals["still_committed"]
+ cash_vals["refunded"]):
out.append(
f"buy cash quoted {cash_vals['cash_quoted']} != spent "
f"{cash_vals['spent']} + still_committed "
f"{cash_vals['still_committed']} + refunded "
f"{cash_vals['refunded']} — the unspent remainder of a "
f"buy is the CASH refund")
else:
for c in ("proceeds_in_box", "cash_collected"):
if record.get(c, 0) < 0:
out.append(f"{c} is negative ({record[c]})")
return out
def slot_phase_violations(record):
"""Phase gates (governor 10390 finding 4): assets may not be reused or
a slot closed before the GE collection box is withdrawn.
- 'buy-filled-awaiting-collect': filled must equal quoted (fully
filled) and NOTHING may have been sold yet — a full buy that sells
before ge_collect is a violation (owned units cannot leave the
collection box early).
- 'sell-filled-awaiting-collect': sold units require proceeds_in_box
> 0 — proceeds arrive in the box as units fill and only leave via
ge_collect.
- 'closed': nothing may remain uncollected — proceeds_in_box must be
0 and no cancelled/returned units may still be in the box (a full
sell may not close before proceeds collection).
"""
out = []
sub = record.get("sub_state")
side = record.get("side", "buy")
sold = sum((record.get("sold") or {}).values())
filled = sum((record.get("filled") or {}).values())
quoted = sum((record.get("quoted") or {}).values())
if side == "buy" and sub == "buy-filled-awaiting-collect":
if sold:
out.append(
"buy-filled-awaiting-collect slot has sold units — a full "
"buy cannot sell before ge_collect")
if filled != quoted:
out.append(
f"buy-filled-awaiting-collect slot is not fully filled "
f"(filled {filled} != quoted {quoted})")
if sub == "sell-filled-awaiting-collect" and sold and not record.get("proceeds_in_box"):
out.append(
f"cash quoted {record.get('cash_quoted')} != still_offered "
f"{record.get('still_offered_cash')} + collected "
f"{record.get('collected_cash')} + refunded {record.get('refunded_cash')}")
"sell-filled-awaiting-collect slot has sold units but no "
"proceeds_in_box — proceeds arrive in the collection box on "
"fill and leave only via ge_collect")
if sub == "closed":
if record.get("proceeds_in_box"):
out.append(
"closed slot still has proceeds_in_box — a full sell may "
"not close before proceeds collection (ge_collect first)")
cancelled = sum((record.get("cancelled_unfilled") or {}).values())
returned = sum((record.get("returned_unsold") or {}).values())
if cancelled or returned:
out.append(
"closed slot still holds cancelled/returned units — "
"collect them before closing the slot")
return out
@ -343,48 +455,100 @@ def run_selftest(verbose=False):
# --- per-slot scheduler dispatch: the worked example as a regression ---
# Governor 10323 finding 3 + owner: reproduce the simultaneous
# partial-buy / full-sell-collect / no-fill-buy case and prove no
# unrelated slot is closed or advanced.
slot_states = {"arrowtips": "buy-open", # partial fill 4,000/10,000 — resting
"iron": "ready-to-sell", # full fill — sell this pass
"cosmic": "buy-open"} # no fill, within quote age — resting
# partial-buy / full-buy-fill / no-fill-buy case and prove no
# unrelated slot is closed or advanced. Under governor 10390 finding 2
# a FULL buy fill goes to buy-filled-awaiting-collect (assets sit in
# the GE collection box) — the pass dispatches ge_collect, it never
# lets the slot sell directly.
slot_states = {"arrowtips": "buy-open", # partial fill 4,000/10,000 — resting
"iron": "buy-filled-awaiting-collect", # full fill — ge_collect this pass
"cosmic": "buy-open"} # no fill, edge positive — resting
dispatched = {name: scheduler_dispatch(s) for name, s in slot_states.items()}
ok = dispatched == {"arrowtips": None, "iron": "sell", "cosmic": None}
check("worked example: partial-buy + full-sell + no-fill slots dispatch only the iron slot to sell; unrelated slots stay untouched",
ok = dispatched == {"arrowtips": None, "iron": "collect", "cosmic": None}
check("worked example: partial-buy + full-buy-fill + no-fill slots dispatch only the iron slot to ge_collect; unrelated slots stay untouched",
ok)
check("resting slots (buy-open) are never closed or advanced by a pass",
dispatched.get("arrowtips") is None and dispatched.get("cosmic") is None)
check("a fully filled buy must pass through buy-filled-awaiting-collect (ge_collect) before any sell",
scheduler_dispatch("buy-filled-awaiting-collect") == "collect"
and scheduler_dispatch("sell-filled-awaiting-collect") == "collect"
and scheduler_dispatch("ready-to-sell") == "sell")
# --- conservation invariants (finding 4) ---
# Balanced snapshot mirroring the worked example: 10,000 Rune arrowtips
# quoted, 4,000 filled (units in the player's possession), 6,000 still
# resting in the live offer; plus an independent cash quote (2,230 GP)
# fully committed in live buy offers.
clean_rec = {
"quoted": {"Rune arrowtips": 10},
"still_offered": {"Rune arrowtips": 6},
"collected_items": {"Rune arrowtips": 4},
"refunded": {},
"sold": {},
"cash_quoted": 2230, "still_offered_cash": 2230,
"collected_cash": 0, "refunded_cash": 0,
# --- side-aware conservation invariants (finding 4) ---
# BUY balanced snapshot mirroring the worked example t1: 10,000 Rune
# arrowtips quoted @ 223; 4,000 filled (units in the GE collection
# box), 6,000 still open on the resting offer; cash 2,230,000 =
# spent 892,000 + still_committed 1,338,000 + refunded 0.
buy_partial = {
"side": "buy",
"quoted": {"Rune arrowtips": 10_000},
"filled": {"Rune arrowtips": 4_000},
"still_open": {"Rune arrowtips": 6_000},
"cancelled_unfilled": {},
"cash_quoted": 2_230_000, "spent": 892_000,
"still_committed": 1_338_000, "refunded": 0,
}
check("conservation: a balanced per-slot ledger (partial fill split across still_offered + collected, partition sums to quoted) has zero violations",
conservation_violations(clean_rec) == [])
dup_rec = dict(clean_rec, quoted={"Rune arrowtips": 10},
still_offered={"Rune arrowtips": 4},
sold={"Rune arrowtips": 6})
check("conservation: units both in a live offer AND already sold (impossible pair) is a violation",
any("multiple buckets" in v for v in conservation_violations(dup_rec)))
over_rec = dict(clean_rec, collected_items={"Rune arrowtips": 7})
check("conservation: over-reported buckets break the quoted partition (sum 13 != quoted 10)",
any("quoted 10 !=" in v for v in conservation_violations(over_rec)))
neg_rec = dict(clean_rec, sold={"Rune arrowtips": -1})
check("conservation: BUY side — partial fill splits units into filled + still_open and cash into spent + still_committed (partition sums hold, zero violations)",
conservation_violations(buy_partial) == [])
buy_cancelled = dict(buy_partial, still_open={},
cancelled_unfilled={"Rune arrowtips": 6_000},
still_committed=0, refunded=1_338_000)
check("conservation: BUY side — a cancelled partial buy carries filled units + refunded CASH (still_open -> cancelled_unfilled + refunded), zero violations",
conservation_violations(buy_cancelled) == [])
phantom_rec = dict(buy_cancelled, refunded={"Rune arrowtips": 6_000})
check("conservation: BUY side — 'refunded UNITS' on a buy is a phantom (unfilled/cancelled quantity is CASH): violation",
any("CASH" in v or "invalid for side" in v
for v in conservation_violations(phantom_rec)))
sold_on_buy = dict(buy_partial, sold={"Rune arrowtips": 4_000})
check("conservation: BUY side — a full buy that sells before ge_collect (sold units on a buy ledger) is a violation",
any("invalid for side" in v for v in conservation_violations(sold_on_buy)))
buy_cash_bad = dict(buy_partial, still_committed=1_380_000)
check("conservation: BUY side — cash partition mismatch (spent + still_committed + refunded != cash_quoted) is a violation",
any("cash quoted" in v for v in conservation_violations(buy_cash_bad)))
# SELL balanced: 10,000 sold @ 157, proceeds 1,570,000 in the box.
sell_full = {
"side": "sell",
"quoted": {"Iron bar": 10_000},
"sold": {"Iron bar": 10_000},
"still_open": {}, "returned_unsold": {},
"proceeds_in_box": 1_570_000, "cash_collected": 0,
}
check("conservation: SELL side — full sell partitions sold + still_open + returned_unsold, proceeds in the box (zero violations)",
conservation_violations(sell_full) == [])
sell_partial = dict(sell_full, sold={"Iron bar": 4_000},
still_open={"Iron bar": 6_000},
proceeds_in_box=628_000)
check("conservation: SELL side — a partial sell legitimately spans sold + still_open (zero violations)",
conservation_violations(sell_partial) == [])
sell_dup = dict(sell_full, sold={"Iron bar": 6_000},
still_open={"Iron bar": 3_000},
returned_unsold={"Iron bar": 1_000})
check("conservation: SELL side — units both in a live offer AND returned (impossible pair) is a violation",
any("both still_open and returned_unsold" in v
for v in conservation_violations(sell_dup)))
over_rec = dict(buy_partial, filled={"Rune arrowtips": 7_000})
check("conservation: over-reported buckets break the quoted partition (sum 13,000 != quoted 10,000)",
any("quoted 10000 !=" in v for v in conservation_violations(over_rec)))
neg_rec = dict(sell_full, sold={"Iron bar": -1})
check("conservation: a negative bucket is a violation",
any("negative" in v for v in conservation_violations(neg_rec)))
cash_rec = dict(clean_rec, collected_cash=900)
check("conservation: cash quoted != still_offered + collected + refunded is a violation",
any("cash quoted" in v for v in conservation_violations(cash_rec)))
# --- phase gates (finding 4: assets cannot be reused/closed pre-collect) ---
check("phase gate: a full buy that sells before ge_collect (buy-filled-awaiting-collect with sold units) is rejected",
any("cannot sell before ge_collect" in v
for v in slot_phase_violations(dict(buy_partial, sub_state="buy-filled-awaiting-collect",
filled={"Rune arrowtips": 10_000}, still_open={},
sold={"Rune arrowtips": 10_000}))))
check("phase gate: a full sell that closes before proceeds collection (closed with proceeds_in_box > 0) is rejected",
any("before proceeds collection" in v
for v in slot_phase_violations(dict(sell_full, sub_state="closed"))))
check("phase gate: a sell-filled-awaiting-collect slot with sold units but no proceeds_in_box is rejected",
any("no proceeds_in_box" in v
for v in slot_phase_violations(dict(sell_full, sub_state="sell-filled-awaiting-collect",
proceeds_in_box=0))))
check("phase gate: a closed slot that has been fully collected (no proceeds_in_box, no cancelled/returned units) passes",
slot_phase_violations(dict(sell_full, sub_state="closed",
proceeds_in_box=0, cash_collected=1_570_000)) == [])
# --- 4h buy-limit ledger vs offer expiry (finding 1) ---
check("buy limit: a requote within the remaining capacity is allowed (4,000 used of 10,000 -> 6,000)",
@ -394,29 +558,37 @@ def run_selftest(verbose=False):
check("buy limit: a cancelled offer does NOT renew the limit (10,000 used of 10,000 -> any requote blocked before reset)",
not buy_capacity_ok(limit_used=10000, limit=10000, qty_requested=1))
# --- structured lifecycle gates (findings 1-5) ---
# --- structured lifecycle gates (findings 1-5 + governor 10390) ---
margin_tags = ["flipping"]
ssm_ok = {
"sub_states": SLOT_SUB_STATES,
"slot_transitions": {
"free": ["buy-open"],
"buy-open": ["ready-to-sell", "buy-cancelled-awaiting-collect"],
"buy-cancelled-awaiting-collect": ["ready-to-sell", "free"],
"buy-open": ["buy-filled-awaiting-collect",
"buy-cancelled-awaiting-collect"],
"buy-filled-awaiting-collect": ["ready-to-sell", "free"],
"buy-cancelled-awaiting-collect": ["ready-to-sell", "ready-to-requote", "free"],
"ready-to-sell": ["sell-open"],
"sell-open": ["closed", "sell-cancelled-awaiting-collect"],
"sell-open": ["sell-filled-awaiting-collect",
"sell-cancelled-awaiting-collect"],
"sell-filled-awaiting-collect": ["closed"],
"sell-cancelled-awaiting-collect": ["ready-to-requote", "closed"],
"ready-to-requote": ["buy-open", "sell-open", "free"],
"closed": [],
},
"slot_transition_whens": [
"free -> buy-open: quote placed",
"buy-open -> ready-to-sell: offer fully filled",
"buy-open -> buy-cancelled-awaiting-collect: abort (stale edge / quote age)",
"buy-open -> buy-filled-awaiting-collect: offer FULLY filled — units sit in the GE collection box until ge_collect",
"buy-open -> buy-cancelled-awaiting-collect: abort on stale edge (margin no longer positive) or strategy reallocation — never on the 4h limit reset",
"buy-filled-awaiting-collect -> ready-to-sell: ge_collect withdrawn units; sell edge positive",
"buy-filled-awaiting-collect -> free: ge_collect withdrawn; margin gone, slot released",
"buy-cancelled-awaiting-collect -> ready-to-sell: collected, sell edge positive",
"buy-cancelled-awaiting-collect -> free: collected, full refund, slot freed",
"buy-cancelled-awaiting-collect -> ready-to-requote: collected; margin positive and capacity remains",
"buy-cancelled-awaiting-collect -> free: collected, full cash refund, slot freed",
"ready-to-sell -> sell-open: sell offer placed",
"sell-open -> closed: fully filled and proceeds collected",
"sell-open -> sell-cancelled-awaiting-collect: abort",
"sell-open -> sell-filled-awaiting-collect: sell FULLY filled — proceeds sit in the box until ge_collect",
"sell-open -> sell-cancelled-awaiting-collect: abort on stale edge or strategy reallocation",
"sell-filled-awaiting-collect -> closed: proceeds collected and ledger reconciled",
"sell-cancelled-awaiting-collect -> ready-to-requote: collected, requote",
"sell-cancelled-awaiting-collect -> closed: collected, slot closed",
"ready-to-requote -> buy-open: requoted buy",
@ -424,17 +596,17 @@ def run_selftest(verbose=False):
"ready-to-requote -> free: abandoned",
],
"scheduler_iteration": "one pass over all slots; dispatch pending actions only",
"quote_age_policy": "resting offers re-checked every 600 s; stale edges requoted",
"persisted_state": "per-slot record: sub-state + quoted/filled/sold/collected",
"limit_ledger": "limit_used + first_fill_ts + limit_resets_at, independent of offer expiry",
"conservation": "quoted == still_offered + collected + refunded + sold",
"quote_age_policy": "offers rest while the scanned margin stays positive; aborted only on a stale edge or strategy reallocation — the 4h limit reset never cancels an offer",
"persisted_state": "per-slot record: sub-state + side-aware quoted/filled/sold/collected buckets",
"limit_ledger": "limit_used + first_fill_ts + limit_resets_at, independent of offer expiry; cancellation never renews it",
"conservation": "side-aware: buy quoted units = filled + still_open + cancelled_unfilled and acquired items = filled only; sell quoted = sold + still_open + returned_unsold",
"break_resume": "breaks between passes; state persists in the per-slot record",
}
flip_ok = {"tags": margin_tags, "economy": {
"income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 17,
"ceiling_gp_per_hour": 149500, "gp_per_hour_low": 102600,
"gp_per_hour_high": 138800}, "slot_state_machine": ssm_ok}
check("flip lifecycle: ceiling + nested realized bounds + canonical slot machine passes",
check("flip lifecycle: ceiling + nested realized bounds + canonical 10-state slot machine passes",
ge_lifecycle_errors("ok.yaml", flip_ok) == [])
errs = ge_lifecycle_errors("bad.yaml", {"tags": margin_tags, "economy": {
"income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 17,