jengine-market
An order-book market - limit and market orders, price-time matching, escrow, NPC producers and consumers, price history - the economy of Saltwind.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-market |
| Version | 0.1.0 |
| Entry point | jengine_market::MarketPackage |
Using it
Section titled “Using it”List it in the top-level "packages" array of project.jengine:
{ "packages": [ { "name": "jengine-market", "source": { "type": "path", "path": "../../packages/jengine-market" } } ]}Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the
editor’s Rebuild viewport banner). Native and web builds link it on their own.
From packages/jengine-market/README.md.
An order-book market as plain, unit-tested Rust: goods, limit and market orders matched by price then time, partial fills, cancels, escrow and settlement into accounts, fees, NPC producers, consumers, market makers and townsfolk with a target price and an elasticity, price history as OHLC candles - and serde on all of it, so a whole market is one stored document.
It is the economy of Saltwind (projects/saltwind), where every port’s
market is one Market kept in the server’s store: an offer a captain leaves
in Port Coralis fills while they are at sea, and is still in the book after
the server restarts. Nothing in it is about ships or ports, though - any game
whose players trade with each other can use it: Thornwick’s trade between
towns, a space game’s stations, an MMO’s auction house.
The package registers no components and no script commands: a market
lives in a game’s server, which calls it, and the client draws what the
server sends (Saltwind’s port room is the worked example). It is a valid
JEnginePackage so a project may still list it.
The model
Section titled “The model”| Type | What it is |
|---|---|
Market | one place’s trading floor: a Book per good, the Accounts of everyone who trades there, the Agents, the History, the fee pot, the Flows, a clock |
Account | someone’s warehouse here: free coins, free goods, and news - their fills since they last looked |
Order | a resting order: owner, good, side, price, units left, units placed, when |
Book | bids best (highest) first, asks best (lowest) first, oldest first at one price |
Fill | one trade: price, units, buyer, seller, fee, which side was resting |
Agent | an NPC trader (Role::Producer, Consumer, Maker, Taker) |
History / Candle | OHLC candles per period of the market’s clock, the last price |
Flows | every coin and unit that came in or went out, by kind |
Coins and units are whole numbers (i64). Account ids are strings; ids that
start with npc: are NPCs (no news is kept for them).
Trading
Section titled “Trading”use jengine_market::{Agent, Market, Price, Side};
let mut m = Market::new(&["sugar", "rum"], 100, 30.0); // 1% fee on sales, 30 s candlesm.add_agent(Agent::producer("npc:sugar", "sugar", 11, 80, 300.0));m.advance(10.0); // NPCs post their quotes
m.deposit_coins("ada", 500);let placed = m.place("ada", "sugar", Side::Buy, Price::Market, 20)?; // takes the asksm.place("ada", "sugar", Side::Sell, Price::Limit(30), 20)?; // rests in the book// ... later, somebody buys it; Ada is not there:let news = m.take_news("ada"); // what filled while she was awaylet coins = m.withdraw_coins("ada", m.coins("ada"));let doc = m.to_json(); // the whole market, to a store- Price, then time. The best price first; at one price the order placed first. A trade happens at the resting order’s price, so an aggressive limit buy pays the asks it crosses and gets back what it held over them.
- Escrow.
placetakes a buy’s coins (price x qty, or for a market buy everything free, refunded after) and a sell’s goods first; a refusal (MarketError) changes nothing, a fill can never fail, and nobody spends twice.cancelreturns the escrow. - Partial fills rest the remainder of a limit order; a market order rests nothing (a market sell keeps what found no bid).
- No self-trades. An order meeting its owner’s own resting order cancels the older one instead.
- Fees come off the seller’s proceeds into
Market::fees. - Settlement goes straight into the accounts, owner present or not, and a
line for each fill waits in their
news.
An Agent trades one good through its own account and every refresh
seconds takes its orders back, works and posts again - through place, like
anyone, so a requote that crosses a player’s resting order fills it.
| Role | Works | Posts |
|---|---|---|
Producer | makes rate units an hour (up to three times normal), pays its takings over a float out as wages | asks, and one lot bid well under them |
Consumer | uses up rate units an hour, is paid enough to buy normal stock twice | bids, and one lot asked well over them |
Maker | nothing: holds its stock and its worth in coins | both sides around its price |
Taker | is kept at normal stock and purse | one lot against the book each refresh, never further than a quarter from its target - what keeps the charts moving when no player trades, and why a lowball bid waits for ever |
Each opens with its normal stock. Its price is the target moved by how far
its stock is from normal: target * (1 + elasticity * (normal - stock) / normal), between 0.35 and 2.5 of the target. A producer with a full yard
sells cheap; one that players have bought bare asks more. A consumer short of
a good pays well; sell it a shipload and its bid drops for the next one. So
players moving goods move prices, and a route stays profitable only while
nobody runs it too hard.
History
Section titled “History”Every fill updates its good’s History: a candle per period seconds of the
market’s clock (kept: the last 96), and the last price. Market::candles(good, n) gives the last n periods with quiet ones drawn flat at the close before
them. Market::tape is the last 40 trades (no names in them for a game to
leak; it chooses what to send).
Conservation
Section titled “Conservation”Nothing is made or destroyed except by a declared flow, each counted in
Market::flows: deposits and withdrawals, NPC production and consumption,
NPC income and wages. Market::check() balances every coin and unit held
(free, in escrow, in the fee pot) against them, and also refuses a crossed
book, an order out of price-time order, or an account below zero. The test
coins_and_goods_are_conserved_through_a_long_random_run runs 6,000 random
deposits, withdrawals, limit and market orders, cancels, refused cancels and
NPC refreshes (all four roles, five traders, four goods, fees on) and checks
after every step - and once more on the document read back from JSON.
cargo test -p jengine-market: escrow and refunds, price-time priority at
the resting price, partial fills, market orders bounded by the book and the
purse, no self-trades, fees, fills while the owner is away (with news), a
producer selling cheap and a consumer bidding less once stocked, an NPC
requote filling a resting order, townsfolk keeping the history moving but
never taking a lowball, a whole market round-tripping through JSON, candles,
conservation, and registration with a package host.
Limits
Section titled “Limits”- Whole coins only: a price is at least one coin, and the fee rounds down.
- One market is one place; moving goods between markets is the game’s
(Saltwind’s ships carry them, and its
Captainis not in this crate). - No order expiry, stop orders or iceberg orders; an NPC’s quote lasts until its next refresh.
- A market only moves when
advanceis called: a game that keeps a market in a room nobody has opened since a restart has NPCs that wait for it.