--- description: "API and patterns for writing custom Python indicator scripts that compute values from OHLCV data and plot live on the chart." --- # 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`](../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) ```python 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() ``` ```python 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: ```python 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`: ```python "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_"`. Include `custom_metadata` in the patch value so the web client can render it. 4. **Use in strategies** via `ta.custom_(...)`. See [`../strategies/strategy-development.md`](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. ```python # 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: ```python 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 - [`../pandas-ta-reference.md`](../pandas-ta-reference.md) — Full catalog of built-in indicators and calling conventions - [`../api-reference.md`](../api-reference.md) — DataAPI and ChartingAPI for research scripts - [`../strategies/strategy-development.md`](strategy-development.md) — Using custom indicators in strategies via `ta.custom_*`