# Execution (/scripting-v2/execution)



Python V2 calls Market Lab's existing execution API with ordinary dictionaries:

```text
on_data / on_execution -> ctx.trade / ctx.order / ctx.cancel -> simulator or mlabd
```

Live jobs connect their BULK, Hyperliquid, and HyperLink execution WebSockets before market-data processing begins.

Each trade or order names its execution exchange. The script does not inspect `ctx.mode` or branch between separate backtest and live APIs.

| Command           | What the request's `exchange` selects                                |
| ----------------- | -------------------------------------------------------------------- |
| `script backtest` | The historical simulator and matching exchange-specific price source |
| `script run`      | The live execution exchange                                          |

Python V2 rejects `--venue`. Use one of the supported execution names in every `ctx.trade(...)` or `ctx.order(...)` request:

* `bulkf`
* `hyperliquidf`
* `hyperlinkf`
* `hyperliquid`
* `hyperlink`

Core and HIP-3 perpetual execution both use `hyperliquidf`. A HIP-3 request puts the DEX in its symbol, such as `xyz:TSLA`. BULK and Hyperliquid use mainnet by default. The job-wide `--testnet` flag moves supported sources and execution requests to testnet. HyperLink uses `hyperlink` for Spot and `hyperlinkf` for core or HIP-3 perpetuals. Both are mainnet-only and cannot be used in a `--testnet` job.

Market data and execution are independent. A script may read a Binance source and execute on Hyperliquid, or read one symbol as a signal and trade another declared symbol. One Python job may also execute on multiple exchanges.

HyperLink execution uses ordinary Hyperliquid public market data. Declare the matching `hyperliquidf` source for a core or HIP-3 perpetual, then select `hyperlinkf` in the request:

```python
price = history.source("btc@trades@hyperliquidf", 0)

ctx.trade(
    {
        "exchange": "hyperlinkf",
        "symbol": "BTC",
        "position": "open-long",
        "margin": 100,
        "leverage": 5,
    }
)
```

HIP-3 keeps the same DEX-scoped symbol on both sides:

```python
history.source("xyz:tsla@candles@hyperliquidf:timeframe=60")

ctx.trade(
    {
        "exchange": "hyperlinkf",
        "symbol": "xyz:TSLA",
        "position": "open-long",
        "margin": 100,
        "leverage": 5,
    }
)
```

## `ctx.trade(request)` [#ctxtraderequest]

Use `ctx.trade` for an intentional perpetual position transition:

```python
order = ctx.trade(
    {
        "exchange": "hyperliquidf",
        "symbol": "BTC",
        "position": "open-long",
        "margin": 100,
        "leverage": 5,
        "order": {"type": "limit", "price": 65000, "tif": "alo"},
        "sl": 63000,
        "tp": 69000,
    }
)
```

The call validates synchronously and returns a stable local reference:

```python
{"id": "ord_..."}
```

It means the command was accepted by the local execution layer. It does not mean the exchange filled the order. Observe `on_execution` for the actual lifecycle.

HIP-3 uses the same request shape with a scoped symbol:

```python
history.source("xyz:tsla@candles@hyperliquidf:timeframe=60")

ctx.trade(
    {
        "exchange": "hyperliquidf",
        "symbol": "xyz:TSLA",
        "position": "open-long",
        "margin": 100,
        "leverage": 5,
    }
)
```

| Field              | Required     | Contract                                                              |
| ------------------ | ------------ | --------------------------------------------------------------------- |
| `exchange`         | Yes          | Execution exchange for this request.                                  |
| `account`          | No           | `main` by default, or a configured named subaccount.                  |
| `symbol`           | Yes          | A symbol declared by one of the job's sources.                        |
| `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`         | No           | At least `1`; defaults to `1`. Invalid on close.                      |
| `order.type`       | No           | `market` or `limit`; defaults to `market`.                            |
| `order.price`      | Limit only   | Positive, tick-aligned limit price.                                   |
| `order.tif`        | Limit only   | `gtc`, `ioc`, or `alo`; defaults to `gtc`.                            |
| `sl`               | Open only    | Native stop-loss trigger price.                                       |
| `tp`               | Open only    | Native take-profit trigger price.                                     |
| `max_slippage`     | Market only  | Maximum slippage as a decimal fraction, such as `0.0005` for 5 bps.   |
| `max_slippage_bps` | Market only  | Maximum slippage in basis points. Do not combine with `max_slippage`. |

