# Python API Reference (/scripting-v2/api-reference)



This page is the compact contract for a Python V2 script. The other Scripting V2 pages explain how and when to use each part.

## Complete example [#complete-example]

```python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

CANDLES = "btc@candles@hyperliquidf@mmt:timeframe=60"
SOURCE = "btc@candles@hyperliquidf@mmt"

script = {
    "name": "python-sma-crossover",
    "version": "2",
    "description": "Open and close a long position on SMA crossovers",
    "lookback": 100,
    "params": {
        "fast_period": {"type": "number", "default": 5},
        "slow_period": {"type": "number", "default": 20},
        "margin": {"type": "number", "default": 100},
    },
}


def on_data(ctx, history):
    if ctx.source != SOURCE:
        return

    candles = history.source(CANDLES)
    slow_period = int(ctx.params["slow_period"])
    if len(candles) < slow_period + 1:
        return

    closes = pd.Series([candle["c"] for candle in candles], dtype=float)
    fast = closes.rolling(int(ctx.params["fast_period"])).mean()
    slow = closes.rolling(slow_period).mean()

    previous = np.sign(fast.iloc[-2] - slow.iloc[-2])
    current = np.sign(fast.iloc[-1] - slow.iloc[-1])
    position = next(
        (item for item in ctx.positions().open if item["symbol"] == "BTC"),
        None,
    )
    if previous <= 0 and current > 0 and position is None:
        ctx.trade(
            {
                "exchange": "hyperliquidf",
                "symbol": "BTC",
                "position": "open-long",
                "margin": ctx.params["margin"],
                "order": {"type": "market"},
            }
        )
    elif previous >= 0 and current < 0 and position is not None:
        ctx.trade(
            {
                "exchange": "hyperliquidf",
                "symbol": "BTC",
                "position": "close-long",
                "order": {"type": "market"},
            }
        )

    return {
        "metrics": {
            "fast_sma": fast.iloc[-1],
            "slow_sma": slow.iloc[-1],
        }
    }


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


def on_finish(ctx, history):
    frame = pd.DataFrame(history.source(CANDLES))
    if frame.empty:
        return

    frame["fast_sma"] = frame["c"].rolling(
        int(ctx.params["fast_period"])
    ).mean()
    frame["slow_sma"] = frame["c"].rolling(
        int(ctx.params["slow_period"])
    ).mean()

    axis = frame.plot(x="t", y=["c", "fast_sma", "slow_sma"])
    chart = ctx.artifact_path("sma-crossover.png")
    axis.figure.savefig(chart, dpi=160, bbox_inches="tight")
    plt.close(axis.figure)

    return {"meta": {"chart": chart}}
```

The `history.source(CANDLES)` call declares the source. Run it without repeating that selector:

```bash
mlab script backtest sma-crossover.py \
  --from 2026-07-15 \
  --to 2026-07-16
```

## Manifest [#manifest]

```python
script: dict
```

| Field         | Type   | Required | Rules                                                           |
| ------------- | ------ | -------- | --------------------------------------------------------------- |
| `name`        | `str`  | Yes      | Non-empty script name.                                          |
| `version`     | `str`  | Yes      | Must be `"2"`.                                                  |
| `description` | `str`  | No       | Metadata only.                                                  |
| `lookback`    | `int`  | No       | Per-selector retention; effective range 2–5,000, default 5,000. |
| `params`      | `dict` | No       | Flat parameter definitions.                                     |

Parameter definitions:

```python
"name": {
    "type": "number" | "boolean" | "string",
    "required": True | False,
    "default": value,
}
```

`required` defaults to `False`. A required parameter cannot also define a default.

## Required hook [#required-hook]

```python
def on_data(ctx, history):
    ...
```

Called once for every accepted source event. The triggering record is added to history before invocation.

## Optional hooks [#optional-hooks]

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


def on_finish(ctx, history):
    ...
```

`on_execution` reads the current live or simulated execution lifecycle update through `ctx.execution`. `on_finish` performs final calculations and artifacts but cannot submit execution commands.

## Context [#context]

### Source and runtime values [#source-and-runtime-values]

During `on_data`, the context exposes:

| Value                                | Meaning                                                       |
| ------------------------------------ | ------------------------------------------------------------- |
| `ctx.source`                         | Exact normalized selector that triggered the hook.            |
| `ctx.source_type`                    | Base kind such as `candles`, `orderbook`, or `trades`.        |
| `ctx.provider`                       | Standalone provider or `mmt`.                                 |
| `ctx.exchange`                       | Normalized source exchange.                                   |
| `ctx.symbol`                         | Normalized source symbol.                                     |
| `ctx.source_configs`                 | Parsed settings for configured selectors.                     |
| `ctx.positions(account="main").open` | Open positions for the main or a named execution account.     |
| `ctx.pnl(...)`                       | Current run's PnL history. Also available during `on_finish`. |
| `ctx.study`                          | Native study helpers shared with Scripting V1.                |

During `on_execution`, the current lifecycle payload is available as `ctx.execution`.

### `ctx.params` [#ctxparams]

```python
ctx.params: dict
```

Contains validated manifest defaults and runtime `--param` overrides.

### `ctx.pnl(index=None)` [#ctxpnlindexnone]

Read the current run's PnL history:

```python
points = ctx.pnl()
latest = ctx.pnl(0)
previous = ctx.pnl(1)
missing = ctx.pnl(99)
```

Without an index, `ctx.pnl()` returns points in chronological order:

```python
[
    {"t": 1786992000000, "pnl": 0.0},
    {"t": 1786992060000, "pnl": -2.14},
    {"t": 1786992120000, "pnl": 3.56},
]
```

With an index, `0` is the latest point, `1` is the previous point, and an unavailable index returns `None`. Each point contains a millisecond timestamp in `t` and the PnL value in `pnl`.

`ctx.pnl(...)` is available during `on_data` and `on_finish`. For example:

```python
def on_data(ctx, history):
    latest = ctx.pnl(0)
    if latest is not None and latest["pnl"] <= -100:
        return
