# Script Execution (/scripting/execution)



Strategy scripts do not ask another layer to interpret a returned signal. They call concrete execution APIs:

```txt
onData / onExecution -> ctx.trade / ctx.order / ctx.cancel -> simulator or mlabd
```

The runtime depends on the command:

| Command                                         | Execution target                                                 |
| ----------------------------------------------- | ---------------------------------------------------------------- |
| `script backtest`                               | Historical in-process simulator                                  |
| `script run`                                    | Execution disabled                                               |
| `script run --venue bulkf`                      | Live BULK mainnet execution through `mlabd`                      |
| `script run --venue bulkf --testnet`            | Live BULK testnet execution through `mlabd`                      |
| `script run --venue hyperliquidf`               | Live Hyperliquid mainnet execution through `mlabd`               |
| `script run --venue hyperliquidf --testnet`     | Live Hyperliquid testnet execution through `mlabd`               |
| `script run --venue hyperliquidf-xyz`           | Live XYZ mainnet execution through `mlabd`                       |
| `script run --venue hyperliquidf-xyz --testnet` | Live XYZ testnet execution through `mlabd`                       |
| `script run --venue hyperliquidf-io`            | Live EntropyIO mainnet execution through `mlabd`                 |
| `script run --venue hyperlinkf`                 | Live HyperLink mainnet execution through `mlabd`                 |
| `script run --venue hyperliquid`                | Live Hyperliquid Spot or HIP-4 outcome execution through `mlabd` |
| `script run --venue hyperliquid --testnet`      | Live Hyperliquid Spot or HIP-4 testnet execution through `mlabd` |

Market data and execution are independent. A job can read MMT and execute on Hyperliquid, read Hyperliquid and execute on BULK, or use one standalone venue for both.

Every `ctx.trade` and `ctx.order` request names the symbol it affects. That symbol must already be declared by one of the job's sources. One source symbol can produce the signal for an order in another declared symbol, but one live script still executes all of its symbols through a single `--venue`.

## Deploy a Live Strategy [#deploy-a-live-strategy]

`script run` submits an immutable copy of the script to `mlabd` and returns immediately:

```bash
mlab script run ./scripts/bulk-limit-protected.js \
  --source btc@candles@bulkf:timeframe=60 \
  --venue bulkf \
  --param armed=true
```

`--venue bulkf`, `--venue hyperliquid`, `--venue hyperliquidf`, `--venue hyperliquidf-{dex}`, or `--venue hyperlinkf` is the explicit switch that enables execution calls. BULK and Hyperliquid venues use mainnet by default; `--testnet` selects testnet when the market exists there. HyperLink is mainnet-only. Omitting the venue keeps an analysis-only job unable to trade.

