📦 FTSeller Wholesale Market Log in · Register

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.