Script Execution
Simulate and execute strategy scripts with ctx.trade, ctx.cancel, and onExecution.
Script Execution
Strategy scripts do not ask another layer to interpret a returned signal. They call a concrete execution API:
onData -> ctx.trade / ctx.cancel -> simulator or mlabdThe runtime depends on the command:
| Command | Execution target |
|---|---|
script backtest | Historical in-process simulator |
script run | Execution disabled |
script run --venue bulk | Live BULK execution through mlabd |
Market data and execution are independent. A job can read MMT and execute on BULK, or use BULK for both.
Deploy a Live Strategy
script run submits an immutable copy of the script to mlabd and returns immediately:
mlab script run ./scripts/bulk-limit-protected.js \
--provider bulk \
--symbol BTC/USDT \
--source candles:timeframe=60 \
--venue bulk \
--param candles:armed=true--venue bulk is the explicit switch that enables ctx.trade and ctx.cancel. Omitting it keeps an analysis-only job unable to trade.
Live execution requires an authorized BULK agent:
mlab auth set bulkctx.trade
Place a market or limit order:
const entry = ctx.trade({
key: 'btc-entry-v1',
side: 'long',
notional: 100,
leverage: 5,
order: {
type: 'limit',
price: 65000,
tif: 'gtc',
},
sl: 63000,
tp: 69000,
})The call validates synchronously and returns a stable local reference:
{ id: "ord_...", key: "btc-entry-v1" }It does not wait for a venue fill. The command is serialized through mlabd, where the agent credential is loaded and the signed order is submitted.
Request fields:
| Field | Required | Contract |
|---|---|---|
key | Yes | Non-empty strategy idempotency key, at most 128 bytes |
side | Yes | long or short |
size | One of | Positive base quantity |
notional | One of | Positive quote notional |
leverage | No | At least 1; defaults to 1 |
order.type | No | market or limit; defaults to market |
order.price | Limit only | Positive limit price |
order.tif | Limit only | gtc, ioc, or alo; defaults to gtc |
reduceOnly | No | Reduce-only execution; defaults to false |
sl | No | Native stop-loss trigger price |
tp | No | Native take-profit trigger price |
Set exactly one of size or notional. Market, price, leverage, lot-size, tick-size, and minimum-notional rules are validated before signing.
Idempotency
The key belongs to one script job. Repeating the exact request returns the existing managed order. Reusing the key with different parameters is rejected.
This makes retries safe across repeated data hooks and worker restarts. Derive keys from stable strategy facts rather than the current wall-clock time:
const order = ctx.trade({
key: `ema-cross-${candle.t}`,
side: 'long',
notional: 100,
order: { type: 'market' },
})Native SL and TP
sl and tp are attached to the parent order using BULK native on-fill actions.
slcreates native stop protection.tpcreates native take-profit protection.- both create a native OCO range where one trigger cancels its sibling.
- protection activates after the parent entry fills.
- Market Lab does not poll prices locally to emulate triggers.
Protection cannot be attached to a reduce-only order. Trigger prices must align with the market tick and be on the correct side of the entry.
The backtest simulator uses the same request fields. On candle data, if one bar touches both sl and tp, the simulator chooses the stop first because the intra-bar path is unknown.
ctx.cancel
Cancel a managed order by its stable local ID or original trade key:
ctx.cancel({
key: 'cancel-btc-entry-v1',
order: entry.id,
})The return value confirms that the command was queued:
{
key: "cancel-btc-entry-v1",
order: "ord_...",
status: "queued"
}Cancellation keys are idempotent inside the job. If cancellation is requested before BULK returns its order ID, mlabd records order.cancel_requested; no venue cancellation is sent at that moment.
onExecution
Live jobs may export a second hook for asynchronous order, fill, position, and account events:
export function onExecution(ctx, event) {
if (event.type === 'order.rejected') {
return {
metrics: {
rejected_order: event.orderId,
details: event.data,
},
}
}
if (event.type === 'order.fill') {
return {
metrics: {
order_id: event.orderId,
venue_order_id: event.venueOrderId,
status: event.status,
},
}
}
}Event envelope:
{
"seq": 7,
"jobId": "job_...",
"tsMs": 1780000000000,
"type": "order.fill",
"orderId": "ord_...",
"key": "btc-entry-v1",
"venue": "bulk",
"venueOrderId": "...",
"status": "filled",
"terminal": false,
"data": {}
}Event types include:
order.pending,order.accepted,order.terminal, andorder.updatedorder.fill,order.filled,order.cancel_requested, andorder.cancelledorder.rejected,order.cancel_failed, andorder.cancel_rejectedposition.updated,position.closed,position.liquidated, andposition.adlaccount.margin_updated
onExecution uses the same persistent QuickJS session as onData, so module state remains available. It may call ctx.trade or ctx.cancel to react to an event.
Events are journaled per job and acknowledged only after the hook succeeds. An unacknowledged event is replayed after a worker restart. Execution keys keep commands idempotent during replay.
onExecution is a live job hook. The historical simulator applies fills and protection internally and exposes resulting positions through input.positions.open instead.
Job Operations
List and inspect deployed jobs:
mlab script jobs
mlab script status <JOB_ID>Tail structured worker output:
mlab script logs <JOB_ID> --follow
mlab script logs <JOB_ID> --follow --output jsonlStop or restart the immutable snapshot:
mlab script stop <JOB_ID>
mlab script restart <JOB_ID>Job states are starting, running, stopping, stopped, completed, and failed.
Worker snapshots, output, execution events, and logs are stored under the owner-only runtime directory:
~/.market-lab/execution/jobs/<JOB_ID>/Failure Boundary
Execution commands are committed only after a hook returns successfully. If the hook throws, commands collected during that invocation are cleared.
After a successful hook:
- the worker sends commands to
mlabd - execution errors are appended as
script.execution.error - lifecycle events are delivered to
onExecution - successful hook output is appended as
script.execution.result
The strategy never receives the agent private key. Signing, nonce sequencing, account streaming, order correlation, and event journaling remain inside mlabd.