An opening request needs exactly one of `size` or `margin`. A closing request is reduce-only automatically. Omit its size to close the full matching position:

```python
ctx.trade(
    {
        "exchange": "hyperliquidf",
        "symbol": "BTC",
        "position": "close-long",
    }
)
```

Market Lab uses one-way position semantics. Close an opposite position before opening a new one; an `open-long` request does not silently reverse an existing short.

## Named subaccounts [#named-subaccounts]

Set `account` on each trade or order that should use a named subaccount:

```python
ctx.trade(
    {
        "exchange": "hyperliquidf",
        "account": "trading-2",
        "symbol": "BTC",
        "position": "open-long",
        "margin": 100,
        "leverage": 5,
    }
)
```

Read positions independently for each account:

```python
main_positions = ctx.positions().open
same_main_positions = ctx.positions("main").open
subaccount_positions = ctx.positions("trading-2").open
```

Create names with `mlab auth set <venue> --subaccount <name>` before live execution. Named accounts are supported for BULK and Hyperliquid only; HyperLink requests must use `main`. Omitting `account` always selects `main`.

## `ctx.order(request)` [#ctxorderrequest]

Use `ctx.order` for a raw buy or sell:

```python
ask = ctx.order(
    {
        "exchange": "hyperliquidf",
        "symbol": "BTC",
        "side": "sell",
        "size": 0.01,
        "leverage": 5,
        "reduceOnly": True,
        "order": {"type": "limit", "price": 66000, "tif": "alo"},
    }
)
```

| Field              | Required    | Contract                                                              |
| ------------------ | ----------- | --------------------------------------------------------------------- |
| `exchange`         | Yes         | Execution exchange for this request.                                  |
| `account`          | No          | `main` by default, or a configured named subaccount.                  |
| `symbol`           | Yes         | A symbol declared by one of the job's sources.                        |
| `side`             | Yes         | `buy` or `sell`; `long` and `short` are accepted aliases.             |
| `size`             | One of      | Positive base quantity.                                               |
| `margin`           | One of      | Positive quote collateral used to derive size.                        |
| `leverage`         | No          | Perpetuals only; at least `1`, default `1`.                           |
| `reduceOnly`       | No          | Perpetuals only; prevents increasing or flipping inventory.           |
| `order.type`       | No          | `market` or `limit`; defaults to `market`.                            |
| `order.price`      | Limit only  | Positive, tick-aligned limit price.                                   |
| `order.tif`        | Limit only  | `gtc`, `ioc`, or `alo`; defaults to `gtc`.                            |
| `max_slippage`     | Market only | Maximum slippage as a decimal fraction.                               |
| `max_slippage_bps` | Market only | Maximum slippage in basis points. Do not combine with `max_slippage`. |

Raw perpetual orders follow venue netting. A non-reduce-only sell can reduce a long, close it, or cross through flat into a short. Raw orders do not accept `sl` or `tp` because their eventual position effect depends on inventory when they fill.

## Market-order slippage [#market-order-slippage]

`ctx.trade` and `ctx.order` accept one slippage unit for market orders:

```python
ctx.trade(
    {
        "exchange": "bulkf",
        "symbol": "BTC",
        "position": "open-long",
        "margin": 100,
        "leverage": 5,
        "max_slippage_bps": 5,
    }
)
```

`max_slippage_bps: 5` and `max_slippage: 0.0005` both mean 5 bps. Market Lab converts bounded market orders into IOC limits at the worst accepted price on BULK and Hyperliquid. Supplying both fields, or adding either field to a limit order, is rejected.

