Files
ai/gateway/knowledge/indicators/indicator-development.md

5.8 KiB

Custom Indicator Development

Custom indicators are Python scripts saved in the indicator category. They compute values from OHLCV data and are plotted live on the TradingView chart alongside built-in indicators.

See ../pandas-ta-reference.md for the full catalog of built-in indicators available via pandas_ta.


Function Signature

A custom indicator must define a top-level function whose name is the lowercase, snake_case form of the name passed to PythonWrite. For example, name="VW RSI" → function def vw_rsi(...).

The function receives the OHLCV columns listed in input_series as positional arguments and must return either:

  • A pd.Series (single-output indicator), or
  • A pd.DataFrame with column names matching output_columns in the metadata (multi-output)
import pandas as pd
import pandas_ta as ta

# Single-output: volume-weighted RSI
def vw_rsi(close: pd.Series, volume: pd.Series, length: int = 14) -> pd.Series:
    rsi = ta.rsi(close, length=length)
    vol_weight = volume / volume.rolling(length).mean()
    return (rsi * vol_weight).rolling(3).mean()
import pandas as pd
import pandas_ta as ta

# Multi-output: custom Bollinger Bands
def vol_bands(close: pd.Series, length: int = 20, std: float = 2.0) -> pd.DataFrame:
    bb = ta.bbands(close, length=length, std=std)
    return pd.DataFrame({
        "upper": bb.iloc[:, 2],
        "mid":   bb.iloc[:, 1],
        "lower": bb.iloc[:, 0],
    })

Always use pandas_ta for standard indicator calculations. Never write manual rolling().mean() or ewm() implementations — use ta.sma(), ta.ema(), ta.rsi(), etc.


Required Metadata

When writing a custom indicator with PythonWrite, supply complete metadata so the web client can build the TradingView plotter automatically:

PythonWrite(
    category="indicator",
    name="VW RSI",
    description="RSI weighted by relative volume.",
    details="""## Volume-Weighted RSI

Computes RSI on close prices, scales by relative volume, applies 3-bar smoothing.

**Formula:** (rsi * (volume / volume.rolling(length).mean())).rolling(3).mean()
**Inputs:** close, volume
**Output:** single Series — smoothed volume-weighted RSI (separate pane)
**Parameters:** length (int, default 14)""",
    code="""...""",
    metadata={
        "parameters": {
            "length": {"type": "int", "default": 14, "min": 2, "max": 200, "description": "RSI period"}
        },
        "input_series": ["close", "volume"],
        "output_columns": [
            {"name": "value", "display_name": "VW-RSI", "plot": {"style": 0}}
        ],
        "pane": "separate"   # "price" = overlay on candles; "separate" = sub-pane
    }
)

Plot styles

Value Renders as
0 Line (default)
1 Histogram bars
4 Area (filled under line)
5 Columns (vertical bars)
9 Step line

Filled areas (shaded bands)

To shade between two output series (e.g. upper/lower bands), add a filled_areas list. The two bounding series must appear at consecutive even/odd positions in output_columns:

"filled_areas": [
    {"id": "fill", "type": "plot_plot", "series1": "upper", "series2": "lower",
     "color": "#2196F3", "opacity": 0.08}
]

Workflow

  1. Check for existing indicators before writing: PythonList(category="indicator"). If one already exists with the same sanitized name, update it with PythonEdit rather than creating a duplicate.

  2. Write with PythonWrite(category="indicator", ...). The system automatically runs the script against synthetic test data to catch compile/runtime errors — no separate validation call needed.

  3. Add to workspace with WorkspacePatch("indicators", ...) using pandas_ta_name: "custom_<sanitized_name>". Include custom_metadata in the patch value so the web client can render it.

  4. Use in strategies via ta.custom_<sanitized_name>(...). See ../strategies/strategy-development.md for details.


Naming Conventions

The workspace pandas_ta_name is "custom_" + the sanitized indicator name. Sanitization: lowercase + spaces/hyphens → underscores. For example:

name function name pandas_ta_name
"VW RSI" vw_rsi custom_vw_rsi
"TrendFlex" trendflex custom_trendflex
"Vol-Bands" vol_bands custom_vol_bands

Two names that sanitize to the same value will conflict — check with PythonList first.


Common Pitfalls

Look-ahead bias

Never use future data in the computation. Indicator values for bar N may only depend on data available at bar N or earlier.

# WRONG — uses future price
signal = close.shift(-1) > close

# CORRECT — only past data
signal = close > close.shift(1)

Repainting

Indicator values for already-closed bars should not change as new bars arrive. Avoid calculations that recalculate over a sliding window that can retrospectively alter past values in non-obvious ways.

NaN handling

Indicators need a warm-up period. The first length - 1 values will be NaN. Strategies that consume custom indicators should guard with:

if vw_rsi.isna().all() or len(df) < min_required:
    return

Overfitting

  • Keep indicator logic simple and parameter-lean
  • Validate on out-of-sample data, not the same window used to tune parameters
  • Prefer indicators with a clear mechanical rationale over curve-fit formulas

See Also