Algos
Open Algos in the order panel to write TypeScript that places simulated orders as replay advances. Pick the built-in example or create an algo, select an instrument, and press Start. Orders appear in the chart, DOM, position readout, and trade history.
Execution
Register one onUpdate(callback) at the top level. The script initializes once at Start; variables outside the callback persist until Stop. The callback runs once per new replay batch for the selected instrument, after the batch's existing order fills are processed.
symbol and tickSize are fixed for the run. The callback receives time in Unix milliseconds, bid and ask as current prices, and trades as that batch's new aggressive orders, oldest first. Each trade has a millisecond timestamp, side (buy, sell, or none), total size, and levels containing price and size.
book is the resting book at the end of the batch. book.bid and book.ask are arrays of price and size, best price first, holding every level the replay has loaded. Size pulled from or stacked onto a price is the difference between this update's book and the last one's; keep the previous book in a variable outside the callback to compare.
A batch can contain multiple trades or only book updates. Its bid/ask is the quote at the end of that batch. Orders submitted in response to a trade use this quote, rather than the historical quote at that trade. Batch resolution can change with replay speed.
Callbacks and order calls are synchronous. Place orders inside the callback; imports and async callbacks are unsupported. print(...values) writes to Activity, which retains the latest 100 lines.
interface AlgoTrade { timestamp: number; side: "buy" | "sell" | "none"; size: number; levels: { price: number; size: number }[] }
interface AlgoBookLevel { price: number; size: number }
interface AlgoBook { bid: AlgoBookLevel[]; ask: AlgoBookLevel[] }
interface AlgoUpdate { time: number; bid: number; ask: number; book: AlgoBook | null; trades: AlgoTrade[] }
interface AlgoBracket { tp?: number; sl?: number }
interface AlgoOrder { id: string; kind: "limit" | "stop"; side: "buy" | "sell"; price: number; qty: number }
interface AlgoFill { kind: "market"; time: number; side: "buy" | "sell"; price: number; qty: number }
interface AlgoFillRecord { time: number; side: "buy" | "sell"; price: number; qty: number; opened: boolean }
interface AlgoPosition { side: "long" | "short" | "flat"; qty: number; avgPrice: number }
declare const symbol: string
declare const tickSize: number
declare function onUpdate(callback: (update: AlgoUpdate) => void): void
declare function print(...values: unknown[]): void
declare const orders: {
buy(qty: number, bracket?: AlgoBracket): AlgoFill
sell(qty: number, bracket?: AlgoBracket): AlgoFill
buyLimit(price: number, qty: number, bracket?: AlgoBracket): AlgoOrder | AlgoFill
sellLimit(price: number, qty: number, bracket?: AlgoBracket): AlgoOrder | AlgoFill
cancel(id: string): void
cancelAll(): void
flatten(): AlgoFill | null
working(): AlgoOrder[]
fills(): AlgoFillRecord[]
position(): AlgoPosition
}Orders
orders.buy(qty, bracket?) buys at the current ask; orders.sell(qty, bracket?) sells at the current bid. Both return a market fill with time, side, price, and quantity.
orders.buyLimit(price, qty, bracket?) and orders.sellLimit(price, qty, bracket?) return a working order with an id, kind, side, price, and quantity. A limit crossing the current spread executes immediately and returns a market fill instead.
Quantities must be positive integers. Prices, including bracket tp and sl, are actual prices on the instrument's tick grid. Take profit must be beyond entry in the trade direction, and stop loss must be behind it. A reducing order cannot attach an entry bracket. Market-entry brackets apply to the resulting net position.
Submitting another limit on the same side at the same price throws an error. Cancel the existing order by id before replacing it.
These orders use the replay order system and its simulated fill rules.
// Follow a large trade with a bracketed entry.
// https://marketbyorder.com/docs/api/algos
const MIN_SIZE = 100
const TAKE_PROFIT_TICKS = 8
const STOP_LOSS_TICKS = 4
const COOLDOWN_MS = 30_000
let nextEntryTime = 0
onUpdate(({ time, bid, ask, trades }) => {
if (time < nextEntryTime) return
if (orders.position().side !== "flat" || orders.working().length > 0) return
const signal = trades.findLast(trade => trade.size >= MIN_SIZE && trade.side !== "none")
if (!signal) return
if (signal.side === "buy") {
orders.buy(1, {
tp: ask + TAKE_PROFIT_TICKS * tickSize,
sl: ask - STOP_LOSS_TICKS * tickSize,
})
} else {
orders.sell(1, {
tp: bid - TAKE_PROFIT_TICKS * tickSize,
sl: bid + STOP_LOSS_TICKS * tickSize,
})
}
nextEntryTime = time + COOLDOWN_MS
})
Position and working orders
orders.position() returns the shared Sim position for this instrument: side (long, short, or flat), quantity, and average price. orders.working() returns working limits and stops, including active bracket exits. orders.fills() returns every fill so far, oldest first, each with time, side, price, quantity, and whether it opened or closed position.
orders.cancel(id) cancels a working order and its linked orders according to replay rules. orders.cancelAll() cancels every working order for the bound instrument. orders.flatten() closes its net position at market and cancels all its working orders, returning a market fill or null when already flat.
Manual trades and algo trades share this position and these orders. An algo can see and cancel manually placed orders. Only one algo can run per instrument; fills and PnL are shown in the existing trading interface.
onUpdate(() => {
const position = orders.position()
const working = orders.working()
if (position.side !== "flat" || working.length > 0) return
})Backtest
Backtest runs the same script over the trades already loaded for the selected instrument, without replaying. It uses the same order rules and fill matching as a live run against a separate scratch account, so the shared Sim position, working orders, and trade history are untouched.
The callback runs once per trade, oldest first, after that trade has been matched against working orders. Where a book snapshot within two seconds exists, bid, ask and book come from it. Otherwise bid and ask are inferred from the trade, a buy prints at the ask, a sell prints at the bid, and the other side is one tick away, and book is null. Book history covers the pre-roll and what has played, which is far less than the loaded trades, so check for null before reading levels.
Loaded history is whatever the chart has fetched. Scroll further back to test more. Gaps in the loaded range produce no updates, so nothing happens inside them. Progress shows in the editor, the position readout gains a line for the backtest account, and the finishing line reports round trips, wins, losses, and net PnL. Stop backtest ends the run early; Clear removes the result.
A backtest processes every trade one at a time, so its fills can differ from a live run, where an update covers a playback batch and uses the quote at the end of it.
onUpdate(({ time, bid, ask, trades }) => {
const last = orders.fills().at(-1)
if (last && time - last.time < 60_000) return
const trade = trades[0]
if (trade.size < 200) return
if (orders.position().side !== "flat") return
if (trade.side === "buy") orders.buy(1, { tp: ask + 8 * tickSize, sl: ask - 4 * tickSize })
})Saving and stopping
Signed-in users' algos autosave to their library. Guest edits last for the current session. Built-in algos are read-only; choose Duplicate to edit to create your own copy.
Start takes a fixed copy of your code. Editing or renaming a running algo changes its library draft. Stop and Start again to run the new code with fresh variables.
Closing the editor keeps execution running. The order panel shows running, paused, and error states; closing the order panel leaves a compact algo control visible. Pausing replay pauses algo execution. Resetting, seeking to a new replay start, removing the instrument, or leaving the replay stops execution.
Stop ends execution and leaves the position and working orders in place. Stop & Flatten also closes the position and cancels working orders for the bound instrument, including manual orders. Runtime errors stop execution and leave existing orders in place. More than 20 order operations in one update stops the algo with an error.
Runs bind to Sim and the instrument selected at Start. Switching the order panel or editor to another instrument does not redirect an existing run. Saved trade playback cannot run algos. Reloading the page never restarts an algo automatically.
onUpdate(({ trades }) => {
const latest = trades[trades.length - 1]
if (latest) print(symbol, latest.side, latest.size)
})