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:
parent
02b366c7ef
commit
76f8e5ad27
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -72,7 +72,7 @@
|
||||
"properties": {
|
||||
"sub_states": {
|
||||
"type": "array",
|
||||
"minItems": 8,
|
||||
"minItems": 10,
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"slot_transitions": {
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user