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.
- DuckDB data loading: When user provides a DuckDB path, load data directly using
duckdb.connect() with read_only=True. Auto-detect format: OpenAlgo Historify (table market_data, epoch timestamps) vs custom (table ohlcv, date+time columns). See duckdb-data.
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) |
| crypto-market-costs |
Crypto fee model (spot, USDT-M, COIN-M futures) |
| futures-backtesting |
Lot sizes (SEBI revised Dec 2025), value sizing |
| long-short-trading |
Simultaneous long/short, direction comparison |
| duckdb-data |
DuckDB direct loading, Historify format, auto-detect, resampling, multi-symbol |
| csv-data-resampling |
Loading CSV, resampling with Indian market alignment |
| walk-forward |
Walk-forward analysis, WFE ratio |
| robustness-testing |
Monte Carlo, noise test, parameter sensitivity, delay test |
| pitfalls |
Common mistakes and checklist before going live |
| strategy-catalog |
Strategy reference with code snippets |
| openstatz-tearsheet |
OpenStatz interactive offline dashboard, metrics, Monte Carlo (replaces QuantStats) |
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
# --- Config ---
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 # Indian delivery equity (STT + statutory)
FIXED_FEES = 20 # Rs 20 per order
ALLOCATION = 0.75
BENCHMARK_SYMBOL = "NIFTY"
BENCHMARK_EXCHANGE = "NSE_INDEX"
# --- Fetch Data ---
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()
if df.index.tz is not None:
df.index = df.index.tz_convert(None)
close = df["close"]
# --- Strategy: EMA Crossover (OpenAlgo ta - default indicator library) ---
ema_fast = ta.ema(close, 10)
ema_slow = ta.ema(close, 20)
buy_raw = (ema_fast > ema_slow) & (ema_fast.shift(1) <= ema_slow.shift(1))
sell_raw = (ema_fast < ema_slow) & (ema_fast.shift(1) >= ema_slow.shift(1))
entries = ta.exrem(buy_raw.fillna(False), sell_raw.fillna(False))
exits = ta.exrem(sell_raw.fillna(False), buy_raw.fillna(False))
# --- Backtest ---
pf = vbt.Portfolio.from_signals(
close, entries, exits,
init_cash=INIT_CASH, size=ALLOCATION, size_type="percent",
fees=FEES, fixed_fees=FIXED_FEES, direction="longonly",
min_size=1, size_granularity=1, freq="1D",
)
# --- Benchmark ---
df_bench = client.history(
symbol=BENCHMARK_SYMBOL, exchange=BENCHMARK_EXCHANGE, interval=INTERVAL,
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d"),
)
if "timestamp" in df_bench.columns:
df_bench["timestamp"] = pd.to_datetime(df_bench["timestamp"])
df_bench = df_bench.set_index("timestamp")
else:
df_bench.index = pd.to_datetime(df_bench.index)
df_bench = df_bench.sort_index()
if df_bench.index.tz is not None:
df_bench.index = df_bench.index.tz_convert(None)
bench_close = df_bench["close"].reindex(close.index).ffill().bfill()
pf_bench = vbt.Portfolio.from_holding(bench_close, init_cash=INIT_CASH, fees=FEES, freq="1D")
# --- Results ---
print(pf.stats())
# --- Strategy vs Benchmark ---
comparison = pd.DataFrame({
"Strategy": [
f"{pf.total_return() * 100:.2f}%", f"{pf.sharpe_ratio():.2f}",
f"{pf.sortino_ratio():.2f}", f"{pf.max_drawdown() * 100:.2f}%",
f"{pf.trades.win_rate() * 100:.1f}%", f"{pf.trades.count()}",
f"{pf.trades.profit_factor():.2f}",
],
f"Benchmark ({BENCHMARK_SYMBOL})": [
f"{pf_bench.total_return() * 100:.2f}%", f"{pf_bench.sharpe_ratio():.2f}",
f"{pf_bench.sortino_ratio():.2f}", f"{pf_bench.max_drawdown() * 100:.2f}%",
"-", "-", "-",
],
}, index=["Total Return", "Sharpe Ratio", "Sortino Ratio", "Max Drawdown",
"Win Rate", "Total Trades", "Profit Factor"])
print(comparison.to_string())
# --- Explain ---
print(f"* Total Return: {pf.total_return() * 100:.2f}% vs NIFTY {pf_bench.total_return() * 100:.2f}%")
print(f"* Max Drawdown: {pf.max_drawdown() * 100:.2f}%")
print(f" -> On Rs {INIT_CASH:,}, worst temporary loss = Rs {abs(pf.max_drawdown()) * INIT_CASH:,.0f}")
# --- Plot ---
fig = pf.plot(subplots=['value', 'underwater', 'cum_returns'], template="plotly_dark")
fig.show()
# --- Export ---
pf.positions.records_readable.to_csv(script_dir / f"{SYMBOL}_trades.csv", index=False)
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:
# Default: OpenAlgo ta for both indicators and signal cleaning
from openalgo import ta
exrem = ta.exrem
ema = ta.ema
except ImportError:
# Fallback ONLY when the openalgo package itself is not installed
# (standalone DuckDB with no OpenAlgo). Use TA-Lib for indicators
# and this inline exrem() replacement for signal cleaning.
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
# --- Config ---
SYMBOL = "SBIN"
DB_PATH = r"path/to/market_data.duckdb"
INIT_CASH = 1_000_000
FEES = 0.000225 # Intraday equity
FIXED_FEES = 20
# --- Load from DuckDB ---
con = duckdb.connect(DB_PATH, read_only=True)
df = con.execute("""
SELECT date, time, open, high, low, close, volume
FROM ohlcv WHERE symbol = ? ORDER BY date, time
""", [SYMBOL]).fetchdf()
con.close()
df["datetime"] = pd.to_datetime(df["date"].astype(str) + " " + df["time"].astype(str))
df = df.set_index("datetime").sort_index()
df = df.drop(columns=["date", "time"])
# --- Resample to 5min ---
df_5m = df.resample("5min", origin="start_day", offset="9h15min",
label="right", closed="right").agg({
"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"
}).dropna()
close = df_5m["close"]
# --- Strategy + Backtest (same as OpenAlgo template, but use the ema()/exrem() resolved above) ---
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.
1---2name: vectorbt-expert3description: 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).4---5
6# VectorBT Backtesting Expert Skill
7
8## Environment
9
10- Python with vectorbt, pandas, numpy, plotly
11- Data sources: OpenAlgo (Indian markets), DuckDB (direct database), yfinance (US/Global), CCXT (Crypto), custom providers
12- DuckDB support: supports both custom DuckDB and OpenAlgo Historify format
13- API keys loaded from single root `.env` via `python-dotenv` + `find_dotenv()` — never hardcode keys
14- 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.
15- Specialty indicators (no TA-Lib equivalent, always `openalgo.ta`): Supertrend, Donchian, Ichimoku, HMA, KAMA, ALMA, ZLEMA, VWMA
16- Signal cleaning: `openalgo.ta` for exrem, crossover, crossunder, flip (always, regardless of indicator library)
17- Fee model: Indian market standard (STT + statutory charges + Rs 20/order)
18- Benchmark: NIFTY 50 via OpenAlgo (`NSE_INDEX`) by default
19- Charts: Plotly with `template="plotly_dark"`
20- Environment variables loaded from single `.env` at project root via `find_dotenv()` (walks up from script dir)
21- Scripts go in `backtesting/{strategy_name}/` directories (created on-demand, not pre-created)
22- Never use icons/emojis in code or logger output
23
24## Critical Rules
25
261. **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.
272. **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.
283. **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](rules/duckdb-data.md).
294. **Always clean signals** with `ta.exrem()` after generating raw buy/sell signals. Always `.fillna(False)` before exrem.
305. **Market-specific fees**: India ([indian-market-costs](rules/indian-market-costs.md)), US ([us-market-costs](rules/us-market-costs.md)), Crypto ([crypto-market-costs](rules/crypto-market-costs.md)). Auto-select based on user's market.
316. **Default benchmarks**: India=NIFTY via OpenAlgo, US=S&P 500 (`^GSPC`), Crypto=Bitcoin (`BTC-USD`). See [data-fetching](rules/data-fetching.md) Market Selection Guide.
327. **Always produce** a Strategy vs Benchmark comparison table after every backtest.
338. **Always explain** the backtest report in plain language so even normal traders understand risk and strength.
349. **Plotly candlestick charts** must use `xaxis type="category"` to avoid weekend gaps.
3510. **Whole shares**: Always set `min_size=1, size_granularity=1` for equities.
3611. **DuckDB data loading**: When user provides a DuckDB path, load data directly using `duckdb.connect()` with `read_only=True`. Auto-detect format: OpenAlgo Historify (table `market_data`, epoch timestamps) vs custom (table `ohlcv`, date+time columns). See [duckdb-data](rules/duckdb-data.md).
37
38## Modular Rule Files
39
40Detailed reference for each topic is in `rules/`:
41
42| Rule File | Topic |
43|-----------|-------|
44| [data-fetching](rules/data-fetching.md) | OpenAlgo (India), yfinance (US), CCXT (Crypto), custom providers, .env setup |
45| [simulation-modes](rules/simulation-modes.md) | from_signals, from_orders, from_holding, direction types |
46| [position-sizing](rules/position-sizing.md) | Amount/Value/Percent/TargetPercent sizing |
47| [indicators-signals](rules/indicators-signals.md) | OpenAlgo ta indicator reference (default), TA-Lib opt-in, signal generation |
48| [openalgo-ta-helpers](rules/openalgo-ta-helpers.md) | Complete OpenAlgo ta catalog (100+ indicators): exrem, crossover, Supertrend, Donchian, Ichimoku, MAs |
49| [stop-loss-take-profit](rules/stop-loss-take-profit.md) | Fixed SL, TP, trailing stop |
50| [parameter-optimization](rules/parameter-optimization.md) | Broadcasting and loop-based optimization |
51| [performance-analysis](rules/performance-analysis.md) | Stats, metrics, benchmark comparison, CAGR |
52| [plotting](rules/plotting.md) | Candlestick (category x-axis), VectorBT plots, custom Plotly |
53| [indian-market-costs](rules/indian-market-costs.md) | Indian market fee model by segment |
54| [us-market-costs](rules/us-market-costs.md) | US market fee model (stocks, options, futures) |
55| [crypto-market-costs](rules/crypto-market-costs.md) | Crypto fee model (spot, USDT-M, COIN-M futures) |
56| [futures-backtesting](rules/futures-backtesting.md) | Lot sizes (SEBI revised Dec 2025), value sizing |
57| [long-short-trading](rules/long-short-trading.md) | Simultaneous long/short, direction comparison |
58| [duckdb-data](rules/duckdb-data.md) | DuckDB direct loading, Historify format, auto-detect, resampling, multi-symbol |
59| [csv-data-resampling](rules/csv-data-resampling.md) | Loading CSV, resampling with Indian market alignment |
60| [walk-forward](rules/walk-forward.md) | Walk-forward analysis, WFE ratio |
61| [robustness-testing](rules/robustness-testing.md) | Monte Carlo, noise test, parameter sensitivity, delay test |
62| [pitfalls](rules/pitfalls.md) | Common mistakes and checklist before going live |
63| [strategy-catalog](rules/strategy-catalog.md) | Strategy reference with code snippets |
64| [openstatz-tearsheet](rules/openstatz-tearsheet.md) | OpenStatz interactive offline dashboard, metrics, Monte Carlo (replaces QuantStats) |
65
66## Strategy Templates (in rules/assets/)
67
68Production-ready scripts with realistic fees, NIFTY benchmark, comparison table, and plain-language report:
69
70| Template | Path | Description |
71|----------|------|-------------|
72| EMA Crossover | `assets/ema_crossover/backtest.py` | EMA 10/20 crossover |
73| RSI | `assets/rsi/backtest.py` | RSI(14) oversold/overbought |
74| Donchian | `assets/donchian/backtest.py` | Donchian channel breakout |
75| Supertrend | `assets/supertrend/backtest.py` | Supertrend with intraday sessions |
76| MACD | `assets/macd/backtest.py` | MACD signal-candle breakout |
77| SDA2 | `assets/sda2/backtest.py` | SDA2 trend following |
78| Momentum | `assets/momentum/backtest.py` | Double momentum (MOM + MOM-of-MOM) |
79| Dual Momentum | `assets/dual_momentum/backtest.py` | Quarterly ETF rotation |
80| Buy & Hold | `assets/buy_hold/backtest.py` | Static multi-asset allocation |
81| RSI Accumulation | `assets/rsi_accumulation/backtest.py` | Weekly RSI slab-wise accumulation |
82| Walk-Forward | `assets/walk_forward/template.py` | Walk-forward analysis template |
83| Realistic Costs | `assets/realistic_costs/template.py` | Transaction cost impact comparison |
84
85## Quick Template: Standard Backtest Script
86
87```python
88import os
89from datetime import datetime, timedelta
90from pathlib import Path
91
92import numpy as np
93import pandas as pd
94import vectorbt as vbt
95from dotenv import find_dotenv, load_dotenv
96from openalgo import api, ta
97
98# --- Config ---
99script_dir = Path(__file__).resolve().parent
100load_dotenv(find_dotenv(), override=False)
101
102SYMBOL = "SBIN"
103EXCHANGE = "NSE"
104INTERVAL = "D"
105INIT_CASH = 1_000_000
106FEES = 0.00111 # Indian delivery equity (STT + statutory)
107FIXED_FEES = 20 # Rs 20 per order
108ALLOCATION = 0.75
109BENCHMARK_SYMBOL = "NIFTY"
110BENCHMARK_EXCHANGE = "NSE_INDEX"
111
112# --- Fetch Data ---
113client = api(
114 api_key=os.getenv("OPENALGO_API_KEY"),
115 host=os.getenv("OPENALGO_HOST", "http://127.0.0.1:5000"),
116)
117
118end_date = datetime.now().date()
119start_date = end_date - timedelta(days=365 * 3)
120
121df = client.history(
122 symbol=SYMBOL, exchange=EXCHANGE, interval=INTERVAL,
123 start_date=start_date.strftime("%Y-%m-%d"),
124 end_date=end_date.strftime("%Y-%m-%d"),
125)
126if "timestamp" in df.columns:
127 df["timestamp"] = pd.to_datetime(df["timestamp"])
128 df = df.set_index("timestamp")
129else:
130 df.index = pd.to_datetime(df.index)
131df = df.sort_index()
132if df.index.tz is not None:
133 df.index = df.index.tz_convert(None)
134
135close = df["close"]
136
137# --- Strategy: EMA Crossover (OpenAlgo ta - default indicator library) ---
138ema_fast = ta.ema(close, 10)
139ema_slow = ta.ema(close, 20)
140
141buy_raw = (ema_fast > ema_slow) & (ema_fast.shift(1) <= ema_slow.shift(1))
142sell_raw = (ema_fast < ema_slow) & (ema_fast.shift(1) >= ema_slow.shift(1))
143
144entries = ta.exrem(buy_raw.fillna(False), sell_raw.fillna(False))
145exits = ta.exrem(sell_raw.fillna(False), buy_raw.fillna(False))
146
147# --- Backtest ---
148pf = vbt.Portfolio.from_signals(
149 close, entries, exits,
150 init_cash=INIT_CASH, size=ALLOCATION, size_type="percent",
151 fees=FEES, fixed_fees=FIXED_FEES, direction="longonly",
152 min_size=1, size_granularity=1, freq="1D",
153)
154
155# --- Benchmark ---
156df_bench = client.history(
157 symbol=BENCHMARK_SYMBOL, exchange=BENCHMARK_EXCHANGE, interval=INTERVAL,
158 start_date=start_date.strftime("%Y-%m-%d"),
159 end_date=end_date.strftime("%Y-%m-%d"),
160)
161if "timestamp" in df_bench.columns:
162 df_bench["timestamp"] = pd.to_datetime(df_bench["timestamp"])
163 df_bench = df_bench.set_index("timestamp")
164else:
165 df_bench.index = pd.to_datetime(df_bench.index)
166df_bench = df_bench.sort_index()
167if df_bench.index.tz is not None:
168 df_bench.index = df_bench.index.tz_convert(None)
169bench_close = df_bench["close"].reindex(close.index).ffill().bfill()
170pf_bench = vbt.Portfolio.from_holding(bench_close, init_cash=INIT_CASH, fees=FEES, freq="1D")
171
172# --- Results ---
173print(pf.stats())
174
175# --- Strategy vs Benchmark ---
176comparison = pd.DataFrame({
177 "Strategy": [
178 f"{pf.total_return() * 100:.2f}%", f"{pf.sharpe_ratio():.2f}",
179 f"{pf.sortino_ratio():.2f}", f"{pf.max_drawdown() * 100:.2f}%",
180 f"{pf.trades.win_rate() * 100:.1f}%", f"{pf.trades.count()}",
181 f"{pf.trades.profit_factor():.2f}",
182 ],
183 f"Benchmark ({BENCHMARK_SYMBOL})": [
184 f"{pf_bench.total_return() * 100:.2f}%", f"{pf_bench.sharpe_ratio():.2f}",
185 f"{pf_bench.sortino_ratio():.2f}", f"{pf_bench.max_drawdown() * 100:.2f}%",
186 "-", "-", "-",
187 ],
188}, index=["Total Return", "Sharpe Ratio", "Sortino Ratio", "Max Drawdown",
189 "Win Rate", "Total Trades", "Profit Factor"])
190print(comparison.to_string())
191
192# --- Explain ---
193print(f"* Total Return: {pf.total_return() * 100:.2f}% vs NIFTY {pf_bench.total_return() * 100:.2f}%")
194print(f"* Max Drawdown: {pf.max_drawdown() * 100:.2f}%")
195print(f" -> On Rs {INIT_CASH:,}, worst temporary loss = Rs {abs(pf.max_drawdown()) * INIT_CASH:,.0f}")
196
197# --- Plot ---
198fig = pf.plot(subplots=['value', 'underwater', 'cum_returns'], template="plotly_dark")
199fig.show()
200
201# --- Export ---
202pf.positions.records_readable.to_csv(script_dir / f"{SYMBOL}_trades.csv", index=False)
203```
204
205## Quick Template: DuckDB Backtest Script
206
207```python
208import datetime as dt
209from pathlib import Path
210
211import duckdb
212import numpy as np
213import pandas as pd
214import vectorbt as vbt
215
216try:
217 # Default: OpenAlgo ta for both indicators and signal cleaning
218 from openalgo import ta
219 exrem = ta.exrem
220 ema = ta.ema
221except ImportError:
222 # Fallback ONLY when the openalgo package itself is not installed
223 # (standalone DuckDB with no OpenAlgo). Use TA-Lib for indicators
224 # and this inline exrem() replacement for signal cleaning.
225 import talib as tl
226
227 def ema(data, period):
228 return pd.Series(tl.EMA(data.values, timeperiod=period), index=data.index)
229
230 def exrem(signal1, signal2):
231 result = signal1.copy()
232 active = False
233 for i in range(len(signal1)):
234 if active:
235 result.iloc[i] = False
236 if signal1.iloc[i] and not active:
237 active = True
238 if signal2.iloc[i]:
239 active = False
240 return result
241
242# --- Config ---
243SYMBOL = "SBIN"
244DB_PATH = r"path/to/market_data.duckdb"
245INIT_CASH = 1_000_000
246FEES = 0.000225 # Intraday equity
247FIXED_FEES = 20
248
249# --- Load from DuckDB ---
250con = duckdb.connect(DB_PATH, read_only=True)
251df = con.execute("""
252 SELECT date, time, open, high, low, close, volume
253 FROM ohlcv WHERE symbol = ? ORDER BY date, time
254""", [SYMBOL]).fetchdf()
255con.close()
256
257df["datetime"] = pd.to_datetime(df["date"].astype(str) + " " + df["time"].astype(str))
258df = df.set_index("datetime").sort_index()
259df = df.drop(columns=["date", "time"])
260
261# --- Resample to 5min ---
262df_5m = df.resample("5min", origin="start_day", offset="9h15min",
263 label="right", closed="right").agg({
264 "open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"
265}).dropna()
266close = df_5m["close"]
267
268# --- Strategy + Backtest (same as OpenAlgo template, but use the ema()/exrem() resolved above) ---
269```
270
271If 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.