VOL-TRADE
Overview
VOL-TRADE is a simulated exchange with real price-time priority order matching. It runs a universe of stocks, indexes, futures and options with live-updating fair values, automated market-making bots, and a full depth-of-book. Multiple human participants can connect simultaneously and compete on a live leaderboard.
You can trade manually through the UI or write Python scripts in the built-in dev environment to automate strategies — the same WebSocket API the bots use is exposed to you.
Your Account
One account works everywhere on VOL-TRADE. Your saved code and your identity follow you into every exchange you join, on any device you sign in from.
| Rule | Detail |
|---|---|
| Display name | Up to 28 characters and unique across the platform — two traders with the same name would make a leaderboard ambiguous. You can change it later by clicking your name in the top bar; the new name appears everywhere except the final standings of competitions that have already closed, which stay frozen as they were. |
| Hosting | You can host one exchange at a time. Hosting runs a whole bot fleet and its market loops, so the limit keeps one account from using up the server. Deleting your exchange frees the slot straight away. |
| Joining | No limit. Being a participant in someone else's exchange does not count against your hosting slot. |
| Saved code | Two independent slots — the dev panel on the trading screen and the full Python terminal — each saved with its own SAVE button. Nothing you save runs while you are offline; your code only executes in your own browser, while you are watching it. |
Sending Feedback
There is a FEEDBACK button in the top bar of your dashboard, which opens a short form. Use it for anything: a bug, an idea, or something in the interface that did not make sense. It goes directly to the person who built VOL-TRADE, and because you are signed in, they can reply to you by email.
Pick a category (General, Bug report, Feature idea, or Question) and write up to 2,000 characters. A counter shows how much room you have left. Press SEND, or Ctrl+Enter; Esc closes the form without sending.
Interface Layout
left column
centre — resizable
right column
The three panels are separated by drag handles (the thin 4px borders). Drag left/right handles to resize columns; drag the horizontal handle to resize the Python terminal. The terminal can be hidden entirely with the ▼ HIDE button.
The right panel contains nine tabs: Participants, Unfilled, History, Positions, Bio, Book, News, Calendar, and Settings. Bio shows detail on the currently selected instrument — description, sector, annualized vol, and related ETFs/futures/options. Book mirrors the depth-of-book for the currently selected instrument and stays visible while you use other parts of the UI. News and Calendar show fired and upcoming headlines respectively; Settings has session info and a disconnect/back-to-dashboard link.
Top Bar — Equity & P&L Widgets
The right side of the top bar shows four live financial widgets for your own account, updated on every market snapshot.
| Widget | Formula | Description |
|---|---|---|
| Total Equity | Cash + mark value of all open positions | Your total account value. Moves with every price tick. This is the primary measure of your performance. |
| Total P&L | Realised P&L + unrealised P&L | Net profit/loss since session start. Shown in green when positive, red when negative. Includes both closed positions (realised) and open positions marked to last trade price. |
| Open Positions | (Equity − Cash) − Unrealised P&L | The net dollar amount committed to your current open positions — i.e., cost basis of all holdings. Zero when you have no open positions. |
| Cash | Uncommitted cash balance | The portion of your account not tied up in open positions — what's available to spend on new orders. |
Market States
| State | Meaning |
|---|---|
| PRE-OPEN | Default on startup. Participants can connect but orders are rejected. Use this to let everyone join before trading begins. |
| OPEN | Orders are accepted and matched. Bots are active. Price history accumulates. |
| HALTED | Trading suspended. Existing orders remain on the book but no new matching occurs. Orders submitted during a halt are rejected. |
| CLOSED | Session over. All order submission is rejected. |
Only the host can change market state, from that exchange's scoped Admin Console.
Chart Navigation
The price chart shows the last-trade price history for the selected instrument since the session began. Click any instrument in the left column to switch. The chart only records actual traded prices — it does not fall back to a fair value line when there are no trades. An instrument with no fills yet will show a flat or empty chart until the first trade prints.
Line vs Candlesticks
The chart header has a LINE / CANDLES toggle. LINE (the default) draws a continuous last-price line with an area fill. CANDLES aggregates the price history into OHLC candlesticks — green when the candle closed at or above its open, red when it closed below — with wicks marking each candle's high and low.
In candle mode a timeframe selector appears with 30s, 1m (default), and 3m options, controlling how much time each candle covers. A live legend in the top-left corner of the chart always shows the latest bar's O / H / L / C (or price, in line mode) and time; hovering any bar updates the legend to that bar. Switching type or timeframe re-fits the view to the data.
Mouse & Trackpad Controls
The chart is powered by TradingView Lightweight Charts™, so it uses the familiar interaction model of a professional charting terminal:
| Action | Effect |
|---|---|
| Hover | Magnet crosshair that snaps to the nearest bar, with price and time pills on the axes and the top-left legend updating to the hovered bar. |
| Drag chart body left/right | Pan through history (scroll the time axis). |
| Drag the price axis (right edge) | Scale the price (Y) axis — drag to stretch or compress the visible price range. |
| Drag the time axis (bottom edge) | Scale the time (X) axis — drag to stretch or compress how much history is shown. |
| Scroll wheel / pinch (trackpad) | Zoom in and out around the cursor. |
| Double-click | Reset the view to fit all available data. |
Chart Controls Reference
The price scale on the right snaps to round-number increments and formats prices to each instrument's tick size. The dashed line and coloured tag on the right axis mark the current price; the top-left legend shows the latest (or hovered) bar's values.
The chart header always shows the last trade price, % change from the first data point, and the current bid / ask. On the admin terminal, fair value is also shown.
Charting by TradingView Lightweight Charts™ (Apache 2.0).
Time Axis
A time axis runs along the bottom edge of the chart. Labels show wall-clock time in HH:MM (for intervals ≥ 1 minute) or HH:MM:SS (for shorter intervals). The axis adapts its interval automatically based on the number of visible data points — zooming in shows finer resolution, zooming out coarser.
Faint vertical grid lines align with each time label to make it easy to correlate price movements with specific times.
Order Book & Tape
Central Depth Strip (below the chart)
Shows the top 10 price levels on each side. Bids (green) are displayed with the best bid at the top, nearest the spread. Asks (red) are displayed with the worst ask at the top and best ask nearest the spread. The spread row shows the current bid-ask spread.
The horizontal bars behind each row are proportional to quantity — a wide bar means a large resting order at that level relative to the deepest visible level.
The depth strip can be toggled on or off using the ▾ DEPTH button in the chart header. When hidden, the chart expands to fill the space — this also hides the tape, since they share the same collapsible panel. Press the button again (which now reads ▴ DEPTH) to restore both.
Tape (last 50 trades)
A real-time print of every fill across all participants. Green prices indicate a buyer-aggressed trade (aggressor was buying); red prices indicate a seller-aggressed trade. Each row shows the aggressor's participant ID in a small badge on the right — the participant who sent the crossing order.
To watch a participant's flow, click their row in the Participants tab to open the inspector popover, then use “Highlight this trader in the tape”. Their tape rows are highlighted in amber and the participant row gains a green left border. Toggle it off from the same button. This lets you track specific participants — for example, bots that appear to be trading ahead of announcements.
Book Tab (right panel)
The Book tab in the right panel shows the same depth-of-book in a persistent, full-height view. This is useful when you want to watch the order book continuously without the depth strip taking space below the chart, or when you need the book and the participants/positions tabs open side-by-side in a workflow. The Book tab updates in real time alongside the central strip.
Placing Orders
Orders are placed from the order ticket below the depth strip. Rather than a single SEND button, the ticket has dedicated BUY and SELL buttons — each shows the exact price it will execute at, so you always see what you're about to do before you click.
| Control | Description |
|---|---|
| LIMIT / MARKET | Segmented toggle at the top. In MARKET mode the price field is hidden and the buttons read BUY MKT / SELL MKT — they fill against the best available price immediately. |
| Qty | Number of lots (minimum 1). Use the − / + steppers or type a value. |
| Price | LIMIT only. Leave it blank to hit the touch — BUY fills at the best ask, SELL at the best bid (the fastest way to take liquidity). Type a value, or use the bid / mid / ask chips, to place a specific limit price. |
| est. notional | Live estimate of the order's value (quantity × price) shown just above the buttons. |
| BUY / SELL | The two action buttons. Each shows its live execution price and is disabled when the market is not OPEN or there is no quote to hit. |
After clicking BUY or SELL, the acknowledgement appears in the Python terminal output (bottom panel) as BUY … → NEW or SELL … → FILLED etc. Any fills also print as FILL … lines in green.
Order Types & Matching
The exchange uses price-time priority: among all resting orders, the best price is matched first; ties are broken by order arrival time (FIFO). There is no hidden order type.
| Type | Behaviour |
|---|---|
| LIMIT BUY | Matches against any resting ask ≤ your price. Unfilled remainder rests on the bid side. |
| LIMIT SELL | Matches against any resting bid ≥ your price. Unfilled remainder rests on the ask side. |
| MARKET BUY | Sweeps the ask side until filled or book exhausted. Any unfilled quantity is dropped (no resting). |
| MARKET SELL | Sweeps the bid side. Unfilled dropped. |
A LIMIT order that is fully matched on arrival is acknowledged with status FILLED. A partial match gives PARTIAL and the remainder rests. An unmatched order is NEW and visible in the Unfilled tab.
MARKET orders are never resting — they sweep the book and any unfilled quantity is silently dropped. A MARKET order that only partially fills will not appear in the Unfilled tab and cannot be cancelled (there is nothing left to cancel). Check the Order History tab to see the fills.
Participants Tab
Shows all connected participants sorted by PnL — humans first, then bots. Your own row is highlighted in amber. Each row shows:
| Field | Meaning |
|---|---|
| ● / ○ / BOT | For human traders: green dot = currently connected WebSocket, empty circle = registered but disconnected. Bots always show a BOT tag instead, regardless of connection state. |
| PnL | Realised + unrealised profit/loss since session start, marked to last trade price. |
| fills · vol · eq | Total fill count, total volume traded, and current total equity (cash + mark value of open positions). |
Click any participant row to open an inspector popover showing their total PnL, equity, realised/unrealised split, fill count, volume, open positions, and most recent trades. Positions are shown for bots and for yourself; other human traders' positions stay private (you still see their public PnL, volume, and tape activity). The popover includes a “Highlight this trader in the tape” button — turn it on to amber-highlight their prints in the tape and give their participant row a green left border, so you can follow a specific bot or participant you suspect is trading on information. Close the popover by clicking the ✕ or the backdrop.
Unfilled Orders Tab
Shows all your currently resting LIMIT orders (status NEW or PARTIAL). MARKET orders never appear here — any unfilled quantity from a MARKET order is dropped immediately rather than resting on the book. For each resting order:
- Large number = lots remaining (unfilled quantity). Updates in real time as fills arrive.
- Progress bar shows fill percentage.
- Status label shows OPEN (no fills yet) or PARTIAL (partly filled) with fill count.
- ✕ CANCEL ORDER — sends a cancel request. The button disables while the server processes it. If the order was already fully filled the cancel will be rejected.
The badge next to the tab name shows the count of open orders.
Bulk Cancel Actions
An actions bar appears at the top of the Unfilled tab whenever you have open orders. It provides two bulk-cancel shortcuts:
| Control | Effect |
|---|---|
| ✕ CANCEL ALL ORDERS | Sends cancel requests for every open order across all instruments simultaneously. Useful for quickly exiting all resting quotes at end of session or on a risk event. |
| Instrument dropdown + ✕ CANCEL | Select a specific instrument from the dropdown (populated with only the symbols where you have open orders) and press ✕ CANCEL to cancel all resting orders on that instrument only. Use this to pull quotes on one leg without disturbing other positions. |
Order History Tab
A full log of all orders you have submitted this session, most recent first. Each entry shows side, type, price, quantity, status, and individual fill records.
| Status | Meaning |
|---|---|
| NEW | Resting on the book, no fills yet. |
| PARTIAL | Some quantity filled, remainder still resting. |
| FILLED | Fully matched. |
| CANCELLED | Cancelled by you before full fill. |
A ✕ RESET HISTORY button clears this client-side list (it doesn't affect your actual orders or the server's record — just what's displayed here).
Positions Tab
Your current net exposure across all instruments. Only instruments with a non-zero position or realised PnL appear.
| Field | Meaning |
|---|---|
| +N / -N | Net long (+) or short (-) position in lots. |
| avg | Volume-weighted average cost of the position. |
| mark | Last trade price used to mark the position (falls back to fair value if no trades yet). |
| unrlzd | Unrealised PnL = (mark − avg cost) × quantity. |
| rlzd | Realised PnL from closed portions of the position. |
| total | unrlzd + rlzd. |
Click any position row to switch the chart to that instrument.
Flatten Position
Each position row has a flatten button beneath it, labeled with your exact position (e.g. ⬡ FLATTEN LONG +N or ⬡ FLATTEN SHORT N). Pressing it submits an aggressive MARKET order in the opposite direction equal to your entire net position, reducing it to zero in a single action.
| Position | Action taken |
|---|---|
| +N long | Submits a MARKET SELL for N lots — sweeps the bid side immediately. |
| −N short | Submits a MARKET BUY for N lots — sweeps the ask side immediately. |
Book Tab
A dedicated right-panel tab that shows the full depth-of-book for the currently selected instrument in a persistent, full-height view — identical data to the central depth strip but always visible regardless of whether the depth strip is toggled on or off.
Switch to this tab when you want the order book always in view while you navigate other tabs, or when you prefer to hide the central depth strip (using the ▾ DEPTH toggle) to give the chart more vertical space.
News Tab
A feed of every headline that has already fired this session, most recent first, color-coded by status: UNCONFIRMED rumour, CONFIRMED/VALIDATED, or DENIED. The tab badge pulses when a new item arrives while you're on another tab.
Click any headline to open a detail panel with the full story, the source and what its credibility tier implies for how the move plays out (see Trading Around News), the complete list of affected instruments, and — for a still-unconfirmed rumour — a live countdown to when it resolves to CONFIRMED or DENIED.
Calendar Tab
Events an admin has pre-scheduled but that haven't fired yet, each showing a vague preview headline, its source, and a live countdown. The badge shows how many events are pending.
Click any upcoming event to open a detail panel with the source's credibility tier, the exact fire time, and a live-updating countdown. The real headline, its market impact, and how it resolves stay hidden until the event fires — that's deliberate, not a bug: publishing them early would hand every trader the same insider-style edge the informed/insider bots exist to exploit unevenly. When the countdown hits zero, switch to the News tab to see what actually happened.
Python Terminal (Dev Environment)
An in-browser Python 3 runtime (Pyodide) connected to the exchange via a pre-built exchange object. You can write and run arbitrary Python to automate orders, scan for opportunities, or run analysis.
Inline Terminal (bottom panel)
A compact split editor/output pane built into the main trading terminal. Useful for quick one-shot scripts and inspections.
| Control | Action |
|---|---|
| ▶ RUN | Execute the code currently in the editor. Disabled until Pyodide is loaded. |
| ■ STOP | Interrupt a running script. Works in any loop — a background line tracer checks the stop flag automatically every 500 Python lines, so no exchange call is required. The script halts within milliseconds. A [Stopped by user] message confirms the halt. Does not affect orders already submitted. |
| SAVE | Saves the editor's contents to your account, so it's still there next time you open the terminal — on any device you log in from. Saving is manual: nothing is stored until you press it, and the indicator beside the button reads unsaved when you have changes that aren't stored yet. This panel and the dedicated terminal below save separately, so each keeps its own script. |
| CLEAR | Wipes the output pane. Does not stop running code — use ■ STOP for that. |
| EXAMPLES | Cycles through built-in starter snippets (strategy loop, snapshot, limit buy, market-maker, options scan). |
| ▼ HIDE / ▲ SHOW | Collapse or restore the entire terminal row to reclaim vertical space. |
| ⎋ OPEN TERMINAL | Opens the dedicated Monaco terminal (see below) in a new tab, pre-populated with your participant ID. |
Dedicated Monaco Terminal (/pyterm)
A full-screen Python environment built on Monaco Editor (the same engine as VS Code). Open it via the ⎋ OPEN TERMINAL link in the inline terminal header, or navigate directly to /pyterm?id=YOUR_ID. It connects to the exchange under the same participant ID so your orders, positions, and PnL are shared with the main terminal.
| Feature | Detail |
|---|---|
| Syntax highlighting | Full Python syntax colouring with the VOL-TRADE dark theme. |
| Autocomplete | Type exchange. and press Ctrl+Space to see all available methods with inline documentation. |
| Ctrl+Enter | Run the script (same as clicking ▶ RUN). |
| Ctrl+. | Stop the running script (same as clicking ■ STOP). |
| Ctrl+S / SAVE | Save this terminal's script to your account so it reloads next time. Kept separately from the inline panel's saved script. |
| Resizable panes | Drag the vertical handle between the editor and output to change the split. |
| EXAMPLES | Same five starter snippets as the inline terminal. |
await is supported in both terminals. Order methods are async — use await exchange.buy(…) directly in your script without wrapping in an async def.
Output from print() appears in the output pane in real time — even inside while True loops. Red lines are errors (with full tracebacks). Green lines are fill notifications. Blue lines are system messages.
Available Python Packages
The terminal runs Pyodide 0.27.7 — a full CPython 3.12 compiled to WebAssembly. You have access to the entire Python standard library plus several pre-installed scientific packages. Additional packages can be installed at runtime using micropip.
Standard Library — always available
All built-in modules ship with the runtime. Commonly useful ones for trading:
| Module | Use |
|---|---|
asyncio | Async loops and sleep — await asyncio.sleep(t) works natively and honours the STOP button. Use it to pace strategy loops and yield time to the event loop. |
math | Floor/ceil/log/exp/sqrt, trig, math.inf, math.isnan. |
statistics | Mean, median, stdev, variance, linear_regression. |
random | Random numbers, shuffling, sampling — useful for simulation. |
collections | deque, defaultdict, Counter, namedtuple. |
itertools | Chain, combinations, groupby, islice, accumulate. |
functools | reduce, lru_cache, partial. |
datetime | Date/time arithmetic and formatting. |
json | Parse / dump JSON (snapshots are already parsed for you by the exchange object). |
re | Regular expressions — useful for filtering instrument symbols. |
heapq | Priority queues. |
bisect | Sorted-list insertion and search. |
decimal | Arbitrary-precision arithmetic for price calculations. |
typing | Type hints. |
dataclasses | Dataclass decorator. |
enum | Enum types. |
copy | copy.deepcopy for duplicating snapshot dicts. |
pprint | Pretty-print nested dicts — handy for inspecting snapshots. |
sys, os | Basic system access. sys.stdout/stderr are redirected to the output pane; os.environ is available but the filesystem is in-memory only. |
Auto-loaded Packages — import directly, no install call needed
Before running your script, the terminal scans it for import statements and transparently loads any matching Pyodide package (via loadPackagesFromImports) — so common scientific packages just work the first time you import them, with a brief one-time download on first use rather than being bundled into every page load:
| Package | Use |
|---|---|
numpy | Fast numerical arrays, linear algebra, FFT, statistics. Essential for vectorised price analysis. |
micropip | The in-browser package installer — see below, for packages beyond Pyodide's own bundled set. |
import numpy as np
prices = [exchange.price(s)["last"] for s in exchange.instruments() if exchange.price(s)["last"]]
arr = np.array(prices)
print(f"mean={arr.mean():.2f} std={arr.std():.2f} min={arr.min():.2f} max={arr.max():.2f}")
Installing Additional Packages with micropip
Pure-Python packages on PyPI can be installed at the top of your script with await micropip.install(). Run this once — Pyodide caches the install for the duration of the browser session.
import micropip
await micropip.install("pandas") # install first (only needed once per session)
import pandas as pd
# build a DataFrame from a snapshot
snap = exchange.snapshot()
rows = []
for sym, info in snap["instruments"].items():
rows.append({"sym": sym, "kind": info["kind"], "fair": info.get("fair_value"), "last": info.get("last_price")})
df = pd.DataFrame(rows).set_index("sym")
print(df.head(10).to_string())
Other popular packages you can install the same way:
| Package | Notes |
|---|---|
pandas | DataFrames, time-series, groupby, rolling windows. |
scipy | Statistics, optimisation, interpolation, Black-Scholes helpers. |
sympy | Symbolic math — useful for options Greeks derivations. |
networkx | Graph algorithms — correlations as a network. |
statsmodels | OLS, ARIMA, cointegration tests. |
await micropip.install() every time you press RUN is harmless but slower — Pyodide will skip the download if the package is already cached. Place the install call at the top of your script guarded by a flag if you run the script in a loop.
Not Available
- Raw TCP / WebSocket from Python —
socket,websockets,aiohttp,requests,httpx. All network I/O must go through theexchangeobject, which uses the page's existing WebSocket connection. - Persistent file I/O —
open()works but writes to an in-memory virtual filesystem that is wiped when you close the tab. Nothing is saved to disk. - Threads —
threading.Threadis not supported in Pyodide. Useasyncioinstead. - Packages with compiled C extensions — any package that requires a native binary (e.g.,
scikit-learn,matplotlib) may fail to install unless Pyodide ships a pre-compiled wheel for it. Check the Pyodide package list first. - Multiprocessing —
multiprocessingis not available.
Exchange API Reference
The exchange object is pre-created and available immediately after PYTHON READY appears.
Read-only Methods
| Method | Returns |
|---|---|
exchange.snapshot() | Full market snapshot dict: state, instruments, participants, my_id. |
exchange.state() | "OPEN", "PRE_OPEN", "HALTED", or "CLOSED". |
exchange.instruments() | List of symbol strings. |
exchange.price(sym) | Dict with keys last, fair, bid, ask. Any may be None if no trades/quotes yet. |
exchange.participants() | List of participant dicts with participant_id, display_name, total_pnl, equity, is_bot, etc. |
exchange.me() | Your own participant dict, or None. |
exchange.positions() | Dict mapping symbol → {quantity, avg_cost, realized_pnl} for your open positions. |
exchange.my_id | Property (no parens) — your own participant ID string. |
Order Methods (async)
| Method | Description |
|---|---|
await exchange.buy(sym, qty, price=None, order_type=None) | Place a buy order. Defaults to LIMIT if price is given, else MARKET — pass order_type explicitly to override that default. Returns an ack dict. |
await exchange.sell(sym, qty, price=None, order_type=None) | Place a sell order. Same signature. |
await exchange.cancel(sym, order_id) | Cancel an open order. Returns {"accepted": True/False, …}. |
exchange.log(message) | Print a highlighted info-colored message to the terminal output, distinct from plain print(). |
Ack Dict Structure
{
"accepted": True, # False if rejected
"order_id": 12345, # use this to cancel later
"order_status": "NEW", # or PARTIAL / FILLED / REJECTED
"trades": [...], # list of fills if immediately matched
"reason": None # error string if rejected
}
Exchange API v2 — Extended Methods
These methods go beyond basic snapshot access. They give you price history, live news, options Greeks, order book depth, and the time-remaining widget — everything you need for quantitative strategies.
/pyterm page — both share the same exchange implementation.
News & Events
| Method | Returns |
|---|---|
exchange.news() | List of current news items, newest first. Each dict includes id, headline, body, source_label, status ("UNCONFIRMED" / "CONFIRMED" / "DENIED"), affected (list of symbols), created_at, resolved. |
exchange.since(event_type=None, after_seq=0) | List of exchange events since after_seq. Event types: "news", "scenario", "timer" — omit event_type (or pass None) for all types. Each event has seq, type, payload, ts (Unix timestamp — note the field is ts, not timestamp). Use this for efficient polling — save the last seq and pass it next call. |
Flow & Tape Analysis
| Method | Returns |
|---|---|
exchange.tape(n=100) | Most recent n fills across all instruments, newest first. Each entry: {"instrument", "price", "quantity", "aggressor", "buyer_id", "seller_id", "timestamp", "trade_id"} — note the field is instrument/quantity/aggressor, not sym/qty/side. |
exchange.flow_by_participant(n=500) | Dict mapping participant ID → {"buy_qty", "sell_qty", "trade_count", "instruments"} aggregated over their last n tape entries — not a list of individual trades. |
exchange.suspect_flow(n=500, min_trades=3) | List of participants with concentrated directional flow, sorted most-one-sided first. Each entry has {"participant_id", "imbalance", "dominant_side", "trade_count", "top_instruments"} — imbalance is 0 (balanced) to 1 (fully one-sided); min_trades filters out low-volume noise. |
Price History & Market Data
| Method | Returns |
|---|---|
await exchange.history(sym, n=100) | Async — needs await. Last n price points for sym as a list of {"time": float, "price": float} dicts, oldest first. time is a Unix timestamp. Returns an empty list if no trade history yet. |
exchange.book(sym) | Full order book for sym as {"bids": [[price, qty], …], "asks": [[price, qty], …]}. Bids in descending order, asks in ascending order — not capped to 10 levels (use depth() below if you want that). |
exchange.depth(sym, levels=10) | Same shape as book(), sliced to the top levels per side. |
exchange.fair_value(sym) | Server-computed fair value (float) for sym. This is the "theoretical" mid that market-maker bots quote around. Returns None if not available. |
Options Analytics
| Method | Returns |
|---|---|
exchange.greeks(sym) | Dict with {"delta", "gamma", "theta", "vega", "iv", "theo", "S", "K", "T_days"} if sym is an option (S/K are the underlying price and strike, T_days is days to expiry); empty dict {} otherwise. Greeks computed via Black-Scholes in real time by solving for IV from the option's live fair value. See the Greeks reference for field definitions. |
Session Timer
| Method | Returns |
|---|---|
exchange.timer() | Dict {"end": float, "remaining": float, "auto_close": bool}, or None if no timer is set — check for None before indexing into it. remaining is seconds until auto-close. |
# Poll for new news events efficiently
last_seq = 0
while True:
events = exchange.since("news", after_seq=last_seq)
for ev in events:
last_seq = max(last_seq, ev["seq"])
n = ev["payload"]
exchange.log(f"NEWS [{n['status']}]: {n['headline']}")
await asyncio.sleep(2)
Code Examples
Inspect the Market
state = exchange.snapshot()["state"]
print("market state:", state)
print("instruments:", len(exchange.instruments()))
print("my positions:", exchange.positions())
Place a Limit Order
p = exchange.price("ACME")
print("ACME bid/ask:", p["bid"], "/", p["ask"])
# buy 10 lots just below the best ask
if p["ask"]:
ack = await exchange.buy("ACME", 10, price=round(p["ask"] - 0.05, 2))
print("order_id:", ack["order_id"], "status:", ack["order_status"])
Market Order (Immediate Fill)
# sell 5 lots at market — no price needed
ack = await exchange.sell("ORBT", 5)
print(ack)
Cancel an Open Order
ack = await exchange.buy("DRFT", 100, price=210.00)
order_id = ack["order_id"]
print("resting order_id:", order_id)
# ... some time later ...
result = await exchange.cancel("DRFT", order_id)
print("cancel accepted:", result["accepted"])
Simple Market-Maker
# Quote ±5 cents around fair value on DRFT
sym = "DRFT"
p = exchange.price(sym)
if p["fair"]:
fair = p["fair"]
bid_ack = await exchange.buy(sym, 5, price=round(fair - 0.05, 2))
ask_ack = await exchange.sell(sym, 5, price=round(fair + 0.05, 2))
print("bid:", bid_ack["order_id"], " ask:", ask_ack["order_id"])
Scan for Mispriced Options
snap = exchange.snapshot()
options = {s: i for s, i in snap["instruments"].items() if i["kind"] == "OPTION"}
print(f"found {len(options)} options")
for sym, info in list(options.items())[:8]:
last = info.get("last") or info.get("last_price")
fair = info.get("fair") or info.get("fair_value")
if last and fair:
edge_pct = (last - fair) / fair * 100
print(f" {sym:>22s} last={last:>7.2f} fair={fair:>7.2f} edge={edge_pct:+.1f}%")
News-Driven Event Strategy
# React to incoming news: go long affected stocks on UNCONFIRMED news
# (betting on confirmation), then close on resolution
seen_news = set()
while True:
for item in exchange.news():
nid = item["id"]
if nid in seen_news:
continue
if item["status"] == "UNCONFIRMED" and item["affected"]:
sym = item["affected"][0]
p = exchange.price(sym)
if p["ask"]:
ack = await exchange.buy(sym, 5, price=p["ask"])
exchange.log(f"Positioned on news {nid}: {ack['order_status']}")
elif item["status"] in ("CONFIRMED", "DENIED"):
seen_news.add(nid) # stop tracking resolved items
await asyncio.sleep(3)
Greeks Scanner
# Find options with high gamma (largest convexity per dollar)
syms = [s for s in exchange.instruments() if "-C" in s or "-P" in s]
rows = []
for sym in syms:
g = exchange.greeks(sym)
if g:
rows.append((g["gamma"], sym, g["delta"], g["iv"]))
rows.sort(reverse=True)
for gamma, sym, delta, iv in rows[:10]:
print(f"{sym:>20s} Δ={delta:+.3f} Γ={gamma:.5f} IV={iv:.1%}")
Price History Momentum
# 5-tick vs 20-tick momentum signal on DRFT
hist = await exchange.history("DRFT", n=25)
if len(hist) >= 25:
prices = [h["price"] for h in hist]
fast_ret = (prices[-1] - prices[-5]) / prices[-5] # 5-tick return
slow_ret = (prices[-1] - prices[-20]) / prices[-20] # 20-tick return
exchange.log(f"fast={fast_ret:+.3%} slow={slow_ret:+.3%}")
p = exchange.price("DRFT")
if fast_ret > 0.001 and slow_ret > 0.001 and p["ask"]:
ack = await exchange.buy("DRFT", 8, price=p["ask"])
exchange.log(f"Momentum long: {ack['order_status']}")
elif fast_ret < -0.001 and slow_ret < -0.001 and p["bid"]:
ack = await exchange.sell("DRFT", 8, price=p["bid"])
exchange.log(f"Momentum short: {ack['order_status']}")
Order Book Imbalance Signal
# Use bid/ask queue imbalance as a short-term direction signal
sym = "ACME"
book = exchange.book(sym)
if book["bids"] and book["asks"]:
bid_qty = sum(q for _, q in book["bids"][:3]) # top 3 bid levels
ask_qty = sum(q for _, q in book["asks"][:3]) # top 3 ask levels
total = bid_qty + ask_qty
imbalance = (bid_qty - ask_qty) / total if total else 0
exchange.log(f"Book imbalance: {imbalance:+.2f} (bids={bid_qty} asks={ask_qty})")
p = exchange.price(sym)
if imbalance > 0.40 and p["ask"]: # strong buy pressure
ack = await exchange.buy(sym, 5, price=p["ask"])
exchange.log(f"Imbalance buy: {ack['order_status']}")
elif imbalance < -0.40 and p["bid"]: # strong sell pressure
ack = await exchange.sell(sym, 5, price=p["bid"])
exchange.log(f"Imbalance sell: {ack['order_status']}")
ETF vs Components Arb
# TECH-ETF tracks DRFT, QNTM, FNTM, NXUS at 25% each
components = ["DRFT", "QNTM", "FNTM", "NXUS"]
weights = [0.25, 0.25, 0.25, 0.25]
etf_sym = "TECH-ETF"
nav = sum(w * (exchange.price(s)["last"] or exchange.price(s)["fair"] or 0)
for s, w in zip(components, weights))
etf_p = exchange.price(etf_sym)
etf_last = etf_p["last"] or etf_p["fair"]
if nav and etf_last:
premium = (etf_last - nav) / nav
exchange.log(f"ETF premium to NAV: {premium:+.3%}")
# If ETF trades too rich: sell ETF, buy components
if premium > 0.003 and etf_p["bid"]:
ack = await exchange.sell(etf_sym, 10, price=etf_p["bid"])
exchange.log(f"Sold ETF: {ack['order_status']}")
Tape Flow Analysis
# Who has been trading and in what direction?
flow = exchange.flow_by_participant(n=300)
for pid, f in flow.items():
if f["trade_count"] == 0:
continue
total = f["buy_qty"] + f["sell_qty"]
exchange.log(f"{pid:>12s} buy_qty={f['buy_qty']} sell_qty={f['sell_qty']} "
f"trades={f['trade_count']} instruments={list(f['instruments'].keys())}")
Suspect Flow Scanner
# Find participants whose trades consistently precede price moves
suspects = exchange.suspect_flow(n=500, min_trades=3) # already sorted, most one-sided first
for s in suspects[:5]:
exchange.log(f"{s['participant_id']:>12s} imbalance={s['imbalance']:.0%} "
f"side={s['dominant_side']} trades={s['trade_count']} top={s['top_instruments']}")
Session Timer Wind-Down
# Flatten all positions when 30 seconds remain
while True:
t = exchange.timer()
remaining = t["remaining"] if t else None
if remaining is not None and remaining < 30:
exchange.log(f"⚡ {remaining:.0f}s left — flattening all positions")
for sym, pos in exchange.positions().items():
qty = pos["quantity"]
if qty > 0:
await exchange.sell(sym, qty)
elif qty < 0:
await exchange.buy(sym, -qty)
break
await asyncio.sleep(5)
Options & Greeks
An option is the right (but not the obligation) to buy (call) or sell (put) an underlying at a fixed strike price before expiry. Options let you express leveraged directional views, hedge existing positions, and trade volatility itself.
Delta (Δ)
Delta is the rate of change of option price with respect to the underlying. A call with Δ = 0.6 gains approximately $0.60 when the underlying rises $1.
| Situation | Delta range | Intuition |
|---|---|---|
| Deep in-the-money call | → 1.0 | Behaves almost like owning the stock |
| At-the-money call | ≈ 0.5 | 50% chance of expiring ITM |
| Deep out-of-the-money call | → 0 | Very unlikely to expire ITM |
| Any put | -1.0 to 0 | Puts have negative delta — profit when underlying falls |
The delta of a portfolio tells you your effective exposure to the underlying. A delta of +500 means you're as exposed as owning 500 units of the stock. Delta hedging means keeping your portfolio delta near zero by trading the underlying.
Gamma (Γ)
Gamma is the rate of change of delta — how fast delta shifts as the underlying moves. High gamma means delta changes quickly; your option becomes more (or less) sensitive very rapidly.
ATM options near expiry have the highest gamma. A gamma of 0.04 means delta increases by 0.04 for every $1 move in the underlying. This convexity is valuable — it means your upside accelerates faster than your downside.
Theta (Θ)
Theta is time decay — the amount an option loses in value per day as expiry approaches, all else equal. Options are wasting assets: a call worth $5 today might be worth $4.80 tomorrow just from time passing.
Theta is negative for option buyers (you lose value daily) and positive for option sellers (you collect it). Near expiry, theta accelerates dramatically — ATM options decay fastest in the final days.
Vega (ν)
Vega measures sensitivity to implied volatility — how much the option price changes per 1% move in IV. A vega of 0.30 means the option gains $0.30 if IV rises 1 percentage point.
Long options are long vega — you profit when volatility rises (e.g., before earnings). Short options are short vega — you profit from volatility declining (IV crush). News events drive sharp vega moves: IV spikes when news is rumoured, then collapses after resolution (the "IV crush").
The exchange.greeks(sym) Method
Returns a dict for any option symbol. Returns {} for non-options.
| Key | Type | Definition |
|---|---|---|
delta | float | dOption/dUnderlying. Range: [−1, 0] puts, [0, 1] calls. |
gamma | float | dDelta/dUnderlying. Always positive. Highest at ATM. |
theta | float | Option price change per calendar day. Negative for long options. |
vega | float | Price change per 1% increase in implied vol. |
iv | float | Implied volatility as a decimal (e.g., 0.30 = 30%). |
theo | float | Black-Scholes theoretical fair value. |
S | float | Underlying's current price, as used in the calculation. |
K | float | Strike price. |
T_days | float | Days remaining to expiry. |
# Print a Greek dashboard for all DRFT options
for sym in exchange.instruments():
if not sym.startswith("DRFT-"):
continue
g = exchange.greeks(sym)
if not g:
continue
p = exchange.price(sym)
last = p["last"] or p["fair"] or 0
edge = last - g["theo"]
print(f"{sym:>15s} Δ={g['delta']:+.3f} Γ={g['gamma']:.5f} Θ={g['theta']:+.3f} ν={g['vega']:.3f} IV={g['iv']:.1%} edge={edge:+.2f}")
Common Options Strategies
Long Call — Leveraged Upside
Buy a call to profit when you expect a large upward move. Maximum loss is the premium paid; upside is unlimited. Best used when you have a directional view but want limited downside.
# Buy the DRFT 230-strike call (OTM call, costs less than ATM)
p = exchange.price("DRFT-230-C")
if p["ask"]:
ack = await exchange.buy("DRFT-230-C", 5, price=p["ask"])
exchange.log(f"Long call entry: {ack['order_status']}")
Protective Put — Downside Hedge
Own stock + buy a put = capped downside. The put acts like insurance: if the stock falls below the strike, the put gains value, offsetting your loss on the stock position.
# Already long DRFT. Buy the 210-put to hedge
hedge_ack = await exchange.buy("DRFT-210-P", 5, price=exchange.price("DRFT-210-P")["ask"])
exchange.log(f"Hedge in place: {hedge_ack['order_status']}")
Straddle — Pure Volatility Play
Buy an ATM call and ATM put with the same strike and expiry. Profits if the underlying moves sharply in either direction; loses if it stays flat. Use before expected news when you don't know the direction.
# ATM straddle on QNTM (ATM ≈ 330)
call_ack = await exchange.buy("QNTM-330-C", 3, price=exchange.price("QNTM-330-C")["ask"])
put_ack = await exchange.buy("QNTM-330-P", 3, price=exchange.price("QNTM-330-P")["ask"])
exchange.log(f"Straddle: call={call_ack['order_status']} put={put_ack['order_status']}")
Market Microstructure
Understanding how the market actually works at a mechanical level makes you a better trader — and helps you build smarter strategies.
Bid-Ask Spread & Depth
The bid-ask spread is the gap between the highest buyer's price (bid) and the lowest seller's price (ask). The spread is the market maker's profit margin — they quote both sides and capture the spread on each fill. In liquid instruments (e.g., ACME) spreads are tight (a few cents). In thin instruments (e.g., deep OTM options) spreads can be several percent.
Depth refers to the quantity available at each price level. A deep book has large quantities resting at many price levels — big orders fill without moving the price much. A thin book has small quantities; a large market order can sweep multiple levels and move the price significantly.
| Concept | What it means for you |
|---|---|
| Tight spread (1–5 bps) | Cheap to trade — cross the spread and you've barely paid. Good for fast in/out. |
| Wide spread (50+ bps) | Expensive to cross. Consider using limit orders inside the spread instead. |
| Deep book (large qty at bid/ask) | Your order won't move the price much. Safe to use market orders. |
| Thin book (small qty) | Large orders will "walk the book" and fill at progressively worse prices. |
You can read the live depth with exchange.book(sym) — check how much quantity sits at each level before sizing a market order.
How Bots Behave
63 automated bots operate simultaneously per exchange. Understanding their roles helps you predict when and why prices move.
| Bot type | What it does | Opportunity for you |
|---|---|---|
| Market Makers (MM) | Quote symmetric bids and asks around fair value. Provide liquidity. Inventory skew: buy more when short, sell more when long. | Take liquidity when fair value diverges from last price. The MM will eventually adjust fair value and the price will follow. |
| Momentum (MOM) | Buy on upticks, sell on downticks. Amplify trends. After confirmed news, they pile in — accelerating the move. | Front-run or ride momentum. After a scenario shock, momentum bots drive the continuation — you can join early. |
| Mean Reversion (MR) | Fade large deviations from fair value. Do not fade confirmed news moves. | On confirmed news, MR bots stand aside — the trend is cleaner. On rumours, MR bots fade — the price may drift back. |
| Options Arb (OPT) | Buy cheap / sell rich options relative to Black-Scholes fair value. Keep option prices near theoretical. | If you can spot a mispriced option before the arb bots do, you can exploit it briefly. |
| ETF Arb (ETF) | Keep ETF price near NAV. If ETF trades at a premium: sell ETF, buy components. If discount: buy ETF, sell components. | ETF premiums/discounts rarely persist more than a few ticks. Trade the basket vs ETF spread before the arb bots close it. |
During news events, market makers widen their spreads (they become uncertain about fair value). This creates a window where the spread is wider and the book is thinner — skilled traders can provide liquidity at prices that would normally be impossible to fill.
Trading Around News
News events follow a two-phase model: an initial shock (partial move on announcement) followed by resolution drift (remaining move on confirmation, or full reversal on denial).
| Source label | Credibility | Typical upfront move | Strategy |
|---|---|---|---|
| Government Statement / Corporate Filing | Very high (≥ 95%) | 90%+ of the full shock | Act fast — most of the move happens immediately. Buy/sell as the announcement lands. |
| Reuters | High (85–95%) | ~80% upfront | Position with the move. Small remaining drift on confirmation is tradeable. |
| Analyst Note | Medium (60–80%) | ~60% upfront | Confirmation provides another 20–40% continuation. Denial reverses most of the move. |
| Industry Whisper / Anonymous Tip | Low (20–50%) | 20–35% upfront | High-risk play. If confirmed: large additional move. If denied: full reversal. Watch the source carefully. |
The resolution window: After a news event fires, there is a delay before the outcome (Confirmed / Denied) is determined. During this window, the price hovers near the partially-moved level. This uncertainty window is where the most interesting trading happens — you must judge whether the headline is credible.
Post-resolution drift: Even after resolution, the full shock may not be immediately priced in. There is a drift period where fair value gradually moves toward the final level. Momentum strategies can exploit this drift by riding the continuation.
Stocks
26 simulated equities (ACME, BRYN, CDLR … ZPHR). Each follows a correlated GBM — a shared market factor causes most stocks to move together with their own idiosyncratic noise layered on top. Volatility ranges from ~40% annualised (low-vol, e.g. HRZN) to 100% (high-vol names like UMBR, GLDR, QNTM). All stocks have a 0.01 tick size.
Indexes
5 synthetic indexes: EXMINI (~4500), TECHX (~18750), FINX (~1380), ENRGX (~780), and VOLAX (~16, the volatility index). Indexes use the same GBM but with lower idiosyncratic vol and coarser tick sizes. VOLAX has very high vol-of-vol.
Futures
Futures are listed on 15 underlyings: EXMINI, TECHX, ENRGX, ACME, DRFT, QNTM, HRZN, JNTR, and the 7 commodities (GLDX, SLVX, OILX, NATG, COPX, WHTX, PLTX). Three expiries exist per underlying:
| Symbol | Expiry | Example |
|---|---|---|
{UNDERLYING}-F | Front month — 30 days | DRFT-F |
{UNDERLYING}-F2 | Second month — 60 days | DRFT-F2 |
{UNDERLYING}-F3 | Third month — 90 days | DRFT-F3 |
Fair value is continuously updated as spot × er × T where r = 4% and T is the expiry in years. Longer-dated futures trade at a slight carry premium over the front month, reflecting the cost-of-carry term structure. The calendar spread (e.g. F3 − F2) can be traded by legging into both.
ETFs
Seven sector basket ETFs track weighted portfolios of underlying stocks or commodities. Named with a -ETF suffix:
| Symbol | Name | Components |
|---|---|---|
TECH-ETF | Technology Sector ETF | DRFT, QNTM, FNTM, NXUS (25% each) |
ENRG-ETF | Energy & Resources ETF | CDLR, UMBR, GLDR, YNDR (25% each) |
INDU-ETF | Industrials ETF | ACME, JNTR, MRVL, SLPH (25% each) |
HLTH-ETF | Healthcare & Biotech ETF | HRZN 40%, BRYN 25%, SLPH 25%, KLPS 10% |
DFNS-ETF | Defense & Aerospace ETF | XNTH 40%, JNTR 30%, IGNT 30% |
CONS-ETF | Consumer & Retail ETF | WLLW 30%, ELYS 25%, RVNG 25%, YNDR 20% |
CMDT-ETF | Industrial Commodities ETF | OILX 40%, NATG 25%, COPX 20%, WHTX 15% |
Each ETF's fair value tracks the weighted average of its components' fair values in real time. ETFs can trade at a small premium or discount to NAV (Net Asset Value) — bots will arb the gap, but you can trade that spread too. An ETF position gives broad sector exposure without managing individual stock legs.
Broad-market ETFs: SNP10 & SNP15
SNP10 and SNP15 track the 10 and 15 highest-priced stocks in the universe (a stand-in for the "biggest" names), cap-weighted rather than equal-weighted — each component's weight is proportional to its own fair value, so pricier names dominate the index more, just like a real market-cap-weighted index (e.g. the S&P 500).
| Symbol | Components | Weighting |
|---|---|---|
SNP10 | The 10 highest-priced stocks | Cap-weighted (proportional to price) |
SNP15 | The 15 highest-priced stocks — a strict superset of SNP10's 10 | Cap-weighted (proportional to price) |
Because SNP10 is nested inside SNP15 and both are weighted off the same underlying prices, a stock's weight ranking is always consistent between the two: whichever SNP10 name carries more weight also carries more weight in SNP15 (just diluted, since SNP15 splits weight across 5 more names).
Options
Options are listed on DRFT, QNTM, EXMINI, and TECHX — 10 per underlying (5 strikes × call/put), named {UNDERLYING}-{STRIKE}-C or -P, all with 30-day expiry (40 contracts total).
Strikes span ATM ± 2 steps, where the step size is calibrated to the underlying price (e.g. $10 for mid-price stocks, $50 for indexes). Fair values are continuously updated via Black-Scholes (r = 4%, T = 30/365 years). Tick size is 0.05; market-maker spread is wider than equities (reflecting real-world option spreads).
{UNDERLYING}W-{STRIKE}-C/-P (50 more contracts). These carry a steeper skew and smile than the 30-day family, and their implied vol rises further as expiry approaches within the session — a real theta/gamma-acceleration effect.
Options incorporate an equity skew: lower-strike puts are priced with higher implied volatility than ATM, and higher-strike calls are cheaper. This reflects the asymmetric demand for downside protection seen in real markets.
| Strike position | Vol adjustment |
|---|---|
| Deep OTM put (K ≪ S) | Higher vol — most expensive relative to ATM |
| ATM | Base vol |
| Deep OTM call (K ≫ S) | Lower vol — cheapest relative to ATM |
VOL-TRADE Simulated Exchange · All data is fictional · No real money involved