VOLTRADE
Overview
VOLTRADE 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.
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 five tabs: Participants, Unfilled, History, Positions, and Book. The Book tab mirrors the depth-of-book for the currently selected instrument and stays visible while you use other parts of the UI.
Top Bar — Equity & P&L Widgets
The right side of the top bar shows three 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. |
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 admin can change market state from the Admin Console (/admin).
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.
Mouse & Trackpad Controls
| Action | Effect |
|---|---|
| Hover | Crosshair with price tooltip and cursor price on Y axis. |
| Click & drag left/right | Pan through history (X axis). Automatically locks to a fixed number of visible points so the view stays anchored while you drag. |
| Click & drag up/down | Pan the price range (Y axis). Drag up to show higher prices, drag down to show lower prices. |
| Double-click | Reset to live view (latest data, default zoom, Y axis centred on mid-price). |
| Vertical scroll / pinch (trackpad) | Zoom X axis — scroll up / pinch out = zoom in (fewer points visible); scroll down / pinch in = zoom out (more points). |
| Horizontal swipe (trackpad) | Pan X axis — swipe right = back in time, swipe left = forward toward live. |
| Shift + scroll | Zoom Y axis — tighten or expand the visible price range. |
Chart Controls Reference
A small overlay appears in the top-right corner of the chart whenever you are not in the default live view. It shows the number of visible points and a reminder of the controls. Double-click anywhere on the chart to snap back to live (this also resets any Y-axis pan).
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.
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. The tape remains unaffected by the toggle. Press the button again (which now reads ▸ DEPTH) to restore the strip.
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.
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
Quick Fill Buttons
The two large buttons above the order form — BUY @ ASK and SELL @ BID — submit an immediate limit order at the current best ask or best bid respectively, using the quantity from the QTY field. This is the fastest way to take liquidity. The buttons are disabled when the market is not OPEN or there is no quote.
Manual Order Entry
| Field | Description |
|---|---|
| Side | BUY or SELL. The SEND button turns green for buys and red for sells. |
| Type | LIMIT — rests at your specified price if not immediately matchable. MARKET — fills against the best available price immediately; no price field needed. |
| Qty | Number of lots (minimum 1). Also used by the quick-fill buttons. |
| Price | Required for LIMIT orders. Leave blank for MARKET orders. |
After hitting SEND, the acknowledgement appears in the Python terminal output (bottom panel) as ORDER … → NEW or ORDER … → 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 |
|---|---|
| ● / ○ | Green dot = currently connected WebSocket. Empty circle = registered but disconnected. |
| 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). |
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. |
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. 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.
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. |
| 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 VOLTRADE 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). |
| 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. |
Pre-installed Packages — import directly
These ship inside the Pyodide runtime and can be imported with no installation step:
| Package | Version | Use |
|---|---|---|
numpy | 1.26.x | Fast numerical arrays, linear algebra, FFT, statistics. Essential for vectorised price analysis. |
micropip | bundled | The in-browser package installer — see below. |
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. |
Order Methods (async)
| Method | Description |
|---|---|
await exchange.buy(sym, qty, price=None) | Place a buy order. Omit price for MARKET, include it for LIMIT. Returns an ack dict. |
await exchange.sell(sym, qty, price=None) | Place a sell order. Same signature. |
await exchange.cancel(sym, order_id) | Cancel an open order. Returns {"accepted": True/False, …}. |
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
}
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}%")
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 ~20% annualised (low-vol) to ~50% (high-vol names like UMBR, GLDR, ORBT). 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 8 underlyings: EXMINI, TECHX, ENRGX, ACME, DRFT, QNTM, HRZN, JNTR. 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
Three sector basket ETFs track equally-weighted portfolios of four underlying stocks each. Named with a -ETF suffix:
| Symbol | Name | Components (25% each) |
|---|---|---|
TECH-ETF | Technology Sector ETF | DRFT, QNTM, FNTM, NXUS |
ENRG-ETF | Energy & Resources ETF | CDLR, UMBR, GLDR, YNDR |
INDU-ETF | Industrials ETF | ACME, JNTR, MRVL, SLPH |
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.
Options
Options are listed on DRFT, QNTM, EXMINI, and TECHX — 10 per underlying (5 strikes × call/put), named {UNDERLYING}-{STRIKE}-C or -P.
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).
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 |
VOLTRADE Simulated Exchange · All data is fictional · No real money involved