Skip to content

jengine-market

An order-book market - limit and market orders, price-time matching, escrow, NPC producers and consumers, price history - the economy of Saltwind.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-market
Version0.1.0
Entry pointjengine_market::MarketPackage

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.

TypeWhat it is
Marketone 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
Accountsomeone’s warehouse here: free coins, free goods, and news - their fills since they last looked
Ordera resting order: owner, good, side, price, units left, units placed, when
Bookbids best (highest) first, asks best (lowest) first, oldest first at one price
Fillone trade: price, units, buyer, seller, fee, which side was resting
Agentan NPC trader (Role::Producer, Consumer, Maker, Taker)
History / CandleOHLC candles per period of the market’s clock, the last price
Flowsevery 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).

use jengine_market::{Agent, Market, Price, Side};
let mut m = Market::new(&["sugar", "rum"], 100, 30.0); // 1% fee on sales, 30 s candles
m.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 asks
m.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 away
let 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. place takes 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. cancel returns 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.

RoleWorksPosts
Producermakes rate units an hour (up to three times normal), pays its takings over a float out as wagesasks, and one lot bid well under them
Consumeruses up rate units an hour, is paid enough to buy normal stock twicebids, and one lot asked well over them
Makernothing: holds its stock and its worth in coinsboth sides around its price
Takeris kept at normal stock and purseone 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.

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).

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.

  • 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 Captain is 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 advance is called: a game that keeps a market in a room nobody has opened since a restart has NPCs that wait for it.