- Flink update_bars debouncing - update_bars subscription idempotency bugfix - Price decimal correction bugfix of previous commit - Add GLM-5.1 model tag alongside renamed GLM-5 - Use short Anthropic model IDs (sonnet/haiku/opus) instead of full version strings - Allow @tags anywhere in message content, not just at start - Return hasOtherContent flag instead of trimmed rest string - Only trigger greeting stream when tag has no other content - Update workspace knowledge base references to platform/workspace and platform/shapes - Hierarchical knowledge base catalog - 151 Trading Strategies knowledge base articles - Shapes knowledge base article - MutateShapes tool instead of workspace patch
5.9 KiB
description
| 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 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.DataFramewith column names matchingoutput_columnsin 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
-
Check for existing indicators before writing:
PythonList(category="indicator"). If one already exists with the same sanitized name, update it withPythonEditrather than creating a duplicate. -
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. -
Add to workspace with
WorkspacePatch("indicators", ...)usingpandas_ta_name: "custom_<sanitized_name>". Includecustom_metadatain the patch value so the web client can render it. -
Use in strategies via
ta.custom_<sanitized_name>(...). Seestrategy-developmentfor 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
pandas-ta-reference— Full catalog of built-in indicators and calling conventionsapi-reference— DataAPI and ChartingAPI for research scriptsstrategy-development— Using custom indicators in strategies viata.custom_*