custom-indicator
Builds fast custom trading indicators using math and charting so traders can test new strategies.
Installation
Paste this into Claude Code, Cursor, or any agent that can run commands.
SKILL.mdShow the author's original SKILL.md
---
name: custom-indicator
description: Create a custom technical indicator using vectorized NumPy on top of openalgo's Rust-core ta primitives. Generates production-grade, O(n) indicator functions with charting and benchmarking.
argument-hint: "[indicator-name]"
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
---
Create a custom technical indicator by composing openalgo's Rust-core `ta` primitives with vectorized NumPy.
## Arguments
- `$0` = indicator name (e.g., zscore, squeeze, vwap-bands, custom-rsi, mean-reversion). Required.
If no arguments, ask the user what indicator they want to build.
## Instructions
1. Read the indicator-expert rules, especially:
- `rules/custom-indicators.md` — NumPy + ta-primitive patterns and templates
- `rules/performance.md` — Rust core performance, O(n) guarantees, benchmarking
- `rules/indicator-catalog.md` — Check if indicator already exists in openalgo.ta
2. **Check first**: If the indicator already exists in `openalgo.ta` (100+ indicators), tell the user and show the existing API
3. Create `custom_indicators/{indicator_name}/` directory (on-demand)
4. Create `{indicator_name}.py` with:
### File Structure
```python
"""
{Indicator Name} — Custom Indicator
Description: {what it measures}
Category: {trend/momentum/volatility/volume/oscillator}
"""
import numpy as np
import pandas as pd
from openalgo import ta
# --- Core Computation (vectorized NumPy on ta primitives) ---
def _compute_{name}(arr: np.ndarray, period: int) -> np.ndarray:
"""Vectorized core computation built on Rust-core primitives."""
n = len(arr)
result = np.full(n, np.nan)
# Compose from ta primitives (they run in Rust):
# mean = ta.sma(arr, period); std = ta.stdev(arr, period); ...
# then combine with NumPy array math (np.where, masks)
return result
# --- Public API ---
def {name}(data, period=20):
"""
{Indicator Name}
Args:
data: Close prices (numpy array, pandas Series, or list)
period: Lookback period (default: 20)
Returns:
Same type as input with indicator values
"""
if isinstance(data, pd.Series):
idx = data.index
result = _compute_{name}(data.values.astype(np.float64), period)
return pd.Series(result, index=idx, name="{Name}({period})")
arr = np.asarray(data, dtype=np.float64)
return _compute_{name}(arr, period)
```
5. Create `chart.py` for visualization:
```python
"""Chart the custom indicator with Plotly."""
import os
from pathlib import Path
from datetime import datetime, timedelta
from dotenv import find_dotenv, load_dotenv
from openalgo import api, ta
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from {indicator_name} import {name}
# ... fetch data, compute indicator, create chart ...
```
6. Create `benchmark.py` for performance testing (no warmup needed — the Rust core runs at full speed from the first call):
```python
"""Benchmark the custom indicator."""
import numpy as np
import time
from {indicator_name} import {name}
for size in [10_000, 100_000, 500_000]:
data = np.random.randn(size).cumsum() + 1000
t0 = time.perf_counter()
_ = {name}(data, 20)
elapsed = (time.perf_counter() - t0) * 1000
print(f"{size:>10,} bars: {elapsed:>8.2f}ms")
```
## NumPy Rules (CRITICAL)
### MUST DO
- Compose from `ta` primitives wherever possible — they run in the Rust core
- `np.full(n, np.nan)` to initialize output arrays
- Vectorize with array expressions, `np.where`, and boolean masks
- Guard divisions: `np.errstate(invalid="ignore", divide="ignore")` plus a safe denominator mask
- Respect NaN warm-up periods from the primitives (mask on `~np.isnan(...)`)
- Float64 for all numeric arrays
- O(n) algorithms only
### MUST NOT
- Never reimplement an indicator that already exists in `openalgo.ta`
- Never write per-bar Python loops over large arrays — vectorize instead
- Never divide without masking zero/NaN denominators
- If the indicator is genuinely path-dependent (sequential state no primitive covers), check whether `ta.ema` / Wilder-style primitives already provide the recursion first; a plain Python loop is a last resort — keep it O(n) and document the trade-off
### Available Building Blocks
Public `ta` methods that run in the Rust core:
```python
from openalgo import ta
# Rolling math: ta.sma, ta.ema, ta.wma, ta.stdev, ta.highest, ta.lowest
# Price action: ta.true_range, ta.atr, ta.change, ta.roc
# Bands/channels: ta.bbands, ta.keltner, ta.donchian
# Signals: ta.crossover, ta.crossunder, ta.exrem, ta.rising, ta.falling
```
## Common Custom Indicator Patterns
| Pattern | Implementation |
|---------|---------------|
| Z-Score | `(value - rolling_mean) / rolling_stdev` |
| Squeeze | Bollinger inside Keltner channel |
| VWAP Bands | VWAP + N * rolling stdev of (close - vwap) |
| Momentum Score | Weighted sum of RSI + MACD + ADX conditions |
| Mean Reversion | Distance from SMA as % + threshold |
| Range Filter | ATR-based dynamic filter on close |
| Trend Strength | ADX + directional movement composite |
## Example Usage
`/custom-indicator zscore`
`/custom-indicator squeeze-momentum`
`/custom-indicator vwap-bands`
`/custom-indicator range-filter`
Mirrored from the author's public source. Install counts from the open skills registry.