Market LabDocs
Scripting V2 - PythonPlain Python Files

Python API Reference

Complete reference for the Python V2 manifest, hooks, context, history, and outputs.

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

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:

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

Manifest

script: dict
FieldTypeRequiredRules
namestrYesNon-empty script name.
versionstrYesMust be "2".
descriptionstrNoMetadata only.
lookbackintNoPer-selector retention; effective range 2–5,000, default 5,000.
paramsdictNoFlat parameter definitions.

Parameter definitions:

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

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

Required hook

def on_data(ctx, history):
    ...

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

Optional hooks

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

Source and runtime values

During on_data, the context exposes:

ValueMeaning
ctx.sourceExact normalized selector that triggered the hook.
ctx.source_typeBase kind such as candles, orderbook, or trades.
ctx.providerStandalone provider or mmt.
ctx.exchangeNormalized source exchange.
ctx.symbolNormalized source symbol.
ctx.source_configsParsed settings for configured selectors.
ctx.positions(account="main").openOpen positions for the main or a named execution account.
ctx.pnl(...)Current run's PnL history. Also available during on_finish.
ctx.studyNative study helpers shared with Scripting V1.

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

ctx.params

ctx.params: dict

Contains validated manifest defaults and runtime --param overrides.

ctx.pnl(index=None)

Read the current run's PnL history:

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:

[
    {"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:

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

ctx.study

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

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 for every input and return shape.

ctx.trade(request)

reference = ctx.trade(request: dict)

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

{"id": "ord_..."}

See Execution: ctx.trade for the request fields.

ctx.order(request)

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.

ctx.positions(account="main")

Read open positions for one execution account:

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)

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)

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

Retained list

records = history.source(selector: str)

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

Indexed record

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, Sources, and Data Types.

Data hook context

Use context values to identify the source that triggered on_data:

FieldMeaning
ctx.sourceExact normalized selector that triggered the hook.
ctx.source_typeBase kind such as candles, orderbook, or trades.
ctx.providerStandalone provider or mmt.
ctx.exchangeNormalized exchange name.
ctx.symbolNormalized symbol.
ctx.source_configsParsed selector settings such as timeframe or depth.
ctx.positions(account="main").openOpen positions for the selected execution account.

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

Execution hook context

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

FieldMeaning
seqMonotonic event sequence within the job.
jobIdScript job identifier.
tsMsLocal daemon receipt and journal time.
typeLifecycle type such as order.fill.
orderIdStable local order ID, when applicable.
symbolOrder or position symbol.
venueExecution venue.
venueOrderIdVenue-assigned order ID, when available.
statusCurrent managed-order status.
terminalWhether this event ends the managed order lifecycle.
dataVenue or event-specific payload.

Hook output

All hooks accept these return forms:

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

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

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.

On this page