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()

Next: 083 — Look-ahead bias and survivorship bias