A market-simulation activity for a summer school. Student teams write trading
bots and run them on their own laptops; a central server runs the game, and
each bot opens an outbound WebSocket to it (so it works from behind home NAT with
no port-forwarding). The same server also hosts the marketsim client package
students pip install.
Two audiences, two sections below:
- Python 3.11 or newer (
python --version). Older versions are rejected. - That's it —
pip installpulls everything else (numpy, pandas, websockets, …).
Your organizer gives you an install password and a team token. Install the client wheel with one command (swap in the password and host you were given):
pip install "https://install:PASSWORD@marketsim.hoshinofw.net/dist/marketsim-0.1.0-py3-none-any.whl"That single command installs everything, including pandas for to_dataframe() —
there is no extra step or [ml] flag to remember.
Tip: use a fresh virtual environment so it can't clash with other projects:
python -m venv venv && source venv/bin/activate # Windows: venv\Scripts\activate
The wheel already knows the server address (it's baked in at build time). You only supply your team token and your trading logic:
# my_bot.py
from marketsim import Bot, Context
bot = Bot(team_token="YOUR-TEAM-TOKEN")
@bot.on_tick("gold") # a turn-based market: you're asked once per tick
def trade_gold(ctx: Context):
# Every value here is in euros; only ctx.price is euros-per-item.
if ctx.cash > 100 and ctx.position < 50:
return [ctx.buy(eur=10.0)] # buy 10 € of gold
return [] # return [] to hold
@bot.on_quote("crypto") # a high-frequency market: a live price stream
def trade_crypto(ctx: Context):
prices = ctx.prices # 1-D numpy array of this asset's history
if len(prices) > 50 and ctx.price < prices[-50:].min() * 1.001:
return [ctx.buy(eur=5.0)]
return []
if __name__ == "__main__":
bot.run()Run it:
python my_bot.pyIt connects, auto-reconnects if your wifi drops, and keeps your price history across reconnects. Missing a tick (slow wifi, an exception in your handler) simply counts as a hold — it never crashes your bot.
One market (oil) is turn-based-with-a-human: each round the server simulates a whole
"month" instantly and your client replays it, then you decide. When you run the bot
it prints:
Manual trading UI: http://localhost:8765
Open that URL in your browser — the month animates in and you click Buy / Sell / Hold
before the countdown ends. Prefer to automate it? Register an @bot.on_decision("oil")
handler instead and the UI is skipped. (Run bot.run(ui=False) to use a terminal prompt.)
Each Market is one asset's 1-D price series and doubles as your ML training set:
df = bot.market("gold").to_dataframe() # columns: tick, time, price (pandas)Or download the pre-simulated history over HTTP (no auth needed on /history):
import pandas as pd
df = pd.read_csv("https://marketsim.hoshinofw.net/history/gold.csv")| Attribute | Meaning |
|---|---|
ctx.price |
this asset's current price (€/item) |
ctx.prices |
full history as a 1-D numpy array |
ctx.cash |
your cash in € |
ctx.position |
current € value of your stake in this asset |
ctx.holdings |
dict: asset → current € value of that stake |
ctx.buy(eur=x) / ctx.sell(eur=x) |
build a buy/sell order for x euros |
ctx.markets |
all assets (read another asset's history if you like) |
If you run your own server locally (see below), point the client at it:
export MARKETSIM_SERVER_URL="ws://localhost:8000" # Windows: set MARKETSIM_SERVER_URL=...
pip install "http://install:PASSWORD@localhost:8000/dist/marketsim-0.1.0-py3-none-any.whl"
python my_bot.pyMARKETSIM_SERVER_URL always overrides the baked-in address.
The server is a single-process FastAPI/uvicorn app. It serves the game WebSocket,
the client wheel (behind Basic Auth), a live dashboard, and an admin panel. Run one
process only — all game state is in memory (never use --workers N / gunicorn).
Prereqs: Docker + Docker Compose.
# 1. Team tokens: copy the example and edit it (token -> team-id map).
cp deploy/tokens.example.json deploy/tokens.json
# edit deploy/tokens.json — give each team a unique secret token.
# 2. Set the wheel address for students: edit docker-compose.yml and set
# build.args.PUBLIC_WS_URL to your real host, e.g.
# PUBLIC_WS_URL: "wss://marketsim.hoshinofw.net"
# (this is baked into the wheel students download).
# 3. Passwords come from the environment (required):
export MARKETSIM_INSTALL_PASSWORD="pick-an-install-password" # students use this to pip install
export MARKETSIM_ADMIN_PASSWORD="pick-an-admin-password" # you use this for /admin
# 4. Build (runs the test suite as a build gate) and start:
docker compose up -d --build
# 5. Check it's alive:
curl http://localhost:8000/health # -> {"status":"ok", ...}The build fails if the test suite is red — a broken server never ships.
Backups and the replay log persist in a Docker volume (marketsim-data → /data), so
docker compose restart resumes the game (prices and portfolios continue) rather than
resetting. Stop everything with docker compose down (the volume, and thus the saved
game, survives).
pip install -r requirements.txt
pip install -e client # so `import marketsim` resolves from source
python scripts/build_client.py # build the wheel into dist/ (so /dist can serve it)
export MARKETSIM_INSTALL_PASSWORD=changeme-me
export MARKETSIM_ADMIN_PASSWORD=changeme-me
python main.py # serves on http://localhost:8000Run the test suite any time (plain script, prints PASS/FAIL, exits nonzero on failure):
python test.py| URL | What |
|---|---|
/ |
live public dashboard (prices, rankings) |
/admin |
admin panel — log in with MARKETSIM_ADMIN_PASSWORD; pause/resume/schedule markets |
/health |
liveness JSON |
/dist/<wheel> |
the client wheel, behind Basic Auth (install / MARKETSIM_INSTALL_PASSWORD) |
/history/<market>.csv |
that market's pre-simulated training set (no auth) |
/ws |
the game WebSocket (clients connect here with ?token=…) |
The engine snapshots the whole game to /data/backups/session-*.json every
MARKETSIM_BACKUP_INTERVAL_S seconds (default 20) and on shutdown. On startup it
decides what to load:
- Attached to a terminal (
docker compose run --rm marketsim, orpython main.pyin a shell): shows an interactive menu — pick a backup to resume, or0for a new game. - Detached (
docker compose up -d, auto-restart): silently resumes the latest backup — this is crash recovery. MARKETSIM_SESSIONoverrides both:new(fresh game),latest(force resume), or a path to a specific backup file.
| Layer | File / source | What |
|---|---|---|
| Game/market tuning | server/config.toml |
markets, price models, policies, starting cash, oversell clamp |
| Project version | properties.toml |
the single source of the version number |
| Deployment / secrets | environment variables | see below |
Key environment variables (all optional except the two passwords in Docker):
| Variable | Default | Meaning |
|---|---|---|
MARKETSIM_INSTALL_PASSWORD |
changeme |
Basic-Auth password for the wheel download |
MARKETSIM_ADMIN_PASSWORD |
changeme |
admin-panel password |
MARKETSIM_HOST / MARKETSIM_PORT |
0.0.0.0 / 8000 |
bind address |
MARKETSIM_SESSION |
(auto) | new | latest | <path> |
MARKETSIM_BACKUP_INTERVAL_S |
20 |
seconds between session backups |
MARKETSIM_BACKUP_KEEP |
50 |
how many backups to retain |
MARKETSIM_ROOT_PATH |
"" |
set to your proxy prefix only if the proxy forwards it un-stripped |
MARKETSIM_TOKENS_PATH |
tokens.json |
path to the team-token map |
TLS terminates at your proxy; the container serves plain HTTP/ws on :8000. See
nginx.example.conf for a config that reverse-proxies https://marketsim.hoshinofw.net/
to the container at its root (WebSocket upgrade headers included). Students then
use wss://marketsim.hoshinofw.net and install from https://marketsim.hoshinofw.net/dist/….