Market LabDocs
Scripting

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 mlabd

The runtime depends on the command:

CommandExecution target
script backtestHistorical in-process simulator
script runExecution disabled
script run --venue bulkLive 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 \
  --symbol BTC/USDT \
  --source candles@bulk:timeframe=60 \
  --venue bulk \
  --param 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.

Use --duration <seconds> to bound the live session independently of trading outcomes. For example, --duration 3600 runs for at most one hour. Omit it to run indefinitely. Duration expiry completes the script job; it does not represent TP/SL or close a position automatically.

Live execution requires an authorized BULK agent:

mlab auth set bulk

ctx.trade

Place a market or limit order:

const entry = ctx.trade({
  key: 'btc-entry-v1',
  position: 'open-long',
  margin: 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:

FieldRequiredContract
keyYesNon-empty strategy idempotency key, at most 128 bytes
positionYesopen-long, open-short, close-long, or close-short
sizeOpen: one ofPositive base quantity; optional on close
marginOpen: one ofPositive quote collateral; invalid on close
leverageOpen: noAt least 1; defaults to 1; invalid on close
order.typeNomarket or limit; defaults to market
order.priceLimit onlyPositive limit price
order.tifLimit onlygtc, ioc, or alo; defaults to gtc
slOpen: noNative stop-loss trigger price; invalid on close
tpOpen: noNative take-profit trigger price; invalid on close

Opening operations require exactly one of size or margin. Market Lab multiplies margin by leverage to obtain the order exposure, then converts that exposure to lot-aligned size. Market, price, leverage, lot-size, tick-size, and minimum-notional rules are validated before signing.

For example, margin: 100 with leverage: 5 targets approximately $500 of exposure. Studies such as ctx.study.slippage still accept notional because they measure liquidity for the resulting exposure; pass margin * leverage to those studies.

Closing operations are always reduce-only. Omit size to close the complete matching position, or pass size for a partial close:

ctx.trade({
  key: `close-long-${candle.t}`,
  position: 'close-long',
})

Market Lab uses one-way position semantics. open-long can add to an existing long and open-short can add to an existing short. An opening operation cannot silently reverse the opposite position: submit close-long before open-short, or close-short before open-long. Closing the wrong side or more than the current size is rejected.

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}`,
  position: 'open-long',
  margin: 100,
  order: { type: 'market' },
})

Native SL and TP

sl and tp are attached to the parent order using BULK native on-fill actions.

  • sl creates native stop protection.
  • tp creates 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 is valid only on open-long and open-short. 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,
      },
    }
  }
}

onExecution remains a two-argument hook. Live source history is passed only to onData(ctx, input, history).

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, and order.updated
  • order.fill, order.filled, order.cancel_requested, and order.cancelled
  • order.rejected, order.cancel_failed, and order.cancel_rejected
  • position.updated, position.closed, position.liquidated, and position.adl
  • account.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. Both live runs and backtests expose the current position for the script symbol through input.positions.open; the historical simulator applies fills and protection internally.

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 jsonl

Stop 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:

  1. the worker sends commands to mlabd
  2. execution errors are appended as script.execution.error
  3. lifecycle events are delivered to onExecution
  4. the lifecycle event is appended as script.execution.event whether or not onExecution is exported; an explicit { metrics, meta } hook return is included only when present

Ordinary onData calls that return nothing, null, or {} do not append a script.run.result. A non-empty { metrics, meta } return is an explicit diagnostic and is logged. Returned signal and intent objects are not accepted and cannot execute an order.

The strategy never receives the agent private key. Signing, nonce sequencing, account streaming, order correlation, and event journaling remain inside mlabd.

On this page