Outcome scripts declare exact symbols such as `1009:0@orderbook@hyperliquid`. Outcome execution is spot-like: it can buy an outcome-side token or sell tokens already held. It does not support naked shorting, leverage, reduce-only orders, attached stop loss or take profit, or close-position semantics. See [Hyperliquid outcomes](/providers-execution/hyperliquid#outcome-markets).

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 for the selected network:

```bash
mlab auth set bulk
mlab auth set bulk --testnet
```

Hyperliquid execution requires its network-specific API agents:

```bash
mlab auth set hyperliquid
```

The command configures separate mainnet and testnet agents. A script uses the mainnet agent normally and the testnet agent when its command includes `--testnet`.

## ctx.trade [#ctxtrade]

`ctx.trade` expresses perpetual position transitions. Hyperliquid Spot scripts use `ctx.order` with `buy` or `sell` instead.

Place a market or limit order:

```js
const entry = ctx.trade({
  symbol: 'btc',
  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:

```js
{ id: "ord_...", key: "btc-entry-v1" }
```

It does not wait for a venue fill. The command is serialized through `mlabd`, where the selected venue credential is loaded and the signed order is submitted.

Request fields:

| Field         | Required     | Contract                                                     |
| ------------- | ------------ | ------------------------------------------------------------ |
| `symbol`      | Yes          | Short or full symbol declared by one of the script's sources |
| `key`         | Yes          | Non-empty strategy idempotency key, at most 128 bytes        |
| `position`    | Yes          | `open-long`, `open-short`, `close-long`, or `close-short`    |
| `size`        | Open: one of | Positive base quantity; optional on close                    |
| `margin`      | Open: one of | Positive quote collateral; invalid on close                  |
| `leverage`    | Open: no     | At least `1`; defaults to `1`; invalid on close              |
| `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`                    |
| `sl`          | Open: no     | Native stop-loss trigger price; invalid on close             |
| `tp`          | Open: no     | Native 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:

```js
ctx.trade({
  symbol: 'btc',
  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.

Use `ctx.trade` when the position transition is intentional. Use `ctx.order` when the script needs to place a raw buy or sell whose eventual effect depends on the net inventory when it fills.

### Idempotency [#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:

```js
const order = ctx.trade({
  symbol: 'btc',
  key: `ema-cross-${candle.t}`,
  position: 'open-long',
  margin: 100,
  order: { type: 'market' },
})
```

## ctx.order [#ctxorder]

Place a raw buy or sell without declaring an intended position transition:

```js
const ask = ctx.order({
  symbol: 'btc',
  key: 'btc-maker-ask-1',
  side: 'sell',
  size: 0.01,
  leverage: 5,
  order: {
    type: 'limit',
    price: 66000,
    tif: 'alo',
  },
})
```

`buy` and `sell` are the canonical sides. `long` is accepted as an alias for `buy`, and `short` is accepted as an alias for `sell`. Structured output always serializes the canonical value.

Request fields:

| Field         | Required | Contract                                                                     |
| ------------- | -------- | ---------------------------------------------------------------------------- |
| `symbol`      | Yes      | Short or full symbol declared by one of the script's sources                 |
| `key`         | Yes      | Stable idempotency key shared with `ctx.trade`                               |
| `side`        | Yes      | `buy`/`long` or `sell`/`short`                                               |
| `size`        | One of   | Positive base quantity                                                       |
| `margin`      | One of   | Positive quote collateral used with leverage to derive size                  |
| `leverage`    | No       | At least `1`; defaults to `1`                                                |
| `reduceOnly`  | No       | Defaults to `false`; prevents an order from increasing or flipping inventory |
| `order.type`  | No       | `market` or `limit`; defaults to `market`                                    |
| `order.price` | Limit    | Positive limit price                                                         |
| `order.tif`   | Limit    | `gtc`, `ioc`, or `alo`; defaults to `gtc`                                    |

Exactly one of `size` or `margin` is required. Raw orders do not accept `sl` or `tp`, because their position effect is not known until they fill.

Non-reduce-only raw orders follow execution-venue netting:

```txt
Long 10 + sell 4  -> Long 6
Long 10 + sell 10 -> Flat
Long 10 + sell 14 -> Short 4
```

The same rules apply in backtests. Same-side fills increase the net position and update its weighted entry. Opposite-side fills realize the closed quantity and any remainder opens the other side. A reduce-only raw order can reduce to flat but never flip.

`ctx.order` returns the same stable `{ id, key }` reference as `ctx.trade`. Pass that ID or key to `ctx.cancel`.

### Hyperliquid Spot [#hyperliquid-spot]

A spot symbol includes its real quote asset:

```js
ctx.order({
  symbol: 'hype/usdc',
  key: 'hype-spot-buy',
  side: 'buy',
  margin: 100,
  order: { type: 'market' },
})
```

Run the script with `--venue hyperliquid` or `--venue hyperliquid --testnet`. Spot orders support market and limit orders with GTC, IOC, and ALO. They reject leverage, reduce-only, SL/TP, and position-close semantics. For a buy, `margin` is the quote-asset budget.

## Native SL and TP [#native-sl-and-tp]

`sl` and `tp` remain native venue orders.

They are perpetual-only. Hyperliquid Spot rejects both fields.

* BULK attaches on-fill protection to the parent. Supplying both creates a native OCO range whose triggered leg cancels its sibling.
* Hyperliquid submits reduce-only market triggers with `normalTpsl` grouping. The current adapter does not promise OCO sibling cancellation.
* 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 [#ctxcancel]

Cancel a managed order by its stable local ID or original trade key:

```js
ctx.cancel({
  key: 'cancel-btc-entry-v1',
  order: entry.id,
})
```

The return value confirms that the command was queued:

```js
{
  key: "cancel-btc-entry-v1",
  order: "ord_...",
  status: "queued"
}
```

Cancellation keys are idempotent inside the job. If cancellation is requested before the venue returns its order ID, `mlabd` records `order.cancel_requested`; no venue cancellation is sent at that moment.

## onExecution [#onexecution]

Scripts may export a second hook for asynchronous order, fill, position, and account events:

```js
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:

```json
{
  "seq": 7,
  "jobId": "job_...",
  "tsMs": 1780000000000,
  "type": "order.fill",
  "orderId": "ord_...",
  "key": "btc-entry-v1",
  "symbol": "BTC",
  "venue": "bulkf",
  "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`

`tsMs` is the daemon receipt and journal time. Venue payloads retain their own timestamp inside `data`, which can differ from wall-clock time. Keep the two clocks separate: use the venue timestamp for exchange-event ordering and `tsMs` for local delivery latency and runtime diagnostics.

Each order, fill, position, and execution event identifies its symbol. Each fill is emitted as `order.fill`. A partially filled order remains active and may also emit a non-terminal `order.updated`; only `order.filled` marks the order fully filled.

`onExecution` uses the same persistent QuickJS session as `onData`, so module state remains available. It may call `ctx.trade`, `ctx.order`, 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.

Live jobs receive venue lifecycle events. Backtests generate simulated pending, accepted, fill, filled, and cancelled events, allowing the same order-replacement logic to run in both environments. Both expose current script positions through `input.positions.open`.

During a backtest, every traded symbol needs its own price-bearing source. Market Lab uses the first configured candle, orderbook, or other price-bearing source for that symbol as its deterministic reference. Prices for sources sharing a timestamp are loaded before hooks run, so BTC data cannot accidentally become the execution price for ZEC.

Historical OHLC bars cannot reveal the path between multiple limit prices touched in one bar. Market Lab applies previously submitted orders first and then stable local order ID order for orders submitted on the same event, keeping results deterministic without claiming an unknown intra-bar sequence.

## Job Operations [#job-operations]

List and inspect deployed jobs:

```bash
mlab script jobs
mlab script status <JOB_ID>
```

Tail structured worker output:

```bash
mlab script logs <JOB_ID> --follow
mlab script logs <JOB_ID> --follow --output jsonl
```

Terminal output is a compact lifecycle view: resting orders, fills, cancellations, rejections, position-size changes, closes, and errors. Routine pending records, repeated margin snapshots, and unchanged position snapshots are hidden. Use `--output jsonl` when the complete event payload is required.

Expected venue rejections are shown once with their native status, for example `rejectedCrossing`. A post-only script should wait for fresh orderbook data before retrying rather than immediately resubmitting the rejected or filled price.

Order state is monotonic. Once an order becomes `filled`, `cancelled`, or `rejected`, delayed account-stream or recovery records cannot move it back to a non-terminal state such as `resting`.

Live source connections are supervised. If a market-data WebSocket disconnects, the worker remains alive, cancels its non-terminal managed orders, and reconnects with exponential backoff from one to 30 seconds. Heartbeats and execution-event processing continue while the source is unavailable. After reconnecting, the script waits for new source data before `onData` runs again; trade-built candles discard the new partial startup bucket.

Terminal logs show source disconnects, successful reconnects, cleanup failures, and the final worker error. A transient source disconnect does not change the job to `failed`; only a terminal worker error does.

Stop or restart the immutable snapshot:

```bash
mlab script stop <JOB_ID>
mlab script restart <JOB_ID>
```

Stopping a script, reaching its configured duration, losing its live market-data connection, or failing its worker cancels every non-terminal order managed by that job. This removes resting quotes but does not close an existing position; position exit policy remains the script owner's decision.

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:

```txt
~/.market-lab/execution/jobs/<JOB_ID>/
```

## Failure Boundary [#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 delegated private key. Signing, nonce sequencing, account streaming, order correlation, and event journaling remain inside `mlabd`. BULK signed mutations use its persistent trading WebSocket; Hyperliquid signed actions use its HTTP exchange endpoint. Scripts do not create or authenticate either connection themselves.