```

### `ctx.study` [#ctxstudy]

Python V2 exposes the same native study helpers as Scripting V1:

```python
spread = ctx.study.spread(orderbook)
sma = ctx.study.sma(candles, {"field": "c", "window": 20})
```

Available helpers are `sma`, `ema`, `cvd`, `spread`, `depth`, `imbalance`, `slippage`, and `vamp`. They accept normal Python dictionaries and lists and return dictionaries or lists.

See [Built-In Functions](/scripting-v2/built-ins) for every input and return shape.

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

```python
reference = ctx.trade(request: dict)
```

Submits an intentional perpetual position transition. The request must include an `exchange`; `account` defaults to `main`. Returns:

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

See [Execution: ctx.trade](/scripting-v2/execution#ctxtraderequest) for the request fields.

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

```python
reference = ctx.order(request: dict)
```

Submits a raw buy or sell. The request must include an `exchange`; `account` defaults to `main`. Returns the same local order reference. See [Execution: ctx.order](/scripting-v2/execution#ctxorderrequest).

### `ctx.positions(account="main")` [#ctxpositionsaccountmain]

Read open positions for one execution account:

```python
main = ctx.positions().open
trading_2 = ctx.positions("trading-2").open
```

`ctx.positions("main")` is equivalent to `ctx.positions()`. Named accounts are isolated from one another in both live execution and backtests.

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

```python
result = ctx.cancel(
    {
        "order": "ord_...",
    }
)
```

Queues cancellation of a managed order and returns its local cancellation status. Do not pass `exchange`; cancellation uses the exchange stored on the referenced managed order.

### `ctx.artifact_path(name)` [#ctxartifact_pathname]

```python
path = ctx.artifact_path("charts/performance.png")
```

Returns an absolute string path inside the current run's owner-only artifact directory. The name must be relative and cannot escape that directory.

## History [#history]

### Retained list [#retained-list]

```python
records = history.source(selector: str)
```

Returns copies ordered oldest to newest. Returns `[]` when the selector has no data.

### Indexed record [#indexed-record]

```python
record = history.source(selector: str, index: int)
```

Index `0` is newest, `1` is previous, and so on. Returns `None` when unavailable.

The selector must be a non-empty exact source identity. An index must be a non-negative integer and cannot be a boolean.

See [History](/scripting-v2/history), [Sources](/scripting-v2/sources), and [Data Types](/scripting-v2/data-types).

## Data hook context [#data-hook-context]

Use context values to identify the source that triggered `on_data`:

| Field                                | Meaning                                                |
| ------------------------------------ | ------------------------------------------------------ |
| `ctx.source`                         | Exact normalized selector that triggered the hook.     |
| `ctx.source_type`                    | Base kind such as `candles`, `orderbook`, or `trades`. |
| `ctx.provider`                       | Standalone provider or `mmt`.                          |
| `ctx.exchange`                       | Normalized exchange name.                              |
| `ctx.symbol`                         | Normalized symbol.                                     |
| `ctx.source_configs`                 | Parsed selector settings such as timeframe or depth.   |
| `ctx.positions(account="main").open` | Open positions for the selected execution account.     |

The market record itself is read through `history.source(ctx.source, 0)`.

## Execution hook context [#execution-hook-context]

`ctx.execution` contains the current execution lifecycle payload. Typical fields include:

| Field          | Meaning                                              |
| -------------- | ---------------------------------------------------- |
| `seq`          | Monotonic event sequence within the job.             |
| `jobId`        | Script job identifier.                               |
| `tsMs`         | Local daemon receipt and journal time.               |
| `type`         | Lifecycle type such as `order.fill`.                 |
| `orderId`      | Stable local order ID, when applicable.              |
| `symbol`       | Order or position symbol.                            |
| `venue`        | Execution venue.                                     |
| `venueOrderId` | Venue-assigned order ID, when available.             |
| `status`       | Current managed-order status.                        |
| `terminal`     | Whether this event ends the managed order lifecycle. |
| `data`         | Venue or event-specific payload.                     |

## Hook output [#hook-output]

All hooks accept these return forms:

```python
None
{}
{"metrics": {...}}
{"meta": {...}}
{"metrics": {...}, "meta": {...}}
```

`metrics` and `meta` must be dictionaries. Trading signals in returned output are not interpreted as orders.

The runner can serialize normal JSON values plus `pathlib.Path`, NumPy scalar values, NumPy arrays, and other values implementing a compatible `tolist()` method.

## Interpreter resolution [#interpreter-resolution]

For a `.py` script, Market Lab resolves:

1. `--python <path-or-command>`;
2. an adjacent `.venv/bin/python`;
3. an adjacent `.venv/Scripts/python.exe`;
4. `python3`, then `python`, from `PATH`.

Python 3.9 or newer is required. `--python` is rejected for JavaScript files.

## No Python-only mode object [#no-python-only-mode-object]

Python V2 deliberately has no `ctx.mode`. `script backtest` and `script run` feed the same sequential hook contract. The command owns historical or live data loading, while each `ctx.trade(...)` or `ctx.order(...)` request selects its own execution exchange.

Python V2 does not accept `--venue`. BULK and Hyperliquid use mainnet unless the job is started with `--testnet`; that single flag applies to supported sources and execution requests in the job. HyperLink execution is mainnet-only.
