Market mechanics & the record
Deals
Every session each market publishes a menu of wholesale deals. A deal is:
- an item bundle (a primary item family plus optional add-on perks, with a grade and an origin) — the unit you buy;
- a landed unit cost — what you pay per unit;
- the resale (list) price — what a unit resells for on the market; it moves from session to session and listing to listing (see Prices move);
- a quoted sales velocity — expected units sold per session, a property of the item family;
- a maximum volume per order.
Landed costs and menu sizes move with market conditions. What the platform
publishes about them is what is on the menu itself — the resale price's mean,
range, open and close over the session's listings, the lowest, mean and
highest landed cost listed, the number of deals and desk bids (quote in the
API, the ticker in the terminal) — never the state of the engine behind it.
Prices move
The market core sets a reference resale price and landed cost for every
deal. A separate price process (xgames/sim/pricing.py, selected with
XGM_PRICING) overlays movement on those two numbers and nothing else:
- a market factor — a mean-reverting random walk (discrete Ornstein–Uhlenbeck) with mean zero, shared by every listing of a session;
- a family factor — a walk of the same kind per item family, so families drift apart for a while and come back;
- a path within the session — every listing gets a time in the session
(
listed_at, an evenly interleaved sequence) and the level walks from the previous session's close to this session's along a straight bridge plus a Brownian bridge, so each session has an open, a high, a low and a close; - a scatter per listing from a selectable distribution
(
XGM_PRICE_SCATTER: uniform, normal, triangular, none), mean zero.
The landed cost follows the same walk plus its own scatter, so the margin
keeps exactly the mean the core gave it. Every component has mean zero: the
expected price and cost of a deal are the core's reference values — the
process is neutral, and demand, downside events and the desk policy are the
core's alone. The process is deterministic in (seed, session, listing)
and is part of the frozen market identity, so a database keeps the process it
was recorded with (a database recorded before it existed runs with
XGM_PRICING=none).
Sales rank, rank drops and sellers (the core_v2 engine)
The default engine (core_v1) quotes each item family's sales velocity on the
menu and sells a position at its list price for the whole horizon. The
core_v2 engine (XGM_KERNEL=core_v2, xgames/sim/core_v2.py) replaces both
with what a reseller actually sees on a marketplace. A market's status says
which one it runs: quotes_velocity is true or false, and signals lists
the listing signals its menus carry.
Sales rank. Every item family is a listing in a large catalogue, ranked by recent sales (1 = best seller). Every unit the item sells — all its sellers together — lifts it in the catalogue, so its rank number drops; between sales the rank number climbs back. A slow mover saws between a good rank right after a sale and the far end of the catalogue; a fast mover sits near the top and wobbles. A menu publishes, per deal:
| Field | |
|---|---|
sales_rank |
the item's rank at the session open |
sales_rank_avg |
its mean over the last 30 sessions |
rank_drops |
rank drops over the last 30 sessions |
rank_drops_long |
rank drops over the last 90 sessions |
nsellers |
sellers competing on the item at the session open |
The rank is sampled several times per session, and a drop is a sample that
improved on the one before — the item sold in between. GET
/markets/{id}/families/{cluster}/signals serves an item's series session by
session (sales_rank and nsellers at each open, rank_drops_prev for the
session before), up to the open session and never beyond it.
No velocity is quoted. sales_velocity is discontinued on a core_v2
market: deals, families, orders, cart lines and trades carry no
sales_velocity / inverse_sv, the datasets no sv / isv, the session
statistics no sv / mean_sv; the tape's quoted and demand_index and the
analyser's quoted_velocity are null (the realised velocity of executed
deals — velocity on the tape, the analyser's history — is still there: it is
the record); the board has no velocity column, filter or sort — it sorts by
margin, sales rank or sellers. How fast an item sells is for the reader of the
signals to infer, and it takes care:
- the rank is relative — when the whole market sells less, ranks barely move, while drops (actual sales) thin out;
- drops saturate — a fast mover sells in every sample, and only samples that beat its fading score count, so its drops badly understate its sales;
- a slow mover's current rank says when it last sold, not how often it sells; the mean rank and the drops do;
- rank and drops measure the item, all sellers together — not what one more seller will get.
The desk's legacy policy reads the same menu: it sizes its bids from rank drops per session split evenly among the sellers and one more — a crude reading, badly short on fast movers, and blind to the sliding price.
Sellers. A position gets a share of the item's demand: one rotating share
among nsellers + 1, tilted a little either way. The crowd moves while the
position sells — the item's own seller count keeps changing, and a deal with a
fat margin attracts company: whoever was offered the same deal lands on the
same listing a few sessions later.
The sale price slides. A position does not sell at its list price for eight sessions. Every session the sale price slides from the list price, and the more sellers there are the faster it slides, with an occasional sharper undercut that is likelier in a crowd, down to a floor. Proceeds are the session's sales at that session's price, so slow stock in a crowded listing earns visibly less than the list price promised. The demand a position shares is the item's own — the same sales that move its rank on the following menus — so the menus and the record tell one story.
On the record. A trade carries the signals it was executed on and, session
by session as they happen, price_by_tick (the sale price) and
nsellers_by_tick (the sellers it competed with) next to sales_by_tick;
positions carry the same. Revenue everywhere — positions, the ledger, the
tape — is sales at those prices, and the realised result of a trade is the
plain cash arithmetic on sales_by_tick × price_by_tick. The datasets gain
the five signal columns in both frames. As always, nothing hidden is served:
not the item's velocity, not the crowd a deal will attract, not a price before
its session has happened.
The engine is part of the frozen market identity like any other: a database
recorded with core_v1 keeps running core_v1; core_v2 needs a fresh
XGM_DB_PATH.
Execution and settlement
At session close the desk buys the inventory of every funded order. Over the next 8 sessions each position sells into that deal's realised demand at list price; proceeds are credited to the account every session. Each unit still in stock costs a holding fee per session. After the horizon, unsold units are written off. Some deals sit in a hidden downside event: demand is cut sharply and a logistics surcharge of up to 55 % of the inventory cost basis is charged at settlement.
Realised profit of a position = revenue − inventory cost − fulfilment fee − holding fees − surcharge (write-offs are the inventory cost never recovered).
The desk's own book
The desk runs a legacy policy on every menu: it scores margin and velocity against a strictness level that drifts over time, executes deals above the threshold at a volume of roughly 1.25–2 sessions of expected demand, and takes a small share of exploratory positions. Its bids are shown on every menu (desk bids ×N) and its results are on record.
The record: executed vs rejected
- The desk has traded these markets for months before this platform went live;
that history is loaded into the record at first start (newest sessions
first; progress at
/api/v1/markets/{id}/coverage). - Since going live, every session close records what actually happened: the desk's executions plus every account holder's fill.
- A menu row is therefore either executed / observed — somebody actually bought it, and its session-by-session sell-through and realised result are on record — or rejected — nobody bought it and no outcome exists. Results of executed trades are revealed session by session: sales as they happen, the final P&L once the horizon has passed.
- Sales are observed, demand is not. A position reveals what it sold each session, capped by the stock it had. Once it sells out, demand beyond that stock is unknown, so the realised profit of an executed deal is on record at every volume only if it never sold out — otherwise only up to the executed volume. No engine state, driver or latent variable is ever served.
- The history API (
/history,/trades,/tape,/dataset.csv), the terminal, the asset browser and the deal analyser all serve this record and nothing else. Estimates built from it inherit the selection of whoever executed the trades. - Projections are naive by construction. The only forecasts on the platform — the trend overlay of the terminal and the deal analyser — are a least-squares line (or a plain average) through the observed points on screen, extended over the settlement sessions, and the plain cash arithmetic of a position applied to that path. Every input is shown; the API returns them so the fit can be redone.
- Menus are deterministic from the engine's recorded seed and configuration, frozen at first start and verified at every start.
For research deployments only, XGM_ORACLE=1 enables an oracle that also
reveals outcomes of rejected deals (history?reveal=full, extra series in the
explorer). It is off by default.
Datasets
dataset.csv?split=labeled returns executed trades of settled sessions
(date, key, listed_at, cluster, unit_cost, list_price, sv, isv, actor,
executed_qty, realized_pnl, units_sold, sold_out, qty, profit,
is_executed_qty — unit_cost and list_price are the listing's own moving
prices and listed_at its time in the session; with
expand_qty=true one row per volume at which the realised profit is knowable:
every volume for deals that never sold out, up to the executed volume for
deals that did).
split=unlabeled returns every menu row of recorded sessions with accepted
(executed by anyone), desk_policy_accepted and desk_qty, and no outcomes.
Fees (defaults)
| Fulfilment fee | 2.5 per unit, charged at funding |
| Holding cost | 0.5 per unit in stock, per session |
| Leftover write-off | 100 % of the unit cost of unsold units after 8 sessions |
| Downside surcharge | 0–55 % of inventory cost basis, only if a downside event hit the deal |
| Resale (list) price | around 34 per unit, moving with the price process (market and family walks, per-listing scatter) |
| Landed unit cost | roughly 10 to 38, drifting with market conditions and moving with the same walk |
Swapping the market engine
The engine behind the menus implements a small interface
(offers(tick), ground(offer), settlement()) in xgames/sim/kernel.py;
xgames/sim/core.py is the production engine, xgames/sim/core_v2.py the
engine with sales ranks, sellers and a sliding sale price (above), and a toy
random_walk engine is included as a template. Select with XGM_KERNEL. An
engine may publish listing signals on its offers and realise a sale price and a
seller count per settlement session (Grounding.price_path,
nsellers_path); one that does neither sells at the list price, as before. The price process wraps
whichever core is selected (xgames/sim/pricing.py), so a new core only has
to set reference prices — unless it reprices its listings itself
(own_price_process). An engine may also give its groundings sale terms
(Grounding.terms: a lead time before the units can sell, marketplace fees per
sale, a per-lot holding cost) and pool an account's lots of one SKU into a
stock that sells first-in first-out (pools_inventory, ground_lot): the
wholesale engine (xgames/wholesale/, see The wholesale market)
does both. Orders, settlement, cash, the record and the console are
engine-agnostic.
The book
This market has no limit order book: deals are listed at a cost and positions
sell into demand at the resale price. The book the terminal shows is a
reconstruction from the session tape alone (xgames/sim/orderbook.py,
GET /markets/{id}/book): the mid is the open menu's mean resale price; the
spread is Roll's estimator over session closes (falling back to the
Corwin–Schultz high–low estimator, then a floor); depth is sized to units sold
per holding period on the bid side and to inventory on hand on the ask side,
laid out on the hump-shaped average book profile; the tick-rule order flow of
the window tilts the two sides; Kyle's lambda is reported as price impact. It
is biased by construction and labelled reconstructed wherever it appears.