Backtest on NSE data with vectorbt
pip install vectorbt
import vectorbt as vbt
import pandas as pd
# Load from your local store (chapter 30)
df = load_candles(instrument_token=408065, interval="day") # INFY daily
fast_ma = vbt.MA.run(df["close"], window=20)
slow_ma = vbt.MA.run(df["close"], window=50)
entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)
portfolio = vbt.Portfolio.from_signals(
df["close"],
entries, exits,
init_cash=100000,
fees=0.0003, # 0.03% per trade — set realistically, chapter 86
slippage=0.001, # 0.1% slippage assumption — set realistically, chapter 86
freq="1D",
)
print(portfolio.stats())
Key stats to actually read (not just "total return"):
stats = portfolio.stats()
print("Total return:", stats["Total Return [%]"])
print("Sharpe ratio:", stats["Sharpe Ratio"])
print("Max drawdown:", stats["Max Drawdown [%]"])
print("Win rate:", stats["Win Rate [%]"])
print("Number of trades:", stats["Total Trades"])
Running across a universe, not just one stock
def backtest_universe(tickers: list[str], strategy_fn) -> pd.DataFrame:
results = []
for ticker in tickers:
df = load_candles(get_token(ticker), "day")
if len(df) < 100:
continue # not enough history — exclude, don't silently include with bad stats
pf = strategy_fn(df)
results.append({"ticker": ticker, **pf.stats().to_dict()})
return pd.DataFrame(results)
Look at the distribution of results across the universe, not just the average — a strategy that's wildly profitable on 5 stocks and loses money on 45 others is not a universe-wide edge, it's noise plus a few lucky picks (directly relevant to chapter 85's overfitting discussion).
Backtrader alternative, for event-driven needs (chapter 81)
pip install backtrader
import backtrader as bt
class SmaCross(bt.Strategy):
params = (("fast", 20), ("slow", 50))
def __init__(self):
fast_sma = bt.ind.SMA(period=self.p.fast)
slow_sma = bt.ind.SMA(period=self.p.slow)
self.crossover = bt.ind.CrossOver(fast_sma, slow_sma)
def next(self):
if not self.position and self.crossover > 0:
self.buy()
elif self.position and self.crossover < 0:
self.close()
cerebro = bt.Cerebro()
cerebro.addstrategy(SmaCross)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(commission=0.0003)
cerebro.run()