The Market Model (pm-market-sim)¶
Learning objectives
After reading this page you will be able to:
- Explain what the true value of a symbol is and who gets to see it
- Write or generate a
market_sim.yamland deploy it with the engine configuration - Run
pm-market-sim, check on it, and anchor a market maker to it - Tell how a compressed trading day relates to a real one
- Publish news with
pm-news, by hand or from a scenario
Prerequisites: Session Scheduling, Running the AI-Trader Swarm.
What it does¶
Prices on the exchange are whatever the participants agree on. pm-market-sim
adds what a real market has behind its prices: a true value for every
symbol, moving the way company values move — together with the market, with
the sector, on its own, and now and then in a jump. Bots that trade on value
pull prices toward it; noise and momentum traders push them away; the gap is
what students can learn to read.
The values are for the bots and the instructor only. They travel on
pm-market-sim's own sockets (loopback by default), never on the engine bus
that students' clients reach, and the API gateway serves them to ADMIN keys
only.
| Who | Uses the value for |
|---|---|
AI traders with the value strategy (presets value-investor, institutional) |
Buy below their view of the value, sell above it |
pm-mm-bot --anchor-sim W |
Move its quoted mid a share W of the way toward the value |
| ADMIN API keys | GET /api/v1/admin/sim, WebSocket /api/v1/admin/sim |
pm-market-sim --status --id OPS01 |
Where the market is furthest from the value |
The model also publishes news: headlines that move the values (see
News). Headlines are public — every API key, pm-ticker and both
GUIs show them; what a headline does to the value is not.
The model¶
For each symbol, one step of trading time Δ (in days) moves the log value by
- σ and μ are annual (252 trading days);
uis an intraday multiplier, high after the open and before the close, low at midday, averaging 1. β_mandβ_sload each symbol on a market factor and its sector's factor, so symbols of one sector move together: two of them correlate by aboutβ_m² + β_s².- Jumps arrive at
jump_rateper day with normally distributed log size. - When the exchange reopens after a close, one extra move with
overnight_fractionof a day's variance is applied.
The model steps only during continuous trading. A day's variance accrues over
the continuous phase as the engine reports it — from its start to the
scheduler's countdown to the close — so a day compressed with
pm-scheduler --speed moves as much as a real one. Without a countdown (no
scheduler, or pm-scheduler --now) a 6.5-hour day is assumed.
Every source of randomness is its own stream seeded from the model seed, so a run is reproducible and adding a symbol does not change the others' paths.
market_sim.yaml¶
version: 1
seed: 150
step_sec: 1.0 # seconds between steps
overnight_fraction: 0.2
defaults: {vol: 0.30, drift: 0.05, jump_rate: 0.05, jump_mean: 0.0,
jump_std: 0.04, beta_market: 0.5, beta_sector: 0.4}
sectors:
TECH: {vol: 0.35, beta_market: 0.6, beta_sector: 0.45}
symbols:
AAPL: {sector: TECH, vol: 0.28}
NVDA: {sector: TECH, vol: 0.45, initial: 120.0}
A symbol's parameters are the defaults, overridden by its sector's, overridden
by its own entry. A symbol of the engine configuration that the file does not
list gets the defaults (sector OTHER); a symbol the engine does not list is
an error. A symbol starts at its initial, or else at the engine's previous
close, or else at the configuration's last price.
pm-market-sim --init writes a file for the deployed symbols, each in its
sector (the usual industry sectors for the bundled tickers) with that
sector's betas and a volatility spread around the sector's. The s150-* and
s300-load examples ship one.
Keep the file next to the engine_config.yaml you deploy: pm-config-deploy
checks it against the engine's symbols and installs it beside the compiled
configuration; deploying a configuration without one removes the old one.
pm-market-sim --init --seed 7 > my-course/market_sim.yaml
pm-config-deploy my-course/engine_config.yaml
Running it¶
It follows the engine's session (asking for it at startup and after an engine
restart), publishes sim.value once per step_sec during continuous trading
and a sim.state heartbeat every second (RUNNING or PAUSED). It saves its
values to <DATA_DIR>/market_sim_state.json every ten seconds and on exit, so
a restart continues from where the model stood.
| Socket | Port | Carries |
|---|---|---|
| PUB | 5553 (EDUMATCHER_SIM_PUB_PORT) |
sim.value, sim.state, news.event, sim.command_ack.<ID> |
| PULL | 5554 (EDUMATCHER_SIM_PULL_PORT) |
sim.command from ADMIN participants |
Both bind to EDUMATCHER_SIM_BIND_HOST (default 127.0.0.1).
pm-market-sim --status --id OPS01 asks the running process where it stands
and lists the symbols whose market mid is furthest from the value:
pm-market-sim: RUNNING seq=4120 symbols=150
symbol value mid mid/value
TSLA 212.40 219.95 +3.55%
...
Anchoring a market maker¶
pm-mm-bot --anchor-sim 0.3 moves the bot's mid 30% of the remaining way to
the true value at every model step (once a second), and from the first value
on the book no longer sets its mid: its quotes converge on the value within a
few seconds. With 0 (the default) the bot ignores the model and quotes
around the book. A market maker anchored this way is a strong pull
toward the value; leave it off to let the value traders do the work.
News¶
A headline has a scope — one or more symbols, one or more sectors, or the
whole market — a kind (EARNINGS, GUIDANCE, MNA, REGULATORY,
PRODUCT, LEGAL, MANAGEMENT, MACRO), a public sentiment from −1 (bad)
to 1 (good), and a secret impact: the log move of the true value (0.10 is
about +10.5%). A sector headline moves each of its symbols by β_s times the
impact, a market headline every symbol by β_m times it.
A headline can start as a rumour. A rumour carries a credibility (0 to 1) and moves nothing; it is later confirmed — a second headline, and the value moves then — or retracted, and nothing ever moves. Prices may well move on a rumour anyway: that is traders betting on it.
| Who | Reads the news through |
|---|---|
AI traders with the news strategy (preset news-trader) |
Trade the headline within seconds, in its direction, harder the stronger the sentiment |
AI traders with the value strategy |
Shift their view of the value on a rumour, by sentiment × credibility |
| Every API key | GET /api/v1/news, and the news channel of the market-data WebSocket |
pm-ticker, the trading GUI, TapeDeck |
A news line, a News screen, a News view |
Generated news¶
Without anyone doing anything, pm-market-sim publishes news at random during
continuous trading: on average 6 symbol, 0.5 sector and 0.3 market headlines
per trading day across the whole exchange. 30% start as rumours, which resolve
on their own 0.02 to 0.15 trading days later — confirmed 60% of the time; a
rumour that will be confirmed tends to carry a higher credibility. Tune it in
market_sim.yaml; a rate of 0 turns that scope off:
news:
rate_symbol: 6.0
rate_sector: 0.5
rate_market: 0.3
rumour_share: 0.3
rumour_confirm: 0.6
rumour_delay_min: 0.02 # trading days
rumour_delay_max: 0.15
Publishing news yourself: pm-news¶
pm-news sends commands to the running pm-market-sim as an ADMIN
participant (--id) and prints what happened:
pm-news --id OPS01 inject --symbol AAPL --kind EARNINGS --sentiment 0.8 --impact 0.08
pm-news --id OPS01 inject --sector ENERGY --kind REGULATORY --sentiment -0.7 \
--impact -0.06 --rumour --credibility 0.6
N7
pm-news --id OPS01 confirm N7 # or: retract N7
pm-news list # print headlines as they are published
pm-news past --limit 20 # recent headlines, newest first
pm-news past --date 2026-10-14 # one session date
inject prints the new headline's id. --headline sets the text; without it
one is written from the kind and the sentiment. A rumour you inject waits for
your confirm or retract — it never resolves on its own. Commands from a
participant that is not ADMIN are refused.
Scenarios¶
pm-news play scenario.yaml runs a timed script. Each step fires
after_sec seconds into a session phase (or into the play when phase is
left out); a phase already under way when the play starts counts from the
start of the play. A step either injects a headline (same fields as the
inject flags) or confirms or retracts an earlier step by its name:
version: 1
steps:
- at: {phase: CONTINUOUS, after_sec: 60}
name: bid
inject: {symbol: JPM, kind: MNA, sentiment: 0.7, impact: 0.12, rumour: true,
credibility: 0.5, headline: "JPM said to be in talks over a takeover bid"}
- at: {phase: CONTINUOUS, after_sec: 240}
confirm: bid
Three classroom scenarios ship in docs/examples/news/ and fit every s150-*
example and s300-load: earnings-surprise.yaml, rumour-confirmed.yaml and
rumour-retracted.yaml. The AI Traders & News
training chapter uses them.