For Hyperliquid or HyperLink Spot, use the exact market pair and omit leverage and reduce-only. The HyperLink route is:

```python
ctx.order(
    {
        "exchange": "hyperlink",
        "symbol": "HYPE/USDC",
        "side": "buy",
        "margin": 100,
        "order": {"type": "limit", "price": 45, "tif": "alo"},
    }
)
```

A spot buy's `margin` is its quote-asset budget. A spot sell normally uses base-asset `size`. Replace `hyperlink` with `hyperliquid` for direct Hyperliquid execution.

HyperLink perpetual leverage comes from authenticated asset metadata and is cached in `mlabd` memory by account and asset. It is not persisted and is fetched again on demand after a daemon restart.

Hyperliquid outcome markets are also spot-like. Use an exact outcome-side symbol such as `1009:0`, and use `ctx.order` to buy tokens or sell tokens already held:

```python
ctx.order(
    {
        "exchange": "hyperliquid",
        "symbol": "1009:0",
        "side": "buy",
        "margin": 100,
        "order": {"type": "limit", "price": 0.42, "tif": "alo"},
    }
)
```

Outcome orders do not support naked shorting, leverage, reduce-only, attached SL/TP, or perpetual position-close operations. See [Hyperliquid outcomes](/providers-execution/hyperliquid#outcome-markets) for the venue contract.

## `ctx.cancel(request)` [#ctxcancelrequest]

Cancel a managed order with its stable local ID:

```python
ctx.cancel(
    {
        "order": order["id"],
    }
)
```

The return value confirms that cancellation was queued; the later execution event confirms its venue result.

Do not add `exchange` to a cancellation request. Market Lab reads the exchange from the managed order referenced by `order`.

## Multiple execution exchanges [#multiple-execution-exchanges]

Because routing belongs to each order, a single Python V2 job can execute both legs of an arbitrage without a job-wide venue:

```python
bulk_bid = ctx.order(
    {
        "exchange": "bulkf",
        "symbol": "BTC",
        "side": "buy",
        "size": 0.01,
        "order": {"type": "limit", "price": bulk_price, "tif": "alo"},
    }
)

hyperliquid_ask = ctx.order(
    {
        "exchange": "hyperliquidf",
        "symbol": "BTC",
        "side": "sell",
        "size": 0.01,
        "order": {"type": "limit", "price": hl_price, "tif": "alo"},
    }
)
```

This makes routing deterministic: the data selector identifies a data stream, while `exchange` identifies the execution destination. Market Lab does not infer execution from the most recent event or source.

## `on_execution(ctx)` [#on_executionctx]

Use the optional execution hook to react to fills, cancellations, and rejections:

```python
def on_execution(ctx):
    print(ctx.execution)
```

`ctx.execution` contains the current lifecycle update:

```json
{
  "seq": 7,
  "jobId": "script_...",
  "tsMs": 1780000000000,
  "type": "order.fill",
  "orderId": "ord_...",
  "symbol": "BTC",
  "venue": "hyperliquidf",
  "venueOrderId": "...",
  "status": "filled",
  "terminal": false,
  "data": {}
}
```

Common event types include:

* `order.pending`, `order.accepted`, `order.updated`, and `order.terminal`
* `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's journal time. A venue timestamp, when supplied, remains inside `data`.

Execution events are journaled and acknowledged only after the hook succeeds. Backtests generate equivalent simulated lifecycle events, so fill-driven logic can use the same hook in both environments.

## Backtest execution prices [#backtest-execution-prices]

Every traded exchange and symbol pair needs its own price-bearing source. Market Lab uses the matching exchange-specific candle, order book, or other price-bearing stream as its execution reference. A BTC source from BULK cannot price a BTC order routed to Hyperliquid, and BTC data is never used as the fill price for a ZEC order.

OHLC candles do not reveal the order in which multiple prices were touched inside a bar. The simulator processes existing orders first and uses stable local order-ID order for orders submitted on the same event. This keeps the result deterministic without pretending to know the missing intrabar path.
