Study Scripting
Write custom Market Lab studies in JavaScript.
Study Scripting
A study script measures market data and returns metrics.
Studies should not return buy or sell decisions. Decisions belong in strategies.
Run a Study Script
mlab study run ./studies/buy-pressure.js \
--provider mmt \
--exchange bybitf \
--symbol BTC/USDT \
--timeframe 60 \
--from 1704067200000 \
--to 1704067800000 \
--input min_vbuy=50000 \
--output jsonRuntime flags such as --provider, --exchange, --symbol, --timeframe, --from, --to, and --stream are owned by Market Lab.
Study-specific values are passed with repeated --input key=value flags.
Manifest
Every study script exports a study manifest:
export const study = {
name: "buy-pressure-filter",
version: "1",
source: "candles",
modes: ["window"],
inputs: {
min_vbuy: {
type: "number",
required: true,
description: "Ignore candles where buy volume is below this value"
},
min_delta: {
type: "number",
required: false,
default: 0
}
}
}Required manifest fields:
nameversionsourcemodes
Input types:
stringnumberboolean
Input names should use snake_case.
Input names cannot reuse Market Lab runtime flag names. Reserved names include:
providerexchangesymbolsourcetimeframefromtostreamdepthbucketoutputverbose
Data Hook
Study scripts export onData.
study.source determines which market payload Market Lab passes into input.
export function onData(ctx, input) {
const candles = input.candles
return {
metrics: {
points: candles.length,
latest_close: candles[candles.length - 1].c
}
}
}Window payload:
{
mode: "window",
candles: [...]
}Stream payload:
{
mode: "stream",
candle: {...}
}Use input.mode when one script supports both modes.
Context
The first hook argument is ctx.
ctx.inputs.min_vbuy
ctx.inputs.min_deltactx.inputs contains validated values from the script manifest and CLI --input flags.
Unknown inputs, missing required inputs, and invalid input types are rejected before the hook runs.
See Data Types for shared script types and Candles for candle field shapes.
Return Value
Return a JSON-serializable object with metrics.
return {
metrics: {
qualifying_candles: 8,
avg_delta: 120400.5
}
}meta is optional:
return {
metrics: {
qualifying_candles: 8
},
meta: {
note: "filtered by minimum buy volume"
}
}Do not return the full Market Lab envelope from your script. Market Lab wraps the result.
Full Example
export const study = {
name: "buy-pressure-filter",
version: "1",
source: "candles",
modes: ["window"],
inputs: {
min_vbuy: { type: "number", required: true },
min_delta: { type: "number", required: false, default: 0 }
}
}
export function onData(ctx, input) {
if (input.mode !== "window") {
return null
}
const filtered = input.candles.filter((c) => {
return c.vb >= ctx.inputs.min_vbuy && c.vb - c.vs >= ctx.inputs.min_delta
})
const avgDelta =
filtered.length === 0
? 0
: filtered.reduce((sum, c) => sum + (c.vb - c.vs), 0) / filtered.length
return {
metrics: {
qualifying_candles: filtered.length,
avg_delta: avgDelta,
latest_close: input.candles[input.candles.length - 1].c
}
}
}Run it:
mlab study run ./studies/buy-pressure.js \
--provider mmt \
--exchange bybitf \
--symbol BTC/USDT \
--timeframe 60 \
--from 1704067200000 \
--to 1704067800000 \
--input min_vbuy=50000 \
--input min_delta=0 \
--output jsonOutput
Market Lab wraps the script result in the standard study envelope.
{
"type": "study.buy-pressure-filter.result",
"version": "1",
"provider": "mmt",
"exchange": "bybitf",
"symbol": "BTC/USDT",
"ts_ms": 1704067800000,
"stream": false,
"study": {
"name": "buy-pressure-filter",
"kind": "window",
"source": "custom"
},
"inputs": {
"min_vbuy": 50000,
"min_delta": 0
},
"metrics": {
"qualifying_candles": 4,
"avg_delta": 120500.25,
"latest_close": 42100
}
}Validation
Market Lab validates:
- the script can be loaded
studyis exported- manifest fields are valid
onData(ctx, input)exists- declared inputs are known and typed
- the hook returns JSON-serializable
metrics