| name | vectorbt-expert |
| description | VectorBT backtesting expert. Use when user asks to backtest strategies, create entry/exit signals, analyze portfolio performance, optimize parameters, fetch historical data, use VectorBT/vectorbt, compare strategies, position sizing, equity curves, drawdown charts, or trade analysis. Also triggers for openalgo.ta helpers (exrem, crossover, crossunder, flip, donchian, supertrend). |
| user-invocable | false |
VectorBT Backtesting Expert Skill
Environment
- Python with vectorbt, pandas, numpy, plotly
- Data sources: OpenAlgo (Indian markets), DuckDB (direct database), yfinance (US/Global), CCXT (Crypto), custom providers
- DuckDB support: supports both custom DuckDB and OpenAlgo Historify format
- API keys loaded from single root
.env via python-dotenv + find_dotenv() — never hardcode keys
- Technical indicators: OpenAlgo ta (DEFAULT -
from openalgo import ta, 100+ indicators covering trend/momentum/volatility/volume/oscillators/statistical/hybrid). Use TA-Lib only if the user explicitly asks for TA-Lib/talib. NEVER use VectorBT built-in indicators either way.
- Specialty indicators (no TA-Lib equivalent, always
openalgo.ta): Supertrend, Donchian, Ichimoku, HMA, KAMA, ALMA, ZLEMA, VWMA
- Signal cleaning:
openalgo.ta for exrem, crossover, crossunder, flip (always, regardless of indicator library)
- Fee model: Indian market standard (STT + statutory charges + Rs 20/order)
- Benchmark: NIFTY 50 via OpenAlgo (
NSE_INDEX) by default
- Charts: Plotly with
template="plotly_dark"
- Environment variables loaded from single
.env at project root via find_dotenv() (walks up from script dir)
- Scripts go in
backtesting/{strategy_name}/ directories (created on-demand, not pre-created)
- Never use icons/emojis in code or logger output
Critical Rules
- Default to OpenAlgo ta (
from openalgo import ta) for ALL technical indicators (EMA, SMA, RSI, MACD, BBANDS, ATR, ADX, STDDEV, MOM, and 90+ more). Only use TA-Lib if the user explicitly requests "talib"/"TA-Lib" in their prompt. NEVER use vbt.MA.run(), vbt.RSI.run(), or any VectorBT built-in indicator with either library.
- Always use OpenAlgo ta for indicators not in TA-Lib at all: Supertrend, Donchian, Ichimoku, HMA, KAMA, ALMA, ZLEMA, VWMA - these have no TA-Lib equivalent, so they're openalgo.ta even in a TA-Lib-opt-in script.
- Use OpenAlgo ta for signal utilities:
ta.exrem(), ta.crossover(), ta.crossunder(), ta.flip(). If openalgo.ta is not importable (standalone DuckDB), use inline exrem() fallback. See duckdb-data.
- Always clean signals with
ta.exrem() after generating raw buy/sell signals. Always .fillna(False) before exrem.
- Market-specific fees: India (indian-market-costs), US (us-market-costs), Crypto (crypto-market-costs). Auto-select based on user's market.
- Default benchmarks: India=NIFTY via OpenAlgo, US=S&P 500 (
^GSPC), Crypto=Bitcoin (BTC-USD). See data-fetching Market Selection Guide.
- Always produce a Strategy vs Benchmark comparison table after every backtest.
- Always explain the backtest report in plain language so even normal traders understand risk and strength.
- Plotly candlestick charts must use
xaxis type="category" to avoid weekend gaps.
- Whole shares: Always set
min_size=1, size_granularity=1 for equities.
Modular Rule Files
Detailed reference for each topic is in rules/:
| Rule File | Topic |
|---|
| data-fetching | OpenAlgo (India), yfinance (US), CCXT (Crypto), custom providers, .env setup |
| simulation-modes | from_signals, from_orders, from_holding, direction types |
| position-sizing | Amount/Value/Percent/TargetPercent sizing |
| indicators-signals | OpenAlgo ta indicator reference (default), TA-Lib opt-in, signal generation |
| openalgo-ta-helpers | Complete OpenAlgo ta catalog (100+ indicators): exrem, crossover, Supertrend, Donchian, Ichimoku, MAs |
| stop-loss-take-profit | Fixed SL, TP, trailing stop |
| parameter-optimization | Broadcasting and loop-based optimization |
| performance-analysis | Stats, metrics, benchmark comparison, CAGR |
| plotting | Candlestick (category x-axis), VectorBT plots, custom Plotly |
| indian-market-costs | Indian market fee model by segment |
| us-market-costs | US market fee model (stocks, options, futures) |
|
Strategy Templates (in rules/assets/)
Production-ready scripts with realistic fees, NIFTY benchmark, comparison table, and plain-language report:
| Template | Path | Description |
|---|
| EMA Crossover | assets/ema_crossover/backtest.py | EMA 10/20 crossover |
| RSI | assets/rsi/backtest.py | RSI(14) oversold/overbought |
| Donchian | assets/donchian/backtest.py | Donchian channel breakout |
| Supertrend | assets/supertrend/backtest.py | Supertrend with intraday sessions |
| MACD | assets/macd/backtest.py | MACD signal-candle breakout |
| SDA2 | assets/sda2/backtest.py | SDA2 trend following |
| Momentum | assets/momentum/backtest.py | Double momentum (MOM + MOM-of-MOM) |
| Dual Momentum | assets/dual_momentum/backtest.py | Quarterly ETF rotation |
| Buy & Hold | assets/buy_hold/backtest.py | Static multi-asset allocation |
| RSI Accumulation | assets/rsi_accumulation/backtest.py | Weekly RSI slab-wise accumulation |
| Walk-Forward | assets/walk_forward/template.py | Walk-forward analysis template |
| Realistic Costs | assets/realistic_costs/template.py | Transaction cost impact comparison |
Quick Template: Standard Backtest Script
import os
from datetime import datetime, timedelta
from pathlib import Path
import numpy as np
import pandas as pd
import vectorbt as vbt
from dotenv import find_dotenv, load_dotenv
from openalgo import api, ta
script_dir = Path(__file__).resolve().parent
load_dotenv(find_dotenv(), override=False)
SYMBOL = "SBIN"
EXCHANGE = "NSE"
INTERVAL = "D"
INIT_CASH = 1_000_000
FEES = 0.00111
FIXED_FEES = 20
ALLOCATION = 0.75
BENCHMARK_SYMBOL = "NIFTY"
BENCHMARK_EXCHANGE = "NSE_INDEX"
client = api(
api_key=os.getenv("OPENALGO_API_KEY"),
host=os.getenv("OPENALGO_HOST", "http://127.0.0.1:5000"),
)
end_date = datetime.now().date()
start_date = end_date - timedelta(days=365 * 3)
df = client.history(
symbol=SYMBOL, exchange=EXCHANGE, interval=INTERVAL,
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d"),
)
if "timestamp" in df.columns:
df["timestamp"] = pd.to_datetime(df["timestamp"])
df = df.set_index("timestamp")
else:
df.index = pd.to_datetime(df.index)
df = df.sort_index()
df.index.tz :
df.index = df.index.tz_convert()
close = df[]
ema_fast = ta.ema(close, )
ema_slow = ta.ema(close, )
buy_raw = (ema_fast > ema_slow) & (ema_fast.shift() <= ema_slow.shift())
sell_raw = (ema_fast < ema_slow) & (ema_fast.shift() >= ema_slow.shift())
entries = ta.exrem(buy_raw.fillna(), sell_raw.fillna())
exits = ta.exrem(sell_raw.fillna(), buy_raw.fillna())
pf = vbt.Portfolio.from_signals(
close, entries, exits,
init_cash=INIT_CASH, size=ALLOCATION, size_type=,
fees=FEES, fixed_fees=FIXED_FEES, direction=,
min_size=, size_granularity=, freq=,
)
df_bench = client.history(
symbol=BENCHMARK_SYMBOL, exchange=BENCHMARK_EXCHANGE, interval=INTERVAL,
start_date=start_date.strftime(),
end_date=end_date.strftime(),
)
df_bench.columns:
df_bench[] = pd.to_datetime(df_bench[])
df_bench = df_bench.set_index()
:
df_bench.index = pd.to_datetime(df_bench.index)
df_bench = df_bench.sort_index()
df_bench.index.tz :
df_bench.index = df_bench.index.tz_convert()
bench_close = df_bench[].reindex(close.index).ffill().bfill()
pf_bench = vbt.Portfolio.from_holding(bench_close, init_cash=INIT_CASH, fees=FEES, freq=)
(pf.stats())
comparison = pd.DataFrame({
: [
, ,
, ,
, ,
,
],
: [
, ,
, ,
, , ,
],
}, index=[, , , ,
, , ])
(comparison.to_string())
()
()
()
fig = pf.plot(subplots=[, , ], template=)
fig.show()
pf.positions.records_readable.to_csv(script_dir / , index=)
Quick Template: DuckDB Backtest Script
import datetime as dt
from pathlib import Path
import duckdb
import numpy as np
import pandas as pd
import vectorbt as vbt
try:
from openalgo import ta
exrem = ta.exrem
ema = ta.ema
except ImportError:
import talib as tl
def ema(data, period):
return pd.Series(tl.EMA(data.values, timeperiod=period), index=data.index)
def exrem(signal1, signal2):
result = signal1.copy()
active = False
for i in range(len(signal1)):
if active:
result.iloc[i] = False
if signal1.iloc[i] and not active:
active = True
if signal2.iloc[i]:
active = False
return result
SYMBOL = "SBIN"
DB_PATH = r"path/to/market_data.duckdb"
INIT_CASH =
FEES =
FIXED_FEES =
con = duckdb.connect(DB_PATH, read_only=)
df = con.execute(, [SYMBOL]).fetchdf()
con.close()
df[] = pd.to_datetime(df[].astype() + + df[].astype())
df = df.set_index().sort_index()
df = df.drop(columns=[, ])
df_5m = df.resample(, origin=, offset=,
label=, closed=).agg({
: , : , : , : , :
}).dropna()
close = df_5m[]
If the user explicitly asks for TA-Lib, skip the try/except above and import talib as tl directly instead - the exrem fallback is only for when openalgo itself is unavailable.