Appearance
Custom Indicators
MarketHeist lets you write your own technical indicators in Python. Custom indicators run in a secure WebAssembly (WASM) sandbox — no data is sent to any server.
The compute contract
Every custom indicator must define a single function with this exact signature:
python
def compute(df, params):
...
return {...}df — the input DataFrame
df is a pandas DataFrame with one row per bar (day/week/month). The columns available depend on the requires declaration (see below), but always include at minimum:
| Column | Type | Description |
|---|---|---|
open | float | Opening price |
high | float | High price |
low | float | Low price |
close | float | Closing price |
volume | float | Volume (may be 0 for indices) |
The index is a DatetimeIndex in ascending chronological order.
params — the parameter dict
params is a plain Python dict mapping parameter names to their current numeric values. You declare parameters and their defaults in the indicator metadata (see below). At runtime, params contains the values the user has entered in the UI.
python
# Example: params = {"window": 20, "threshold": 0.02}
window = params["window"] # int or float
threshold = params["threshold"]Return value
compute must return a dict of named pandas Series, each with len == len(df). The series names become the signals available for the position rule.
python
return {
"signal": my_series, # primary signal (required)
"upper_band": upper_series, # optional extra series
"lower_band": lower_series,
}All returned series must have the same index as df. It is fine for the first window - 1 values to be NaN — the backtest treats NaN values as "no signal" (flat/cash position).
INFO
You must return at least one series named "signal". This is the series the position rule will evaluate. Additional series are available for display in the chart overlay.
Indicator metadata
Above the compute function, declare your indicator's metadata as module-level variables:
python
NAME = "My Custom RSI"
DESCRIPTION = "RSI computed from close prices with configurable period."
REQUIRES = ["close"] # which OHLCV columns df will contain
PARAMS = [
{"name": "window", "label": "RSI Period", "default": 14, "min": 2, "max": 200, "step": 1},
{"name": "smooth", "label": "Smooth Period", "default": 3, "min": 1, "max": 20, "step": 1},
]REQUIRES can contain any subset of ["open", "high", "low", "close", "volume"]. Only the declared columns will be present in df.
Full example: Custom RSI
Here is a complete custom indicator reimplementing RSI from scratch:
python
NAME = "Custom RSI"
DESCRIPTION = "Relative Strength Index reimplemented from scratch."
REQUIRES = ["close"]
PARAMS = [
{"name": "window", "label": "Period", "default": 14, "min": 2, "max": 200, "step": 1},
]
def compute(df, params):
import pandas as pd
window = int(params["window"])
close = df["close"]
delta = close.diff()
gain = delta.clip(lower=0)
loss = (-delta).clip(lower=0)
avg_gain = gain.ewm(com=window - 1, min_periods=window).mean()
avg_loss = loss.ewm(com=window - 1, min_periods=window).mean()
rs = avg_gain / avg_loss.replace(0, float("inf"))
rsi = 100 - (100 / (1 + rs))
return {"signal": rsi}This indicator produces a signal series ranging 0–100. You can then apply a threshold position rule (e.g., "long when signal > 50") to turn it into a backtest.
Another example: Price / MA Ratio with band signals
python
NAME = "Price/MA with Bands"
DESCRIPTION = "Price relative to its moving average, with configurable deviation bands."
REQUIRES = ["close"]
PARAMS = [
{"name": "window", "label": "MA Period", "default": 50, "min": 5, "max": 500, "step": 1},
{"name": "band_pct", "label": "Band Width (%)", "default": 5.0, "min": 0.5,"max": 30, "step": 0.5},
]
def compute(df, params):
import pandas as pd
window = int(params["window"])
band_pct = float(params["band_pct"]) / 100.0
ma = df["close"].rolling(window).mean()
ratio = df["close"] / ma # primary signal
upper = pd.Series(1 + band_pct, index=df.index)
lower = pd.Series(1 - band_pct, index=df.index)
return {
"signal": ratio,
"upper_band": upper,
"lower_band": lower,
}WASM sandbox limitations
Custom indicators run inside a Pyodide (Python-in-WebAssembly) sandbox. This provides strong security and privacy guarantees but imposes some constraints:
| Allowed | Not allowed |
|---|---|
pandas, numpy (standard library) | Network requests (requests, urllib, socket) |
| Pure Python computation | File I/O (open(), pathlib) |
| Standard math operations | Subprocess / OS calls |
| NaN / infinite values in output | Importing arbitrary third-party packages |
The sandbox has no access to the internet and no access to your filesystem. Your indicator code is never sent to any server.
WARNING
Do not attempt to fetch external data inside compute. The network is blocked at the WASM level. All data your indicator needs must come from the df argument.
Warm-up periods and NaN handling
It is expected and correct to return NaN for the first window - 1 bars when your indicator needs a minimum number of observations. The backtest engine treats NaN signals as "flat" (no position). You do not need to fill or interpolate warm-up NaNs.
Next steps
- Position Rules — how signals become long/flat decisions
- Indicators — the full list of built-in indicators