# 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_<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`](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_*`
