Trader guide
What do you want to do?
How the trading screen works, how orders match, and the full Python API. Search with /, or start from one of these.
exchange method with its arguments and what comes back.Python
Trade optionsGreeks, implied volatility and the skew, explained for this exchange.Options
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.
Your Account
One account works everywhere on Voltrade. 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. |
| Email verification | Only needed to host an exchange. You can join and trade the moment you sign up; the reminder to confirm your email appears in the New exchange dialog, not before. |
| 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. |
| Invite links | Opening an invite while logged out shows the exchange on the login page, with Create account and join or Log in and join. Either one puts you straight on its trading screen. |
| 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. |
Trying It as a Guest
When the platform has a featured public exchange, the homepage shows a Join as guest button (at the top of the page and in the header), and the login page has Trade as a guest. While that exchange is running, the homepage's markets section shows its live prices; clicking any tile joins as a guest too. One click puts you on the live trading screen under a generated name like Guest-7KQ4, with the same starting cash as everyone else. The order ticket, chart, book, positions and leaderboard all work; guests appear on leaderboards with a GUEST tag.
| As a guest | Detail |
|---|---|
| Python | Locked. The Python panel, and the full terminal at /pyterm, need a free account. |
| Other exchanges | Guests can only trade the featured exchange. Hosting, joining others, renaming yourself and sending feedback all need an account. |
| Order rate | Capped (20 orders per 10 seconds by default). Plenty for trading by hand; an account lifts it. |
| Coming back | The session lives in this browser. Return within a week and you pick up where you left off. A guest untouched for 7 days is deleted, along with its positions. |
Keeping your trading
The top bar's SIGN UP FREE button (it shows your profit once you have some, e.g. SAVE $1,240.00 · SIGN UP) opens the signup form. Choose an email, a name and a password, and the guest becomes your account: same positions, cash, orders and history, now under your chosen name. You land back on the trading screen with Python unlocked. Names starting with “Guest” are reserved, so pick something else.
The Dashboard
The dashboard is a quote board. Each exchange is a tile showing a short ticker made from its name, its market state, its price and a sparkline of recent movement. The price is the exchange's EX-Mini broad index, and the change is measured from where it stood when the market first opened. A tile reads Asleep when nobody has had the exchange open for a while. It is still there and still open, and prices come back the moment someone opens it.
When an exchange is featured, it gets the large panel at the top with a bigger chart and a Trade button. A ticker tape of its main indexes and futures scrolls across the very top of the page. Hover over the tape to pause it.
| Tab | What it shows |
|---|---|
| Your exchanges | Exchanges you host or have joined, with your role (Host, Trader or Watching) and links to the leaderboard, the admin console (hosts only) and the trading screen. The last tile creates a new exchange. |
| Browse public | Public exchanges anyone can see. Join appears on open ones; the rest need an invite link from their host. |
Light and dark
The Dark / Light switch in the top bar changes the dashboard's colours. Dark is the default. Your choice is remembered in this browser and applies to the dashboard's forms and pop-ups too.
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 Voltrade, 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, and the handle between the code and its output to change how the terminal is split. The terminal folds down to a single bar with Close ▼ (or the ` key); closed, the bar still shows the last output line and a Run button. On screens under 800 px tall it starts closed. Your panel sizes, and whether Python is open, are remembered next time.
The right panel has nine tabs. The four you glance at constantly, Traders, Orders, History and Positions, sit in a row. The five occasional ones, Bio, Book, News, Calendar and Settings, are under More; while one of them is open the More button takes its name, so the row always shows where you are. 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. News and Calendar show fired and upcoming headlines respectively; Settings has session info and a disconnect/back-to-dashboard link.
The Dark / Light switch in the top bar changes the terminal's colours. It is the same setting as the dashboard's and the Python editor's, remembered in this browser.
On a Phone
Below roughly 820 pixels wide — every phone in portrait, and most in landscape — the terminal stops trying to show four panels at once and gives each one the whole screen. A tab bar along the bottom switches between them:
| Tab | What it shows |
|---|---|
| Markets | The instrument list and its type filters. Tapping an instrument selects it and jumps you to Trade, so you rarely come back here twice in a row. |
| Trade | The chart, the depth and tape stacked beneath it, and the order ticket. The ticket deliberately stays here rather than becoming a tab of its own — you want the price in front of you while you set a limit. |
| Book | The market panel: Participants, Unfilled, History, Positions, Bio, Book, News, Calendar and Settings. The tab strip scrolls sideways. |
| Python | The dev environment, editor above and output below. |
The drag handles are gone — there is no width to divide — and so are the clock and the leaderboard link. Log out from the dashboard; the trading screen has no Log out button. Your equity, P&L and open-position counters move onto their own row under the top bar and stay visible whichever tab you are on.
The Python terminal
The Python editor shows one column at a time, with tabs for Files, Code, Output and Reference. Running a script switches you to Output; if output arrives while you are elsewhere, a dot appears on the Output tab. Notebook cells show their results inline, so a notebook stays on Code.
Everything else is unchanged — same account, same exchange, same positions. You can start an order on a laptop and finish it on a phone.
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.
Leaving & Coming Back
You can leave an exchange open as long as you like — days, weeks. There is no need to close it when you finish for the day, and nothing is lost if you simply shut your laptop.
When everyone has been disconnected for a few minutes, the exchange goes to sleep in the background. It stays open: it keeps its place in Browse Public, its members, and its session timer. Opening it again brings it straight back.
What you'll find when you return
- Prices will have moved. The market doesn't freeze while you're gone — the time you were away is simulated and applied the moment you come back, so returning after a long break feels like returning to a market that kept trading. Longer absences move prices more, up to a cap of about four hours' worth of drift.
- Your positions and cash are exactly as you left them. So is your order history, your saved Python code, and the price chart's history.
- Your resting orders are still on the book — they are restored with the exchange. Because prices jump to catch up, an order that was far from the market may now be near it, or already the best bid or offer. Check your Unfilled Orders tab before you start trading again.
- Bot orders are rebuilt fresh. The bots re-quote the moment the exchange wakes, so the book fills back in within a tick or two.
One small thing to know: if you look at the leaderboard of an exchange nobody has open, positions are valued at the instrument's fair value rather than at live bid/offer. The numbers can differ very slightly from what you'd see with the market up.
Standings
Every exchange has a ranks page, linked from the dashboard and the terminal. The top three are on a podium, and everyone else is in a table below them. Your own row has a You tag. Only people appear; bots are never ranked.
| Part | What it shows |
|---|---|
| Rank by | P&L, Return % (P&L divided by your starting cash) or Equity. Your browser remembers the choice. |
| Arrows | Places gained (▲) or lost (▼) in the last 5 minutes. |
| Sparkline | Your P&L over the last 10 minutes, sampled every 15 seconds while the market is open. |
| Clock | The time left in the session, if the host has set a timer. |
When the session closes, the standings freeze as the final results, without arrows or sparklines.
Projector mode
Present shows the standings full screen in large type, for a classroom screen. It adds the session clock, the biggest climber and the last headline. When the host opens it, it can also show the address for joining. It updates by itself. Press Esc to leave.
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. If an order is refused (not enough cash, a position limit, the market not open, and so on), or a cancel, flatten or the connection fails, a short notice also pops up at the bottom right of the screen (above the tab bar on a phone). You see it even with the Python panel hidden or locked. Click a notice to dismiss it.
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.
Examples in this guide
Python examples here have Copy and Run in editor buttons. Run in editor opens the Python editor in a new tab with the code in a new notebook cell, on the exchange whose trading screen you opened last. If you haven't opened one in this browser yet, open a trading screen first; the code waits ten minutes for the next editor you open.
Press / anywhere on this page to search it. The Trader / Host switch at the top moves between this guide and the host handbook.
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). |
| Close ▼ / Open ▲ | Fold the terminal down to one bar (last output line and Run) to give the chart and order ticket the room, or open it again. The ` key does the same from anywhere outside a text box. |
| ⎋ OPEN TERMINAL | Opens the dedicated Monaco terminal (see below) in a new tab, pre-populated with your participant ID. |
The Python editor (/pyterm)
A full-screen Python environment built on Monaco (the editor inside VS Code). Open it from the trading screen's Python bar (Open editor). It trades as the same participant as the terminal you came from, so orders, positions and P&L are shared. It has two modes over one set of files, switched with Script / Notebook in the top bar.
Your files
The left column lists My files: scripts (py) and notebooks (nb) saved to your account, so they are there on any device you sign in from. + Script and + Notebook start a new one; hover a file for Rename and Delete, or double-click its name to rename it. Open files appear as tabs above the editor, and the tabs you had open come back next visit. A dot on a tab means unsaved changes; saving is manual (Ctrl+S or Save). You can keep up to 50 files. Examples, below your files, open a starter as a new file: a mean-reversion loop, a market maker, spread and tape watchers, an options scan, and a research notebook.
If you saved code in the old single-script terminal, it was moved into main.py the first time you opened the new editor.
Script mode
| Feature | Detail |
|---|---|
| Run / Ctrl+Enter | Runs the whole script from the top, with a fresh set of variables each time. The top bar shows Running · 12s while it goes. |
| Stop / Ctrl+. | Stops the script at its next step, even inside while True. Orders already sent stay sent. |
| Code in another tab | Code runs inside the browser tab that started it. If it is running in a different tab (say you opened the editor twice), the top bar says main.py running in another tab and Stop stops it there. The trading screen's Python bar does the same for code running in the editor. |
| Back to trading | While code is running, this switches to your trading tab (opening one if needed) and leaves the editor tab, and your script, running. Closing or reloading the tab while code runs asks first, because that stops it. |
| Output | print() output with timestamps. Errors are red and point at the line that raised them; fills are green; system messages are blue. |
| Problems | Checked as you type. It catches the two mistakes that silently cost you: calling exchange.buy, sell, cancel or history without await (the order is never sent), and time.sleep() (it freezes the page, Stop included). Each has a one-click fix: Add await / Use asyncio.sleep, also offered from the lightbulb in the editor. Problems never stop a script from running. |
| Script orders | Every order and cancel your code sent this session, with its result (accepted, filled, rejected and why). |
| API reference | The right column lists every exchange method, with a filter box. Click one to insert it at the cursor, await included where needed. Hover any exchange. call in your code for the same docs, and type exchange. for completions. |
| Status bar | Connection, market state, save state, problem count, cursor position and Python version. |
Notebook mode
A notebook is a list of cells that share one Python session: a variable set in one cell is there in the next, so you can fetch history once and then look at it ten ways without fetching again.
| Feature | Detail |
|---|---|
| Shift+Enter | Runs the cell and moves to the next (adding one at the end). Ctrl+Enter runs it and stays. The number beside a cell, [3], is the order cells ran in; [*] means running. |
| Last line is shown | If a cell ends with an expression, its value is displayed, as in Jupyter. Lists of dicts, dicts of dicts and pandas DataFrames show as tables; anything else as text. |
plot(…) | Draws a line chart in the cell: plot(await exchange.history("ACME", n=200), fair=True). Takes a history() list, a list of numbers, or a dict of name → list for several lines; fair=True adds the instrument's fair value as a dashed line. |
display(…) | Shows a value the same way the last line does, from anywhere in a cell. |
| Run all / Restart Python | Run all goes top to bottom and stops at the first error. Restart Python clears every variable so you can start clean. Clear outputs keeps the code. |
| Text cells | + Text adds a note. Write # for a heading, **bold**, `code` and - lists; click a note to edit it. |
| Outline | The left column lists the notebook's headings and cells; click one to jump to it. |
| Variables | The right column's Variables tab lists everything your cells defined, with type and value, plus live prices for the symbols your notebook mentions. |
Notebooks save only the code and notes, not the outputs: run them again to see fresh numbers.
await is supported everywhere. 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. |
Bundled Packages — just import them
Scripts and notebooks share the same Python. Before each run the editor scans your code for import statements and loads any package Pyodide ships with, so there is no install step. The first import of a package downloads it (about 1–6 s); after that it is instant until you close the tab.
| Package | Use |
|---|---|
numpy | Arrays, linear algebra, vectorised returns and statistics. |
pandas | DataFrames, time series, groupby, rolling windows. |
scipy | Statistics (scipy.stats), optimisation, interpolation; norm.cdf for Black-Scholes. |
statsmodels | OLS, ARIMA, cointegration and stationarity tests. |
sklearn (scikit-learn) | Regression, clustering, classifiers, PCA. |
xgboost, lightgbm | Gradient-boosted trees. |
cvxpy | Convex optimisation, e.g. portfolio weights. |
sympy | Symbolic maths, e.g. deriving Greeks. |
networkx | Graphs, e.g. correlations as a network. |
polars, pyarrow | Fast columnar DataFrames. |
matplotlib, bokeh, altair | Import and compute fine, but their figures don't show in the editor. Use plot() for charts. |
dateutil, pytz, regex, joblib, patsy, PIL, lxml, bs4 | Utilities that come along with the above. |
The full list is on the Pyodide 0.27.7 package page; anything there imports the same way.
import pandas as pd
snap = exchange.snapshot()
rows = [{"sym": s, "kind": i["kind"], "fair": i.get("fair_value"), "last": i.get("last_price")}
for s, i in snap["instruments"].items()]
df = pd.DataFrame(rows).set_index("sym")
print(df.head(10).to_string())
Other Packages from PyPI — micropip
A package Pyodide doesn't ship can be installed from PyPI if it is pure Python and its dependencies are available. Install it at the top of the script, or in the first notebook cell; it stays installed until you close the tab.
import micropip
await micropip.install("seaborn")
import seaborn as sns
Checked and working this way: seaborn, plotly, tabulate. Packages that need compiled code Pyodide doesn't have fail to install: yfinance, arch, ta, pandas_ta, TA-Lib, torch, numba, QuantLib.
Not Available
- Network access from Python —
socket,websockets,aiohttp,requests,httpx.requestsimports but can't fetch market data from outside sites. All exchange I/O goes through theexchangeobject. - 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. - 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
Every order call returns this dict once the exchange has handled the order.
| Field | Meaning |
|---|---|
accepted | False if the order was rejected. |
order_id | Use it to cancel later. |
order_status | "NEW", "PARTIAL", "FILLED" or "REJECTED". |
trades | List of fills if it matched straight away. |
reason | Why it was rejected, otherwise None. |
# Bid one tick under the best ask
p = exchange.price("ACME")
bid = round(p["ask"] - 0.05, 2)
ack = await exchange.buy("ACME", 10, price=bid)
print(ack["order_status"], ack["order_id"])
Run in editor opens the Python editor in a new tab with this code in a notebook cell, on the exchange you last traded.
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 |
Voltrade simulated exchange · All data is fictional · No real money involved