Ledger & Candle
Chapter 12 · Risk & Systems

Trading Systems & Backtesting

Reading time ~110 min · Data as of 30 Sep 2026 · Based on Zerodha Varsity Module 10 (Trading Systems, all 16 chapters) and the performance chapters of Module 9 (Equity Curve, Expected Returns)

1. What you'll learn and why it matters

So far this course has given you tools: candlesticks, support and resistance, indicators, a swing playbook, fundamentals, futures, options and risk rules. A trading system combines those tools into one complete written rulebook, and backtesting checks how that rulebook would have done on past data before you put money behind it.

By the end of this chapter you will be able to:

Why this matters for you

Swing traders: without a tested rulebook, every trade is a fresh guess and every loss feels like a disaster. With one, you know that a 20-trade losing streak can happen inside a system that still makes money (System B below had one). Long-term investors: the same tools tell you whether a "rule" such as "sell when the 50-day crosses below the 200-day" actually beats doing nothing. On Nifty it did not (Section 6). Future futures traders: with leverage, an untested system loses money quickly. The pair and calendar-spread sections use real futures lot sizes and current F&O charges.

An honest preview

Most of what you will see in this chapter is disappointing, on purpose. Two of our three systems lost to plain buy-and-hold after costs. The one that beat Nifty did so mainly because of the stocks we chose to test it on. Random entries with the same exits did as well as its breakout entries. That is a normal result. Backtesting is mostly a tool for rejecting bad ideas cheaply, before they cost real money. Everything here is historical analysis for learning, not a recommendation to buy or sell anything.

How to read this chapter

Sections 2–10 are the core: what a system is, how to backtest one honestly, and how to judge the result. Sections 11–13 (pair trading and calendar spreads) need futures and some statistics, so on a first read you can skim them and return after you are comfortable with Chapter 9. Section 14 (momentum portfolios) and Section 16 (going live) apply to everyone. Section 15 has short programs you can run yourself.

2. What a trading system is (and is not)

Varsity's Module 10 opens with a trade its author placed on 25 Oct 2017, the day after the government announced ₹2.1 lakh crore of recapitalisation for public-sector banks. PSU bank stocks shot up. Bank Nifty rose about 3%, even though PSU banks were only a small slice of that index. That looked overdone, so the author sold a Bank Nifty strangle to collect premium. Varsity calls this kind of reasoning systematic deduction: a sensible, one-off chain of logic. It is also not a system, because it cannot be written down as a repeatable process and tested on history. The same is true of trading on gut feel, on a friend's tip, on a TV anchor's call or on a broker's call.

A trading system is a process with defined inputs (prices, volumes, fundamentals), fixed logic, and an output that tells you what to do: trade or don't, how much, and when to get out. You design the logic once and then follow it. Varsity warns that a system is not a "holy grail" money machine. You still choose the inputs, design the logic and decide to follow it, so the results are still your responsibility.

The five parts of a complete system

ComponentQuestion it answersExample (System B in this chapter)
UniverseWhich instruments am I allowed to trade?The 37 large, liquid NSE stocks in the course data set
EntryExactly when do I buy (or sell short)?Close above the highest high of the previous 55 sessions; buy at the next day's open
ExitWhen do I get out, whether I am winning or losing?Close below the lowest low of the previous 20 sessions, or the initial stop is touched
Position sizeHow many shares or lots?Risk 1% of current equity between entry and a 2×ATR(20) stop; never more than 20% of equity in one stock
Portfolio / risk rulesHow much in total, and what if many signals come at once?Maximum 10 open positions; if there are more signals than slots, take the strongest 6-month performers first

If any of the five is missing, you will fill the gap with emotion at the worst possible moment. "I'll exit when it looks weak" is not an exit rule.

Discretionary vs systematic

AspectDiscretionary traderSystematic trader
DecisionReads the chart and context, decides on the dayRules decide; the trader executes
Can it be backtested?Not reliably (memory and hindsight leak in)Yes, on years of data in minutes
StrengthAdapts to unusual events (results, policy, news)Consistent; removes fear and greed from each decision; scales to many stocks
WeaknessInconsistent; biases (Chapter 11) creep in; hard to tell skill from luckCan be over-fitted to history; can fail when the market changes; boring to follow through losing streaks
Realistic middle groundA rules-based trader: the system generates the candidates and fixes size and stop; discretion is allowed only to skip a trade for a written reason (for example, results due tomorrow), never to widen a stop or add size.
What Varsity Module 10 covers

Varsity's module planned five systems (pair trading, volatility-based, delta hedging, calendar spreads, momentum). It finished three in detail: pair trading (two methods), calendar spreads and a momentum portfolio. Its author says plainly that he left out backtesting because he did not code, and that you must backtest any system yourself before trading it. This chapter fills that gap. It teaches the backtesting method, applies it to three common swing/positional systems, and then backtests Varsity's own three systems on 2026 data.

3. The design process: idea to live money

Seven boxes in a row: hypothesis, rules, backtest, robustness, paper trade, small live, scale or retire, with an arrow looping from robustness back to hypothesis
The system design process. The first four steps cost only time. Real money enters only at step 6, and only at reduced size.
  1. Hypothesis: why should this make money? Write one sentence about market behaviour. "Stocks breaking to new highs tend to keep going because institutions buy in stages" is a hypothesis. "RSI below 30 is good" is not. If you cannot explain who is on the other side of the trade and why they lose, be suspicious of any profit you find.
  2. Rules. Turn the idea into the five components above, precisely enough that two people coding it would get the same trades. Decide the parameters before you look at results (use textbook defaults: 50/200, 55/20, RSI(2) < 10).
  3. Backtest. Run the rules on history with realistic execution (next-bar fills), costs and slippage.
  4. Robustness. Do the results survive a different period (out-of-sample), small changes in parameters, higher costs, random entries with the same exits, and a universe that includes the losers? Section 10 does each of these.
  5. Paper trade for 1–3 months. Log every signal in real time, exactly as the rules say. This catches practical problems a backtest cannot: missed orders, gaps at the open, data that arrives late, and your own reluctance to take a trade.
  6. Small live. Trade at 25–50% of the intended size for at least 20–30 trades and compare live results with the backtest's distribution.
  7. Scale up or retire. Go to full size only if live results fall inside the backtest's normal range. Set a kill switch in advance, for example "stop if drawdown exceeds 1.5× the backtest's maximum".
The one rule that protects you most

If a test fails, go back to step 1 with a new hypothesis. Do not tweak parameters until the backtest looks good. Every tweak "uses up" some of the history's information. After enough of them you have a perfect fit to the past and no edge in the future. Section 10.3 shows this on real data.

4. Build a backtester from scratch, step by step

A backtester answers one question: if I had followed these rules, trading only on information available at the time, what would my account have done? The phrase "only on information available at the time" is where most beginners' backtests go wrong. So we start with timing.

Two terms first. Slippage is the gap between the price your rules assume and the price you actually get (the bid-ask spread plus the price moving while your order fills). It is a hidden cost, so we charge a fixed percentage for it on every buy and sell. A vectorised backtest does each calculation on a whole column of dates at once (for example, one line computes the 50-day average for every day), instead of looping through the days one at a time. It is fast and short, and works well for one instrument that is either fully in or fully out.

Timeline of day t and day t+1 showing that indicators are computed after the close of day t and orders are filled at the open of day t+1
The timing convention used in every backtest in this chapter. A signal uses day t's completed bar and is filled at day t+1's open. Stops are checked against day t+1's low.

This convention suits swing and positional traders: you scan after 3:30 pm, place orders in the evening or before 9:15 am, and get filled at or near the open. It is also conservative. You never assume you got the signal day's closing price, because you only knew the signal after the close.

4.1 The six steps of a vectorised backtest (one instrument)

  1. Load clean data. Daily OHLC, split- and bonus-adjusted. (Our files are split-adjusted but not dividend-adjusted. For a price-index system on Nifty that is what you want.)
  2. Compute the signal at the close. For example signal = SMA50 > SMA200. A value of 1 means "I want to be long from tomorrow". Every indicator may use only data up to and including today.
  3. Shift by one bar to get the position. position = signal.shift(1). This single line is the difference between an honest backtest and a fantasy (Section 10.1).
  4. Earn returns correctly. Split each day's return into the overnight part (yesterday's close → today's open) and the intraday part (today's open → today's close). Yesterday's position earns the overnight gap. Today's position earns the intraday move.
  5. Charge costs on the days the position changes. Statutory charges plus slippage, as a fraction of the value traded.
  6. Compound into an equity curve. equity = (1 + net_return).cumprod(), then compute metrics.
Daily return of a long/flat system \[ r^{\text{co}}_t=\frac{O_t}{C_{t-1}}-1,\qquad r^{\text{oc}}_t=\frac{C_t}{O_t}-1 \] \[ r^{\text{gross}}_t = \big(1 + p_{t-1}\, r^{\text{co}}_t\big)\big(1 + p_t\, r^{\text{oc}}_t\big) - 1 \] \[ r^{\text{net}}_t = r^{\text{gross}}_t - \text{buy}_t\,(c_b + s) - \text{sell}_t\,(c_s + s) \] \[ E_t = E_{t-1}\,(1+r^{\text{net}}_t) \]
\(t\)
Today. \(t-1\) is the previous trading day.
\(O_t,\ C_t\)
Today's opening and closing price. \(C_{t-1}\) is yesterday's close.
\(r^{\text{co}}_t\)
The overnight return: from yesterday's close to today's open, as a fraction (0.003 means +0.3%). "co" = close to open.
\(r^{\text{oc}}_t\)
The intraday return: from today's open to today's close. "oc" = open to close.
\(p_t\)
The position held from today's open: 1 = long, 0 = flat (no position). It is yesterday's signal, shifted by one bar. \(p_{t-1}\) is the position you carried overnight into today.
\(r^{\text{gross}}_t\)
Today's return before costs.
\(\text{buy}_t,\ \text{sell}_t\)
1 on a day the position changes from 0 to 1 (a buy) or from 1 to 0 (a sale); 0 on every other day.
\(c_b,\ c_s\)
Statutory charges per rupee bought or sold (STT, exchange, SEBI, GST, stamp), as a fraction of the value traded. For Nifty delivery, 0.1187% to buy and 0.1037% to sell (Section 4.2).
\(s\)
Slippage per side, as a fraction of the value traded: 0.05% for Nifty, 0.1% for single stocks in this chapter.
\(r^{\text{net}}_t\)
Today's return after costs.
\(E_t\)
The equity curve: the account's value as a multiple of the starting money (1.0 at the start, 1.3 means +30%).
  1. Work out the overnight and intraday returns from the prices.
  2. The overnight move belongs to whatever you held overnight (\(p_{t-1}\)). The intraday move belongs to what you hold from today's open (\(p_t\)). A position of 0 earns nothing, because \(0 \times r = 0\). Multiplying the two \((1 + \dots)\) terms and subtracting 1 joins them into one day's return.
  3. On a day you buy or sell, subtract the charges and slippage. On other days nothing is subtracted.
  4. Multiply yesterday's equity by \((1 + r^{\text{net}}_t)\). Repeating this every day is the cumprod() in step 6. Section 4.3 works through real Nifty days with these formulas.

4.2 Realistic Indian costs (verified 2 Oct 2026)

From the Zerodha charges page (checked 2 Oct 2026). Equity delivery: zero brokerage; STT 0.1% on buy and on sell; NSE transaction charge 0.00307%; SEBI fee ₹10 per crore; GST 18% on (brokerage + exchange + SEBI); stamp duty 0.015% on the buy side; DP charge ₹15.34 per scrip on each sell day (₹3.5 CDSL + ₹9.5 Zerodha + ₹2.34 GST). Futures: brokerage 0.03% or ₹20 per order, whichever is lower; STT 0.05% on the sell side (raised from 0.02% by Budget 2026, effective 1 Apr 2026); NSE 0.00183%; stamp 0.002% on buy. Chapter 13 covers costs and tax in full.

Worked example · costs on a ₹1,00,000 delivery trade

What one round trip really costs

  1. Buy leg: STT 0.1% = ₹100.00; exchange 0.00307% = ₹3.07; SEBI = ₹0.10; GST 18% × (3.07 + 0.10) = ₹0.57; stamp 0.015% = ₹15.00. Total ₹118.74 (0.1187%).
  2. Sell leg: STT ₹100.00 + exchange ₹3.07 + SEBI ₹0.10 + GST ₹0.57 + DP ₹15.34 = ₹119.08.
  3. Excluding the flat DP charge, the round trip is 0.2225% of value (0.1187% to buy + 0.1037% to sell). Add slippage (we use 0.05% per side for Nifty, 0.1% per side for single stocks) and a stock round trip costs about 0.42%.
  4. Implication: a system whose average trade earns less than about 0.4% before costs cannot make money in delivery. System C below runs into exactly this problem.

4.3 The backtester, row by row, on real Nifty data

Here is the 50/200 crossover around its June 2025 buy signal, computed by the same code used for System A. The 50-day SMA closed above the 200-day SMA on 4 Jun 2025 (24,073.05 vs 24,070.94). The signal therefore turns 1 on the evening of 4 June, and the position starts at the open of 5 June.

DateOpenCloseSMA50SMA200SignalPositionOvernight rIntraday rCostNet rEquity
3 Jun 202524,786.3024,542.5024,038.7924,068.5300+0.282%−0.984%001.317066
4 Jun 202524,560.4524,620.2024,073.0524,070.9410+0.073%+0.243%001.317066
5 Jun 202524,691.2024,750.9024,104.2524,073.9811+0.288% (not earned)+0.242%0.1687%+0.0731%1.318028
6 Jun 202524,748.7025,003.0524,137.3024,076.2911−0.009%+1.028%0+1.0188%1.331455
9 Jun 202525,160.1025,103.2024,166.2024,078.9411+0.628%−0.226%0+0.4005%1.336788
  1. 5 June, the entry day. We were flat overnight, so the +0.288% gap from 4 June's close to 5 June's open is not ours. We earn only the intraday move: 24,750.90 / 24,691.20 − 1 = +0.2418%.
  2. Cost of buying = statutory 0.1187% + slippage 0.05% = 0.1687%. Net = 0.2418% − 0.1687% = +0.0731%. Equity 1.317066 × 1.000731 = 1.318028.
  3. 6 June. Long both overnight and intraday: (1 − 0.0000889) × (1 + 0.010277) − 1 = +1.0188%. Equity becomes 1.331455.
  4. 9 June. (1 + 0.006281)(1 − 0.002262) − 1 = +0.4005%. Equity 1.336788. Repeat for every day of the test, and the last value is your final equity multiple.

4.4 When you need stops and position sizing: the event-driven portfolio loop

Vectorised code is perfect for "in or out of one instrument". Once you have stops that trigger inside a day, sizing that depends on current equity, a cap on positions and many stocks competing for cash, it is clearer to loop through days. Our portfolio backtester (in course/scripts/ch12_lib.py) does this each day:

  1. Exits first. Exit signals from yesterday's close fill at today's open. For open positions, if today's open is already below the stop, we exit at the open (a gap through the stop, which you cannot avoid in real life). Otherwise, if today's low touches the stop, we exit at the stop price.
  2. Entries next. Yesterday's entry signals fill at today's open + slippage, strongest score first, while slots and cash last. Quantity = equity × risk% ÷ (fill − stop), capped at 20% of equity. If a gap opens the price at or below the planned stop, the trade is skipped.
  3. Mark to market at the close, record equity, then read today's signals for tomorrow.
  4. Every buy and sell pays delivery charges (including the ₹15.34 DP charge) and 0.1% slippage.
# the heart of the daily loop (simplified from course/scripts/ch12_lib.py)
for d in dates:
    for sym in open_positions:                       # 1. exits
        bar = data[sym].loc[d]
        if sym in exit_tomorrow:            sell(sym, bar.open)
        elif bar.open <= pos[sym].stop:     sell(sym, bar.open)          # gapped through the stop
        elif bar.low  <= pos[sym].stop:     sell(sym, pos[sym].stop)
    for score, sym, stop in sorted(buy_tomorrow, reverse=True):          # 2. entries
        if len(open_positions) < MAX_POS:
            fill = data[sym].loc[d].open * (1 + SLIP)
            qty  = min(int(equity * RISK / (fill - stop)), int(equity * 0.20 / fill))
            buy(sym, qty, fill, stop)
    equity = cash + sum(qty * close for each open position)         # 3. mark to market
    exit_tomorrow, buy_tomorrow = read_signals_at_close(d)          #    decide for tomorrow
In your framework

framework.strategies.donchian_trend, ma_crossover, trend_pullback and breakout_52w each turn a price DataFrame into entry/exit/stop columns. framework.backtest.run_backtest(signals_by_symbol, capital, risk_pct, max_positions, costs=True) runs this same daily loop and returns trades, equity and metrics. From the terminal: python -m framework.backtest_cli donchian_trend.

5. Performance metrics: formulas and a full worked example

Varsity's Module 9 builds an equity curve by scaling a portfolio to ₹100 at the start and compounding daily returns. That gives one line you can compare with a benchmark such as Nifty. Every chart in this chapter does the same thing. From the equity curve \(E_t\) and the trade list we compute the following.

MetricFormulaWhat it tells you
CAGR\(\left(E_{\text{end}}/E_{\text{start}}\right)^{1/\text{years}} - 1\)Compound annual growth rate: the single-rate summary of growth
Volatility\(\sigma_{\text{daily}}\sqrt{252}\)How bumpy the ride is, annualised
Sharpe ratio\(\dfrac{\overline{r - r_f/252}}{\sigma_{\text{daily}}}\sqrt{252}\)Return above the risk-free rate per unit of total volatility
Sortino ratio\(\dfrac{\overline{r - r_f/252}}{\sigma_{\text{down}}}\sqrt{252}\), with \(\sigma_{\text{down}}=\sqrt{\tfrac1n\sum \min(r-r_f/252,0)^2}\)Like Sharpe, but only downside volatility counts as risk
Drawdown, max drawdown\(DD_t = E_t / \max_{s\le t}E_s - 1\); MDD = \(\min_t DD_t\)Worst fall from a previous peak: the pain you must sit through
Calmar ratioCAGR / |MDD|Growth per unit of worst pain
Win ratewinning trades / all tradesHow often you are right (says nothing about how much)
Profit factorgross profit / |gross loss|Rupees won per rupee lost; must be > 1 after costs
Expectancy (R)\(W\cdot\overline{R}_{\text{win}} + (1-W)\cdot\overline{R}_{\text{loss}}\)Average profit per trade in units of initial risk (Chapter 11)
Average holdingmean (exit date − entry date)Tells you whether it is really a swing or a positional system, and its tax treatment
Exposure% of days with at least one position (and average % of capital invested)How much of the time your money is at risk; idle cash can earn interest

We use \(r_f\) = 5.5%, close to the 91-day Treasury-bill yield (about 5.52% in the RBI data on 1 Oct 2026), and 252 trading days a year.

Worked example · System B (55-day breakout) · 24 Jul 2017 – 30 Sep 2026

Every metric, plugged in by hand

  1. CAGR. ₹10,00,000 grew to ₹28,55,060, a multiple of 2.8551 over 9.1855 years. \(2.8551^{1/9.1855} - 1 = e^{1.0491/9.1855} - 1 = e^{0.11421} - 1 =\) 12.10%.
  2. Volatility. Daily SD of returns = 0.9911%. × √252 (= 15.875) = 15.73%.
  3. Sharpe. Mean daily return 0.0512%, minus the daily risk-free rate 5.5%/252 = 0.0218%, gives excess 0.0294%. 0.0294% / 0.9911% × 15.875 = 0.47.
  4. Sortino. Downside deviation of daily excess returns = 0.7256%. 0.0294% / 0.7256% × 15.875 = 0.64.
  5. Max drawdown. The equity curve's deepest fall was −33.46%, reached on 27 Mar 2023. Calmar = 12.10 / 33.46 = 0.36.
  6. Win rate. 129 winners out of 424 trades = 30.4%. Seven trades in ten lost money.
  7. Profit factor. Gross profit ₹53,42,631 / gross loss ₹34,87,571 = 1.53.
  8. Expectancy. Average winner +3.60R, average loser −1.17R (losers exceed −1R because of gaps through stops and costs). 0.304 × 3.60 + 0.696 × (−1.17) = 1.094 − 0.814 = +0.28R per trade.
  9. Holding period. Average 55 calendar days, median 30. A positional system, and almost all gains are short-term capital gains (taxed at 20%; Chapter 13).
  10. Exposure. At least one position on 99.0% of days; on average 83.5% of capital invested, across about 7 positions.
Reading metrics together

No single number is enough. A 30% win rate is fine with +3.6R winners. A 90% win rate is a disaster if the one loss is −20R. A high CAGR with a −60% drawdown is a system almost nobody can actually follow. Look at CAGR, maximum drawdown, the number of trades (is the sample large enough?) and expectancy after costs, and always compare against the simplest alternative: buying and holding the index.

Expected returns (Varsity Module 9)

Varsity also turns daily mean and SD into a one-year range: annual return ≈ daily mean × 252, annual SD ≈ daily SD × √252, then expect roughly mean ± 1 SD about two years in three. For System B: 0.0512% × 252 ≈ 12.9% and 0.9911% × √252 ≈ 15.7%, so a "normal" year would land anywhere between about −2.8% and +28.6%. Real systems have fat tails, so treat even this wide range as optimistic.

6. System A: 50/200 moving-average crossover on Nifty

Hypothesis. Big bear markets start slowly. If you are out of the index while its 50-day average is below its 200-day average (the "death cross"), you should miss the worst of a crash and be back in for the next bull run (the "golden cross"). Investors find this attractive because it promises to cut drawdowns.

Rules. Universe: Nifty 50 index (in practice a Nifty ETF or futures). Entry: SMA50 closes above SMA200 → buy at the next open. Exit: SMA50 closes below SMA200 → sell at the next open. Size: 100% of capital. Costs: delivery charges (0.1187% buy / 0.1037% sell) + 0.05% slippage per side. Test period: 24 Jul 2017 (first day with a 200-day average in our file) to 30 Sep 2026.

Nifty with 50 and 200 day SMAs, shaded periods when the system was long, equity curves of crossover vs buy and hold, and drawdowns
System A on Nifty. Top: price, the two SMAs, and shaded periods in the market (green arrows = buy, red = sell). Middle: ₹100 invested. Bottom: drawdowns. Source: NSE data via Yahoo Finance; computations in ch12_figures.py.
#Buy (next open)PriceSell (next open)PriceReturnDays
125 Jul 201710,010.5519 Nov 201810,731.25+7.2%482
218 Mar 201911,473.8516 Sep 201910,994.85−4.2%182
313 Nov 201911,908.3020 Mar 20208,284.45−30.4%128
424 Aug 202011,412.0019 Apr 202217,258.95+51.2%603
512 Sep 202217,890.8511 Apr 202317,704.80−1.0%211
62 Jun 202318,550.8520 Jan 202523,290.40+25.6%598
75 Jun 202524,691.2018 Mar 202623,632.90−4.3%286
2.5%
Crossover CAGR
9.3%
Buy & hold CAGR
−42.9%
Crossover max DD
−38.4%
Buy & hold max DD

What happened. The system was meant to reduce drawdowns, and in this period it increased them. In the March 2020 COVID crash the averages crossed only on 19 Mar 2020, so the system sold at 8,284, close to the bottom. It bought back on 24 Aug 2020 at 11,412, which is 37.8% higher than the exit. A slow average needs months of falling prices before it turns, so it is late at both ends of a fast V-shaped crash. Over 9.19 years the system turned ₹100 into ₹125.66 (CAGR 2.52%), while buy-and-hold turned it into ₹226.97 (CAGR 9.33%). It was invested 74% of the time. Even if the idle cash had earned 5.5%, CAGR rises only to 3.95%.

Metric50/200 crossoverNifty buy & hold
CAGR2.52%9.33%
Volatility12.6%16.6%
Sharpe (rf 5.5%)−0.170.30
Sortino−0.220.41
Max drawdown−42.85% (24 Sep 2020)−38.44% (23 Mar 2020)
Calmar0.060.24
Trades / win rate / profit factor7 / 42.9% / 2.101 / – / –
Average holding356 dayswhole period
Exposure74%100%

A longer test. Over 2009–2026 (17.7 years of Nifty history downloaded separately) the crossover made 4.85% a year against 11.96% for buy-and-hold, with 12 trades and the same −42.85% maximum drawdown. The result does not depend on the start date.

Status today (educational). On 30 Sep 2026 Nifty closed at 22,620.45, the 50-day SMA was 23,901.58 and the 200-day SMA 24,372.85. The system has been in cash since its 18 Mar 2026 exit.

Lesson

A profit factor of 2.10 sounds great, but with only 7 trades it means almost nothing. The comparison that matters is with the simplest alternative. A rule whose main job is to reduce risk has to be tested on the risk it claims to reduce, and here it failed. For a long-term index investor in this period, staying invested beat timing with the averages. (Varsity's Module 2 introduces moving-average crossovers as a trend-following tool. This test shows why they are better as a filter than as a stand-alone index-timing system.)

7. System B: 55-day breakout on 37 stocks with 1% risk

Hypothesis. Stocks that make a new 55-day high are often in the early part of a long trend: institutions accumulate in stages, and a breakout forces short sellers and sceptics to buy. Most breakouts fail, so we cut failures quickly and let the few big trends run. This is the logic of the 1980s "Turtle" traders' channel-breakout system.

The tool is a Donchian channel: two lines drawn on the chart, the highest high of the last N sessions (upper line) and the lowest low of the last M sessions (lower line). A close above the upper line is a breakout; a close below the lower line ends the trend. With N = 250 (about a year of sessions) the same rule becomes the popular 52-week-high breakout. It trades less often and enters later in a trend. The framework's breakout_52w strategy lets you test that version on the same universe.

Rules.

Equity curve of the 55 day breakout system versus Nifty and an equal-weight portfolio of the same 37 stocks, with drawdown panel
System B equity (₹10 lakh start) vs Nifty and vs simply holding all 37 stocks in equal weight. Bottom: drawdown. The maximum was −33.5% on 27 Mar 2023.
12.1%
CAGR (Nifty 9.3%)
−33.5%
Max drawdown
424
Trades
30.4%
Win rate
+0.28R
Expectancy
₹2.28 L
Total costs paid
Real trade · TRENT · Nov 2023 – Jul 2024

Sizing a breakout with 1% risk (the system's best trade)

TRENT candlestick chart with 55 day high and 20 day low channels, buy on 2 Nov 2023 and sell on 22 Jul 2024
TRENT: the breakout, the trailing 20-day-low exit line and the untouched initial stop. Prices are on the course framework's adjusted scale: the raw NSE quotes then were 1.5× higher, because the data file was missing a share adjustment that the framework corrects.
  1. Signal (1 Nov 2023 close). Close 1,465.10 > previous 55-day high 1,456.67 → breakout. ATR(20) = 36.97, so stop = 1,465.10 − 2 × 36.97 = 1,391.15.
  2. Equity that evening = ₹20,87,590. Risk budget = 1% = ₹20,876.
  3. Fill (2 Nov open). 1,473.33 + 0.1% slippage = 1,474.81. Risk per share = 1,474.81 − 1,391.15 = 83.65. Quantity = 20,876 / 83.65 = 249.5 → 249 shares (₹3.67 lakh, 17.6% of equity, under the 20% cap).
  4. Exit. The stock trended for 8 months. The first close below the 20-day low came on 19 Jul 2024, and the position was sold at the 22 Jul open: 3,403.26 after slippage.
  5. Result. (3,403.26 − 1,474.81) × 249 − costs ₹1,330 = +₹4,78,854 = +22.99R (R = 83.65 × 249 = ₹20,829).
Real trade · MARUTI · Feb 2025

A typical failed breakout

MARUTI candlestick chart: breakout buy on 3 Feb 2025 followed by a slide to the stop on 21 Feb 2025
MARUTI: a breakout on 1 Feb 2025 that reversed. The stop did its job.
  1. Signal 1 Feb 2025: close 12,916.50 > 55-day high 12,390. ATR(20) = 282.51 → stop 12,916.50 − 565.02 = 12,351.48.
  2. Equity ₹34,63,066 → risk ₹34,631. Fill 3 Feb at 12,990 + 0.1% = 13,002.99. Risk/share 651.51 → 53 shares.
  3. The stock slid back, and the stop was touched on 21 Feb: fill 12,351.48 − 0.1% = 12,339.12.
  4. P&L = (12,339.12 − 13,002.99) × 53 − ₹1,512 costs = −₹36,697 = −1.06R. A small, planned loss. Seven out of ten trades end like this.

Where the money came from

Year (exit)Net P&L (₹)Year (exit)Net P&L (₹)
2017 (Aug–Dec)−77,1252022−1,49,694
2018+1,86,7262023−20,222
2019−43,3912024+10,38,699
2020+80,2352025+2,08,813
2021+10,64,5292026 (to Sep)−4,33,509
Before you get excited: three checks in Section 10

(1) Split the test in two and the first half (Jul 2017 – Dec 2021) made 22.8% a year, while the second half (Jan 2022 – Sep 2026) made 1.25% a year, below Nifty's 5.4%. (2) Forty versions with random entries and the same exits and sizing averaged 12.8% CAGR, and 60% of them beat the breakout entry. (3) Adding five once-popular stocks that later collapsed cuts CAGR from 12.1% to 8.3%. Keep these in mind before you trust the 12.1%.

In your framework

python -m framework.backtest_cli donchian_trend runs the framework's version on the same universe. Its result is close but not identical: CAGR 13.8%, max DD −27.0%, 363 trades, expectancy +0.50R (3 Oct 2017 – 30 Sep 2026). The framework holds at most 8 positions instead of 10, starts trading in October 2017 after a one-year indicator warm-up, uses dividend-adjusted prices and ranks signals by relative strength against Nifty. That two reasonable implementations differ by almost 2 percentage points a year is itself a lesson: small implementation details move results, so never treat a backtest number as precise.

8. System C: RSI(2) mean reversion in an uptrend

Hypothesis. Within a long-term uptrend, sharp 2–3 day sell-offs are often overreactions (forced selling, noise), and prices bounce back within days. So buy short-term weakness in long-term strength. This is the opposite of System B: high win rate, small gains, short holds.

RSI(2) is the ordinary Wilder RSI from Chapter 4, computed over just 2 days instead of 14. With so short a window it swings to extremes after only two down days. A reading below 10 means the last couple of sessions were almost all losses.

Rules. Universe: the same 37 stocks. Trend filter: close > 200-day SMA. Entry: RSI(2) < 10 → buy at the next open + 0.1% slippage. Exit: close > 5-day SMA → sell at the next open; or a time stop after 10 sessions. No price stop (a common design for this style; see the loss example). Size: 10% of equity per position, up to 10 positions; the most oversold signal first. Costs: full delivery charges.

Real trade · ETERNAL · Jul 2025

The textbook case, helped by an earnings gap

ETERNAL chart with 200 and 5 day SMAs and RSI(2) panel; buy on 21 Jul 2025 and sell on 22 Jul 2025
ETERNAL: oversold above its 200-day average, then a results-day gap.
  1. Signal 18 Jul 2025: close 257.20 > SMA200 247.82 (uptrend), RSI(2) = 8.47 < 10.
  2. Bought 232 shares on 21 Jul at 257.00 + 0.1% = 257.26. The stock jumped on its quarterly results that day and closed at 271.70, above the 5-day SMA → exit signal.
  3. Sold 22 Jul at the open, 293.00 − 0.1% = 292.71. Net P&L +₹8,068 (+13.8%, costs ₹157).
  4. Honest note: most of that gain was the results announcement, not "mean reversion". One lucky trade like this can flatter a small sample.
Real trade · HCLTECH · Feb 2026

Why "no stop" plus next-open fills can hurt

HCLTECH chart: buy on 3 Feb 2026 at a gap-up open, followed by a slide to the 10-day time stop on 17 Feb 2026
HCLTECH: the entry filled at a 6% gap-up open, then the stock fell for two weeks.
  1. Signal 2 Feb 2026: close 1,676.80 > SMA200 1,580.32, RSI(2) = 6.89.
  2. On 3 Feb the whole market gapped up (Nifty opened at 26,308 after a 25,088 close). HCLTECH opened at 1,780, 6.2% above the signal close. The rules still buy: 32 shares at 1,781.78. The "oversold" premise was gone before the order filled.
  3. IT stocks then sold off hard. The exit condition never triggered, so the 10-session time stop sold on 17 Feb at 1,462.54: −17.9%, −₹10,347.
  4. Lesson: write rules for gaps ("skip if the open is more than X% above the signal close"). Then test that rule out-of-sample, not on the trade that inspired it.
Left: RSI(2) equity curves before and after costs. Right: bar chart of CAGR under rising cost and slippage assumptions for RSI below 5 and below 10
Left: the same 2,032 trades before costs (green) and after delivery charges plus 0.1% slippage (red). Right: CAGR as friction rises, for two entry thresholds.
Version (Jul 2017 – Sep 2026)TradesWin rateProfit factorAvg trade before chargesCAGR
RSI(2) < 10, no costs, no slippage2,03263.6%1.11+0.17%+2.87%
RSI(2) < 10, delivery costs, 0 slippage2,03259.1%0.91+0.17%−2.41%
RSI(2) < 10, costs + 0.1% slippage (base case)2,03255.5%0.77−0.03%−6.64%
RSI(2) < 5, no costs1,22165.0%1.25+0.32%+3.87%
RSI(2) < 5, costs + 0.05% slippage1,22158.9%0.96+0.21%−0.64%

Diagnosis. The edge is real but tiny. The average trade earned +0.17% before costs, while a round trip of delivery charges on a ~₹1 lakh position costs about 0.24% (0.2225% plus the ₹15.34 DP charge), before any slippage. After costs and 0.1% slippage, ₹10 lakh shrank to ₹5.32 lakh (−6.6% a year, max DD −47.5%), and ₹3.64 lakh went in costs. Charges alone, with zero slippage, already turned the result negative (−2.41%). A 63.6% win rate made it feel like a good system the whole time.

Lesson

High-turnover, small-edge systems are the most sensitive to costs. Delivery trades in India pay 0.1% STT on both sides. Before building a short-holding system, estimate its average gross trade, and if it is not comfortably above ~0.5%, stop there. (Many RSI(2) results published for US markets assume near-zero costs; Indian delivery costs are different.)

9. The three systems side by side

MetricA: 50/200 on NiftyB: 55-day breakoutC: RSI(2)Nifty buy & hold
PeriodJul 2017 – Sep 2026Jul 2017 – Sep 2026Jul 2017 – Sep 2026Jul 2017 – Sep 2026
CAGR2.52%12.10%−6.64%9.33%
Volatility12.6%15.7%11.4%16.6%
Sharpe−0.170.47−1.040.30
Sortino−0.220.64−1.270.41
Max drawdown−42.9%−33.5%−47.5%−38.4%
Calmar0.060.36−0.140.24
Trades74242,032–
Win rate42.9%30.4%55.5%–
Profit factor2.101.530.77–
Avg holding356 days55 days6 days–
Exposure (days with a position)74%99%84% (avg 36% invested)100%

Notice three things. The system with the lowest win rate (B) was the only one that made money. The system with the best profit factor (A) had too few trades for that number to mean anything. And B beat Nifty while holding 37 hand-picked stocks; Section 10.2 shows how much of that is the hand-picking.

10. Backtesting pitfalls, each demonstrated on real data

A backtest can only be wrong in one direction that matters: it can make a bad system look good. Each of the following mistakes makes results look better than reality, and each is shown below on our data.

10.1 Look-ahead bias: using tomorrow's information today

Look-ahead means a decision in the backtest uses information that was not available yet. The commonest version is one missing .shift(1). Take a simple rule, "be long Nifty when it closes above its 20-day SMA", and code it three ways.

sig = (close > close.rolling(20).mean()).astype(int)
ret = close.pct_change()

buggy   = (1 + sig * ret).cumprod()            # WRONG: today's close decides today's return
shifted = (1 + sig.shift(1) * ret).cumprod()   # assumes a fill at the signal day's close, no costs
correct = vector_backtest(df, sig, costs)      # fill at next open, pay costs (Section 4)
Log-scale equity curves of the buggy look-ahead version compounding at 46 percent a year versus the correct version at under 3 percent
Same rule, three implementations, 24 Jul 2017 – 30 Sep 2026, log scale.
ImplementationCAGRSharpeMax DD
Buggy (no shift)45.8%3.41−6.7%
Fill at signal-day close, no costs8.2%0.31−16.2%
Correct: next open + costs + slippage2.75%−0.22−22.7%

Why the bug is so profitable. If today's close is above the 20-day average, today was probably an up day. The buggy code "knew" the day would be up and was long for it. The result is a ₹100 → ₹3,193 equity curve, a Sharpe ratio of 3.4 and a maximum drawdown of only 6.7%. If a simple rule shows a Sharpe ratio above 2 and an almost straight equity curve, assume a bug first. The honest version made 2.75% a year after its 115 round trips, which is below buy-and-hold.

Other ways look-ahead sneaks in: computing the mean and SD of a ratio over the whole period and then trading inside that period (Varsity's Method 1 example does this; see Section 11), using today's high to set today's entry, using quarterly results before their announcement date, and using index membership as of today.

10.2 Survivorship bias: our universe is today's winners

The 37 stocks in this course were chosen in 2026, as today's large, well-known names. Many are there because they did well (TRENT, BEL, HAL, DIXON, ADANIENT). A trader in July 2017 could not have known that list. Popular names in 2017 included YES Bank, Zee Entertainment, Indiabulls Housing Finance (now Sammaan Capital), Vodafone Idea and IndusInd Bank. We downloaded their history and added them back.

Left: bar chart of price returns from July 2017 for the course universe in blue and five collapsed stocks in red. Right: CAGR of equal-weight and breakout system with and without the fallen stocks
Left: price return 24 Jul 2017 – 30 Sep 2026. Right: the effect on returns of adding back five losers.

Even five names, out of the dozens that left the Nifty since 2017, wipe out the system's edge. The proper fix is a point-in-time universe: on each test date, use only the stocks that were in the index (or met your liquidity rules) on that date, including ones that were later delisted. NSE publishes historical index changes. Until you have that data, treat any stock-picking backtest on a modern list as an upper bound.

10.3 Overfitting: the best parameters of the past

Overfitting means tuning rules until they match past noise. We tested 100 fast/slow SMA combinations on Nifty (fast 10–100, slow 50–300), choosing parameters on 2009–2017 (in-sample) and then checking them on 2018–Sep 2026 (out-of-sample), with costs.

Two heatmaps of Sharpe ratio by fast and slow SMA: mostly green in-sample 2009-2017, mostly red out-of-sample 2018-2026; the in-sample best 70/100 is marked
Sharpe ratio for every parameter pair. Star = in-sample best (70/100). Circle = the textbook 50/200.

What the heatmap shows: a robust idea gives a broad plateau of decent results. A fragile one gives isolated bright spots that move around from period to period. Here the in-sample "best" was a bright spot.

10.4 Out-of-sample splits and walk-forward testing

Two habits protect you. (1) Hold out data. Develop on one period, then test once on a period you never looked at. System B's halves (fixed rules, nothing re-tuned) are a sobering example:

PeriodSystem B CAGRMax DDTradesExpectancyNifty CAGR
24 Jul 2017 – 31 Dec 202122.8%−24.0%204+0.47R13.3%
1 Jan 2022 – 30 Sep 20261.25%−24.6%237+0.06R5.4%

(2) Walk forward. If you insist on optimising, do it the way you would have to live: each January, pick the best SMA pair using only the previous 3 years, trade it for the next 12 months, and repeat.

Walk-forward equity curve versus fixed 50/200 and Nifty buy and hold from 2012 to 2026, with the parameters chosen each year
Walk-forward 2012–2026. Bottom: the parameters chosen each year jump around (70/100 in 2012, 10/50 in 2019, 20/50 in 2021–23, 10/75 in 2025).

Result: walk-forward 2.55% CAGR vs fixed 50/200 4.14% vs buy-and-hold 11.14% (Jan 2012 – Sep 2026). Re-optimising made things worse. The "best" parameters kept changing, a sign that there was no stable edge to find.

10.5 Transaction costs and slippage

Seen already: System B lost about 2 points of CAGR to friction (14.1% → 12.1%). System C went from +2.9% to −6.6%. A good habit is to double your slippage assumption and check that the system still works. If doubling slippage kills it, the edge is too thin to trade. For futures, remember STT is now 0.05% on the sell side, and an index-futures position that you hold for a year pays two extra legs of charges at every monthly roll (sell the expiring contract, buy the next one), on top of the original buy and the final sell.

10.6 Small samples: luck looks like skill

How many trades do you need? We resampled System B's 424 trade results (in R) 10,000 times.

Histogram of bootstrapped expectancy for 30-trade samples, wide and centred near zero, versus full 424-trade resamples, narrow around 0.28R
Bootstrapped expectancy: grey = random 30-trade samples, blue = 424-trade resamples.

10.7 A null model: is the entry rule doing the work?

Replace the breakout entry with a coin flip (each stock has a 2% chance of a "signal" each day). Keep the same universe, stops, 20-day-low exits, 1% sizing and costs, and run it 40 times with different random seeds.

Histogram of 40 random-entry CAGRs from about 5 to 20 percent, with the breakout system at 12.1 percent and Nifty at 9.3 percent
Random entries vs breakout entries, same everything else.

The random-entry runs averaged 12.8% CAGR (median 13.4%, range 5.2% to 20.4%), and 60% of them beat the breakout version. So System B's results come from the exits and sizing (cut losers, let winners trend) applied to a universe that went up, not from clever entries. That is useful to know: it tells you where to spend your research time.

10.8 Other traps worth a checklist line

11. Pair trading, Method 1: ratio and density curve

11.1 The logic: two roads that run side by side

Varsity explains pair trading with a picture. Think of a highway and the service road next to it. They climb, dip and cross rivers together. At one spot the service road bends around a tree, then comes back alongside the highway. Two businesses that are truly alike, such as two large private banks with the same products, customers and regulator, are like those roads. Industry-wide news (an RBI rate move, a credit cycle) moves both. A local event (one bank's results, a senior executive leaving, a burst of speculation) can push one away from the other for a while. That is the tree. If the relationship holds, the gap should close, and you profit by buying the relatively cheap stock and selling the relatively expensive one.

Not "market neutral"

Varsity stresses this point: being long one stock and short another is relative value or statistical arbitrage, not a market-neutral position. The two stocks can and do move differently, and that difference is exactly your risk. Pair trades need short positions held overnight, which in India means stock futures, with their lot sizes, margins and expiries (Chapter 9). Only stocks in the F&O segment can be paired this way.

11.2 The vocabulary (Varsity PTM1, chapters 1–2)

11.3 The density curve

Treat the ratio's history as roughly normal with mean \(\mu\) and SD \(\sigma\). Varsity uses Excel's NORM.DIST(ratio, mean, SD, TRUE), which is the cumulative normal probability:

Density value (cumulative normal of the ratio) \[ D_t = \Phi\!\left(\frac{\text{ratio}_t - \mu}{\sigma}\right) \]
\(\text{ratio}_t\)
Today's closing price of stock A divided by today's closing price of stock B.
\(\mu\)
The average ("mean") of the ratio over the look-back window (Varsity uses the whole history in the sheet).
\(\sigma\)
The standard deviation (SD) of the ratio over the same window: how far it typically strays from its mean.
\(\frac{\text{ratio}_t - \mu}{\sigma}\)
The z-score: how many SDs today's ratio is above (positive) or below (negative) its mean.
\(\Phi\)
The cumulative normal function (Excel's NORM.DIST(…, TRUE)). It turns a z-score into a number between 0 and 1: the share of a normal bell curve that lies below that z-score.
\(D_t\)
The density value, between 0 and 1. 0.5 means the ratio is at its mean.

How it works: subtract the mean from today's ratio, divide by the SD to get the z-score, then feed it to \(\Phi\). Read the result like this: \(D\) = 0.16 ≈ −1 SD, 0.025 ≈ −2 SD, 0.003 ≈ −2.75 SD; 0.84 ≈ +1 SD, 0.975 ≈ +2 SD, 0.997 ≈ +2.75 SD. Varsity calls 0.003 and 0.997 the "3 SD" levels, borrowing the rule that 99.7% of a normal curve lies within ±3 SD. That 99.7% is split between both tails, though, so each tail beyond ±3 SD holds only 0.13% (\(\Phi(-3) = 0.0013\)). We keep Varsity's 0.003 and 0.997 levels, but call them what they are: about ±2.75 SD. A value near 0 means A is unusually cheap against B; near 1, unusually expensive. The ICICI/HDFC example below works it through.

TradeTrigger (density)MeaningExitStop
Long the ratio (buy A, sell B)between 0.025 and 0.003ratio about 2 to 2.75 SD below its meanas it moves back towards the mean (Varsity's example closed near 0.25)0.003 or lower (about −2.75 SD or further)
Short the ratio (sell A, buy B)between 0.975 and 0.997ratio about 2 to 2.75 SD above its meanback below 0.975 or nearer the mean0.997 or higher (about +2.75 SD or further)

Varsity also asks for rupee neutrality: roughly equal rupee value on each side, so you are betting on the ratio, not on the market. Method 1 comes from Mark Whistler's book Trading Pairs. Varsity presents it as the simpler foundation before Method 2.

Real data · ICICIBANK / HDFCBANK · Oct 2024 – Sep 2026

Method 1 on the classic Indian pair

The ICICI to HDFC price ratio with mean and plus/minus 1, 2, 3 SD bands, and the density value below, showing the ratio trending up through all bands in 2026
Ratio = ICICIBANK / HDFCBANK, with bands computed from the whole 2-year window (Varsity's method), and below, the density value computed both ways. Prices are bonus/split-adjusted (HDFC Bank issued a 1:1 bonus in Aug 2025).
  1. Qualify the pair. Over 494 sessions (1 Oct 2024 – 30 Sep 2026), the correlation of daily returns was 0.515, well below Varsity's ~0.75 guideline. (Correlation of price levels was 0.423; use returns, because price levels of any two trending stocks look correlated.) Varsity calls the correlation number "sacred" and wants it above 0.75, though it relaxed this for its own Axis/ICICI example. Treat 0.515 as a warning.
  2. Pair stats. Mean ratio 1.5416, median 1.4772, SD 0.16358. Bands: −3 SD 1.0509, −2 SD 1.2144, −1 SD 1.3780, +1 SD 1.7052, +2 SD 1.8688, +3 SD 2.0323. Mean above median warns that the distribution is skewed, not normal.
  3. Signal. On 20 Jul 2026 the ratio was 1,460.20 / 777.60 = 1.8778. \(z = (1.8778 - 1.5416)/0.16358 = 2.055\), \(D = \Phi(2.055) = 0.980\), inside 0.975–0.997 → short the ratio: sell ICICI, buy HDFC.
  4. Trade (₹5 lakh per leg at the next open, 21 Jul): short 340.1 ICICI at 1,470.00, long 650.1 HDFC at 769.15.
  5. Stop. The ratio kept rising. On 27 Aug 2026 it hit 2.0295 (z = 2.98, density 0.9986, past the 0.997 stop). Exit at the 28 Aug open: ICICI 1,435.00, HDFC 710.85. ICICI short made +₹11,905, HDFC long lost −₹37,899: net −₹25,994 before costs. The ratio peaked at 2.0508 on 31 Aug.

Why it failed, and a hidden look-ahead. The ratio did not swing around a stable mean. It trended: it averaged 1.462 in Oct–Dec 2024 and 1.892 in Jul–Sep 2026, because HDFC Bank fell well behind ICICI. The relationship changed, which is the main risk in any pair trade. There is a second problem. The mean and SD came from the whole two-year window, including the months after the signal. Using only the 250 days before each date (the dashed line in the lower panel), the density on 20 Jul 2026 was already 0.998, beyond the stop level, so an honest trader would never have entered. Method 1 as usually shown in spreadsheets has look-ahead built in. Always compute the statistics on a rolling past window.

12. Pair trading, Method 2: regression, error ratio, ADF, z-score

Varsity's second method (its author's own improvement) asks a sharper question: not "do these move together?", but "is there a stable linear relationship whose errors keep snapping back?"

12.1 Straight line and linear regression

The pair equation \[ y_t = \beta\,x_t + c + \varepsilon_t \]
\(t\)
A trading day in the window (the last 200 sessions).
\(y_t\)
The closing price of the dependent stock (the "Y" stock) on day \(t\), in ₹.
\(x_t\)
The closing price of the independent stock (the "X" stock) on the same day, in ₹.
\(\beta\)
The slope ("beta"): how many rupees \(y\) tends to move for a ₹1 move in \(x\). It is also the hedge ratio: \(\beta\) shares of X against each share of Y.
\(c\)
The intercept, in ₹: the part of \(y\)'s price the line does not explain through \(x\).
\(\varepsilon_t\)
The residual ("epsilon"), in ₹: actual \(y_t\) minus the predicted \(\beta x_t + c\). Positive means Y is above where the line says it should be.

How it works: plot the 200 pairs of closes as dots and draw the straight line that fits them best. "Best" means least squares: the line that makes the sum of the squared residuals as small as possible (Excel's regression tool or SLOPE/INTERCEPT does this). The line gives \(\beta\) and \(c\); each day's gap from the line is that day's residual.

Example (TCS on INFY, Section 12.3): TCS = 1.8235 × INFY + 211.31. On 30 Sep 2026 INFY closed at 994.10, so the line predicts 1.8235 × 994.10 + 211.31 = 2,024.05. TCS actually closed at 2,050.60, a residual of +26.55.

From a trader's view, Varsity reads each term like this. \(\beta x\) is hedged: hold 1 share of y against \(\beta\) shares of x. \(c\) is not hedged, so we want it small relative to the price. \(\varepsilon\) is what we trade: if it is stationary (mean-reverting), its extremes are opportunities.

12.2 The error ratio: which stock is Y?

Regress both ways and compare. Varsity's "error ratio" (its own term, not a standard statistic):

Error ratio \[ \text{Error ratio} = \frac{\text{SE of the intercept}}{\text{SE of the regression (residual SD)}} \]
SE of the intercept
The standard error of \(c\), in ₹: how uncertain the estimate of the intercept is. A regression output (Excel's LINEST or Data Analysis tool) prints it next to the intercept.
SE of the regression
The typical size of the residuals, in ₹: roughly their standard deviation. Excel labels it "Standard Error" in the regression statistics.

How it works: run the regression twice, once with each stock as Y. For each run, divide the intercept's SE by the regression's SE. Both are in rupees, so the ratio has no units. Pick the direction with the lower error ratio: the intercept (the part you cannot hedge) is estimated more precisely there.

Real data · HDFCBANK vs ICICIBANK · last 200 sessions, 9 Dec 2025 – 30 Sep 2026

Error ratio both ways

DirectionSlope βInterceptSE interceptSE regressionError ratio
Y = HDFCBANK, X = ICICIBANK0.2048541.11130.7090.16130.70 / 90.16 = 1.4497
Y = ICICIBANK, X = HDFCBANK0.10881,265.0142.1765.7142.17 / 65.71 = 0.6418

So Y = ICICI, X = HDFC. But look at the fit: R² = 0.022. HDFC's price explains about 2% of ICICI's price movement in this window, and the intercept (1,265) is 95.7% of ICICI's price. The "hedge" hedges almost nothing. The ADF test (next) agrees: p = 0.59. In 2026 these two banks were not a pair in the statistical sense, whatever their businesses look like.

What the error ratio really measures

Do the algebra and the Y prices cancel out. The standard error of the intercept is the regression's standard error times \(\sqrt{1/n + \bar x^2/\sum (x-\bar x)^2}\), so

\[ \text{Error ratio} = \sqrt{\frac1n + \frac{\bar x^{\,2}}{\sum (x_t-\bar x)^2}} = \frac{1}{\sqrt n}\sqrt{1 + \frac{1}{\text{CV}_x^{\,2}}},\qquad \text{CV}_x = \frac{\text{SD of }x}{\text{mean of }x} \]

It depends only on the X stock. It picks as X whichever stock's price varied more relative to its own level over the window. In this window ICICI's price varied by only 4.9% of its mean (CV 0.049, error ratio 1.4497), while HDFC's varied by 11.1% (CV 0.111, error ratio 0.6418). That is why TCS/INFY and WIPRO/INFY in the pair sheet below both show exactly 0.4388: it is INFY's number. The error ratio is a reasonable tie-breaker for choosing Y and X. It says nothing about whether the pair is any good. R² and, above all, the ADF test do that job.

12.3 Stationarity and the ADF test

Varsity's check: the residuals must be stationary. That means (1) a stable mean, (2) a stable SD, and (3) little autocorrelation. If they are, the two stocks are cointegrated. The Augmented Dickey-Fuller (ADF) test formalises it. Regress the change in the residual on its previous level:

ADF regression (1 lag) \[ \Delta\varepsilon_t = a + \gamma\,\varepsilon_{t-1} + b\,\Delta\varepsilon_{t-1} + u_t \] \[ t_{\text{ADF}} = \frac{\hat\gamma}{\text{SE}(\hat\gamma)} \]
\(\varepsilon_t\)
The residual from the pair equation on day \(t\), in ₹. \(\varepsilon_{t-1}\) is yesterday's residual.
\(\Delta\)
"Change in" ("delta"). \(\Delta\varepsilon_t = \varepsilon_t - \varepsilon_{t-1}\): how much the residual moved today. \(\Delta\varepsilon_{t-1}\) is yesterday's move (the "1 lag").
\(a\)
A constant fitted by the regression.
\(\gamma\)
("gamma") How today's change depends on yesterday's level. Negative means a high residual tends to fall and a low one to rise.
\(b\)
How today's change depends on yesterday's change. It soaks up short-term momentum so it does not distort \(\gamma\).
\(u_t\)
What is left unexplained on day \(t\) (noise).
\(\hat\gamma,\ \text{SE}(\hat\gamma)\)
The hat means "estimated from the data". \(\hat\gamma\) is the fitted value of \(\gamma\); \(\text{SE}(\hat\gamma)\) is its standard error, how uncertain that estimate is.
\(t_{\text{ADF}}\)
The ADF t-statistic: the estimate divided by its uncertainty. No units.
  1. From the 200 residuals, build three columns: today's change, yesterday's level and yesterday's change.
  2. Regress today's change on the other two. This gives \(\hat\gamma\) and its SE.
  3. Divide to get \(t_{\text{ADF}}\). If \(\gamma\) is clearly negative, a big residual tends to be followed by a move back: mean reversion. A more negative t-statistic is stronger evidence.
  4. Turn the t-statistic into a p-value (the table below gives the critical values). Varsity accepts a pair if the ADF p-value is below 0.05. The TCS/INFY example below has t = −2.90 and fails.

We coded the ADF test in plain numpy (no statsmodels), and found the p-value by simulation. One detail matters a lot. When the residuals come from a regression you just fitted, the regression has already made them look as stationary as possible. So the usual Dickey-Fuller critical values are too lenient. We simulated 4,000 pairs of independent random walks (n = 200), regressed one on the other, and recorded the ADF t-statistic of the residuals. This is the Engle-Granger approach.

Critical value (n = 200)1%5%10%
Plain series (Dickey-Fuller)−3.51−2.86−2.54
Regression residuals (Engle-Granger, used here)−3.79−3.31−3.03

A pair needs an ADF t-stat below about −3.3 to pass at 5%. Using −2.86 would accept many pairs that are just two random walks that happened to wander together.

Real data · TCS (Y) vs INFY (X) · 9 Dec 2025 – 30 Sep 2026

A beautiful fit that still fails the test

Scatter of TCS against INFY prices with the regression line and R squared 0.957, and the residual z-score over time between about minus 2.8 and plus 2.1
Left: regression of TCS on INFY. Right: residual ÷ standard error (the z-score) over the same 200 sessions.
  1. Error ratio. TCS on INFY: 35.18 / 80.16 = 0.4388. INFY on TCS: 0.4678. So Y = TCS.
  2. Equation. TCS = 1.8235 × INFY + 211.31, R² = 0.957 (both fell sharply together in 2026).
  3. Today's residual. 30 Sep 2026: predicted TCS = 1.8235 × 994.10 + 211.31 = 2,024.05; actual 2,050.60; residual +26.55; z = 26.55 / 80.16 = +0.33. No signal.
  4. Stationarity by eye. Residual means over the three thirds of the window: +10.3, −48.0, +38.3 (TCS price means: 2,979, 2,346, 2,242). Residual SDs: 69.5, 84.3, 57.1. Lag-1 autocorrelation of residuals 0.92 (prices 0.99). Much more stable than prices, but far from "no autocorrelation".
  5. ADF. t = −2.90, p = 0.14 → fails at 5%. A 0.957 R² does not mean cointegration. Two stocks falling together give a great fit without any mean-reverting link.

12.4 The pair data sheet and trade rules

Varsity's workflow: for every candidate pair, regress both ways, take the lower error ratio, run ADF on its residuals, and keep only pairs with p < 0.05. For those, track the z-score daily. Varsity's live-example rules:

Method 2 trade rules (Varsity) \[ z_t = \frac{\varepsilon_t}{\text{SE of regression}} \]
\(\varepsilon_t\)
Today's residual, in ₹: actual Y price minus the price the regression line predicts.
SE of regression
The typical size of a residual over the 200-session window, in ₹ (from the same regression).
\(z_t\)
The z-score: today's residual measured in "typical residuals". −2.5 means Y is 2.5 typical gaps below where the line puts it.

How it works: each day, compute the residual and divide by the SE. Long the pair (buy Y, sell β×X) at \(z \le -2.5\); target \(z \ge -1\); stop \(z \le -3\). Short the pair (sell Y, buy β×X) at \(z \ge +2.5\); target \(z \le +1\); stop \(z \ge +3\). Earlier chapters use ±2 for entry. Varsity prefers ±2.5. Skip pairs with negative β.

Example (TCS/INFY, 30 Sep 2026): residual +26.55, SE 80.16, so \(z = 26.55 / 80.16 = +0.33\). Nowhere near ±2.5: no trade.

Pair sheet, 30 Sep 2026 (Y / X)Error ratioβR²ADF tADF pz today
WIPRO / INFY0.43880.14130.917−3.330.048−0.58
TCS / INFY0.43881.82350.957−2.900.136+0.33
AXISBANK / SBIN1.15470.48090.320−2.640.224−0.44
WIPRO / TCS0.46780.07630.928−2.440.302−0.94
HCLTECH / TCS0.46780.45610.892−1.950.556+1.43
ICICIBANK / HDFCBANK0.64180.10880.022−1.880.588−0.31

Only 1 of the 14 bank and IT pairs passes today, and only barely, with no signal. That is normal. Cointegration comes and goes.

Sizing · beta-neutral lots with 30 Sep 2026 lot sizes

Turning β into futures lots (Varsity's live examples)

  1. WIPRO (Y) / INFY (X), β = 0.1413: each WIPRO share needs 0.1413 INFY shares. One WIPRO lot = 3,000 shares → 0.1413 × 3,000 = 424 INFY shares. INFY lot = 400, so 1 lot each, 6% short of perfect neutrality.
  2. TCS (Y) / INFY (X), β = 1.8235: 1 TCS lot (225) × 1.8235 = 410 INFY → 1 INFY lot (400).
  3. When lots do not match, Varsity scales up (several lots of one against one of the other) or tops up the gap in the cash market. If β is negative, a hedge with futures is generally not possible.
  4. Costs (futures, current rates): 1 lot HDFCBANK at ₹715.55 × 650 = ₹4,65,108 costs ₹43.49 to buy and ₹266.75 to sell (mostly the 0.05% STT). A pair round trip pays four legs, about 0.07% of each leg's value plus slippage.

12.5 An honest rolling backtest of Method 2

We ran Varsity's rules day by day on 14 bank and IT pairs from Jul 2017 to Sep 2026, using only the past 200 sessions at each date. Each day: choose Y/X by error ratio, require β > 0 and ADF p < 0.05, enter at the next open when |z| crosses the trigger (but is still below 3), keep the entry-day regression fixed, exit at |z| ≤ 1 (target) or |z| ≥ 3 (stop) or after 40 sessions. ₹5 lakh on the Y leg, β-weighted X leg, futures charges plus 0.05% slippage.

Horizontal bar chart of net P&L by pair for the rolling Method 2 backtest with entry at z of 2, ranging from plus 1.4 lakh for HDFC/AXIS to minus 1.75 lakh for ICICI/KOTAK
Net P&L by pair, entry |z| ≥ 2. Total over 9 years: +₹20,482.
Entry triggerTradesWin rateExits (stop / target / time)CostsNet P&L
|z| ≥ 2.5 (Varsity's preference)4522.2%36 / 9 / 0₹60,387−₹1,87,282
|z| ≥ 2.010840.7%63 / 43 / 2₹1,44,105+₹20,482
Verdict on pair trading

Pair trading is a real technique and teaches excellent statistical habits. On liquid Indian large caps, with daily data, current STT and honest rolling statistics, it was roughly break-even over nine years at best. Use the method to think about relative value (for example, "is this bank cheap relative to its peer?"). If you trade it, use small size, check the ADF test every day, and stop as soon as the relationship breaks.

13. Calendar spreads as a system

Varsity's calendar-spread chapter treats the price difference between two futures expiries of the same stock like a pair. Because both legs are the same underlying, this one really is close to market neutral. Steps: compute the daily difference (next month − current month), its mean and SD; sell the spread (sell the farther contract, buy the nearer) when it rises above mean + 1 SD; buy the spread when it falls below mean − 1 SD; close as it returns towards the mean. Varsity's example used SBIN.

Real data · SBIN futures · Aug–Sep 2026 bhavcopy

Varsity's SBIN calendar spread, 2026 edition

The data comes from NSE's bhavcopy, the official end-of-day file listing every contract's open, high, low, close, settlement price, open interest and lot size.

SBIN next-month minus near-month futures spread over August and September 2026 with mean and plus/minus 1 SD lines and spikes on expiry days
SBIN next-month minus near-month futures close (₹), 42 sessions. Dotted lines mark the near-month expiries.
  1. Mean spread ₹5.71, SD ₹0.94 → sell above 6.65, buy below 4.77. Lot 750, so each ₹1 of spread is ₹750 per lot.
  2. Five signals (exit at the first close back inside the band, within the same expiry pair): gross ₹525, ₹75, ₹1,725, ₹300, ₹450 = ₹3,075.
  3. Each round trip has four legs of futures charges, about ₹935–₹1,021 per lot here. STT alone on one sell leg is 0.05% × ₹967.20 × 750 = ₹362.70. Total costs ₹4,902 → net −₹1,827. Break-even move ≈ ₹1.31 of spread, larger than one SD.
  4. Spread spikes on expiry days (₹7.9 on 25 Aug, ₹8.3 on 29 Sep) are mechanical: the near contract converges to spot. Avoid signals there.
  5. Honest caveat: the mean and SD use the whole 42-day window (look-ahead), and 42 days is a tiny sample.

After the April 2026 STT increase on futures, small mispricings like these rarely cover four legs of costs for a retail trader.

14. Momentum portfolios

Varsity's final system is a portfolio, not a trade. Momentum is the tendency of recent winners to keep outperforming for a while. Varsity's steps: (1) define a tracking universe (it suggests 150–200 stocks, e.g. from the BSE 500, for a 12–15 stock portfolio); (2) get clean, corporate-action-adjusted prices; (3) compute 12-month returns; (4) rank them; (5) buy the top names in equal weight; (6) rebalance monthly, selling names that drop out and buying new entrants.

Momentum portfolio equity curve versus Nifty 2017 to 2026, plus a dotted line for the version that includes five collapsed stocks, and drawdowns
Top 10 of our 37 stocks by 12-month return, rebalanced at each month-end close and traded at the next open, equal weight, delivery charges + 0.1% slippage. Dotted: the same rules on a universe that includes the five fallen stocks.
24.9%
CAGR after costs (Oct 2017 – Sep 2026)
9.7%
Nifty CAGR, same period
−36.0%
Max drawdown (Mar 2020)
₹2.00 L
Charges paid on ₹10 L start
Calendar year201820192020202120222023202420252026 YTD
Momentum (net)+10.2%+11.1%+27.9%+98.7%+4.9%+52.7%+60.3%−1.4%−14.2%
Nifty+3.2%+12.0%+14.9%+24.1%+4.3%+20.0%+8.8%+10.5%−13.4%

Gross of costs the CAGR was 26.3%. With the five fallen names in the universe it was still 22.9%, because momentum rarely holds stocks while they collapse. But the universe still contains the 2026 survivors, which were picked partly because they had enormous runs (TRENT, BEL, HAL, DIXON, ADANIENT). A momentum system run on a 2017 list of 150–200 stocks, many of them later losers, would show a much smaller number. Note also 2025 (−1.4% vs Nifty +10.5%): momentum suffers when leadership changes suddenly. The top 10 by 12-month return at the 31 Aug 2026 rebalance were TITAN (+41.1%), POLYCAB, SBIN, NESTLEIND, SUNPHARMA, AXISBANK, ADANIENT, BAJFINANCE, TATASTEEL and LT (+12.0%). That is a historical illustration of how the rule works, not a recommendation.

Which of these systems fits a swing / positional / investor profile?

Of everything tested here, the two that held up best were both slow, diversified, rules-based portfolio approaches: trend-following with cut losses (System B) and monthly momentum. Even those need a point-in-time universe test before you trust them. Chapter 17 builds your personal framework from these pieces.

15. Try it yourself

These snippets need a copy of the course repository (its data/ folder holds the price files) and Python with pandas and numpy; the setup steps are in the repository's framework/README.md. Save a snippet as a .py file and run it from the repository's root folder (for example .venv/bin/python snippet.py), because the file paths below are relative to that folder. Each one reproduces a number from this chapter.

15.1 A complete honest backtest in 20 lines (System A)

import pandas as pd, numpy as np
D = "data/ohlc/"                      # relative to the repository root
df = pd.read_csv(D + "IDX_NSEI.csv", index_col=0, parse_dates=True)
c, o = df["Close"], df["Open"]

# 1. signal decided at the CLOSE of day t (1 = want to be long)
sig = (c.rolling(50).mean() > c.rolling(200).mean()).astype(int)
# 2. position held from the OPEN of day t+1  (the shift is what prevents look-ahead)
pos = sig.shift(1).fillna(0)
prev = pos.shift(1).fillna(0)                     # position carried overnight into today
# 3. returns: overnight (close->open) belongs to yesterday's position, intraday (open->close) to today's
r_co = (o / c.shift(1) - 1).fillna(0)
r_oc = c / o - 1
gross = (1 + prev * r_co) * (1 + pos * r_oc) - 1
# 4. costs as a fraction of value traded: buy 0.1187% / sell 0.1037% (STT, stamp, exchange, SEBI, GST) + 0.05% slippage
buy, sell = (pos - prev).clip(lower=0), (prev - pos).clip(lower=0)
net = gross - buy * (0.001187 + 0.0005) - sell * (0.001037 + 0.0005)
# 5. equity curve and two headline metrics
eq = (1 + net.loc["2017-07-24":]).cumprod()
yrs = (eq.index[-1] - eq.index[0]).days / 365.25
print(f"CAGR {eq.iloc[-1] ** (1 / yrs) - 1:.2%}   max drawdown {(eq / eq.cummax() - 1).min():.2%}")

Expected output: CAGR 2.52% max drawdown -42.85%. Now change 50/200 to 20/50, or set the costs to zero, and see how much each choice matters.

15.2 See look-ahead bias with your own eyes

import pandas as pd
D = "data/ohlc/"                      # relative to the repository root
c = pd.read_csv(D + "IDX_NSEI.csv", index_col=0, parse_dates=True)["Close"].loc["2017-01-01":]
sig = (c > c.rolling(20).mean()).astype(int)
r = c.pct_change()
buggy = (1 + sig * r).loc["2017-07-24":].cumprod()             # WRONG: today's signal earns today's return
right = (1 + sig.shift(1) * r).loc["2017-07-24":].cumprod()    # signal known at close, earns from tomorrow
for name, eq in [("buggy", buggy), ("shifted", right)]:
    yrs = (eq.index[-1] - eq.index[0]).days / 365.25
    print(f"{name:8s} CAGR {eq.iloc[-1] ** (1 / yrs) - 1:6.1%}")

Expected: the buggy version shows roughly 46% a year, the shifted one roughly 8%. One missing .shift(1) is worth almost 38 percentage points of imaginary return.

15.3 Regression, error ratio and an ADF t-statistic in numpy (Method 2)

import numpy as np, pandas as pd
D = "data/ohlc/"                      # relative to the repository root
def close(sym):
    d = pd.read_csv(D + f"{sym}.csv", index_col=0, parse_dates=True)
    return d.loc[~((d.Volume == 0) & (d.High == d.Low)), "Close"]   # drop holiday filler bars
px = pd.DataFrame({s: close(s) for s in ["TCS", "INFY"]}).dropna().iloc[-200:]
y, x = px["TCS"].values, px["INFY"].values
X = np.column_stack([np.ones(len(x)), x])
(c, m), *_ = np.linalg.lstsq(X, y, rcond=None)          # y = m*x + c
e = y - (m * x + c)                                     # residuals
se = np.sqrt(e @ e / (len(y) - 2))                      # standard error of the regression
se_c = np.sqrt(se**2 * np.linalg.inv(X.T @ X)[0, 0])    # standard error of the intercept
print(f"slope {m:.4f}  intercept {c:.2f}  std err {se:.2f}  error ratio {se_c / se:.4f}  z today {e[-1] / se:+.2f}")
# ADF t-statistic on the residuals (1 lag): regress de_t on e_{t-1} and de_{t-1}
de = np.diff(e)
Z = np.column_stack([np.ones(len(de) - 1), e[1:-1], de[:-1]])
b, *_ = np.linalg.lstsq(Z, de[1:], rcond=None)
u = de[1:] - Z @ b
cov = (u @ u / (len(u) - 3)) * np.linalg.inv(Z.T @ Z)
print(f"ADF t-stat {b[1] / np.sqrt(cov[1, 1]):.2f}  (pair needs roughly < -3.3 for 5% significance)")

Expected: slope 1.8235, intercept 211.31, std err 80.16, error ratio 0.4388, z +0.33, ADF t −2.90. Swap in WIPRO and INFY and you should get an ADF t of about −3.33.

15.4 Exercises with the full chapter code

16. From backtest to live: paper trading and the go-live checklist

Before you call a backtest "done"

Paper trading (1–3 months)

Going live, small

Risk reminder

A backtest is a simulation of the past, not a forecast. Markets change, costs and taxes change (STT on futures more than doubled in 2026), and your own discipline in a drawdown is the variable no backtest captures. Never risk money you cannot afford to lose. Be especially careful with leveraged futures, where losses can exceed your margin.

17. Key takeaways

Key takeaways

18. Quiz and exercises

Q1. A friend's system: "Buy strong stocks when the market looks good; sell when it looks weak." Which of the five components is properly defined?
None. "Strong", "looks good" and "looks weak" are not measurable, so the universe, entry and exit cannot be coded or tested, and there is no position size or portfolio rule at all. A system version: universe = Nifty 100; market filter = Nifty close > 200-DMA; entry = close at a 55-day high; exit = close below the 20-day low or 2×ATR stop; size = 1% risk; max 10 positions.
Q2. Nifty closes above its 20-day SMA on Monday. In an honest backtest, which day's return does that signal earn first, and from what price?
It earns from Tuesday's open: Tuesday's intraday move (open → close), plus Tuesday-to-Wednesday overnight and so on. Monday's return, and even Monday-close-to-Tuesday-open, belong to whatever position you held before. Earning Monday's return from a Monday-close signal is look-ahead bias; in our test it inflated CAGR from 2.75% to 45.8%.
Q3. A system turned ₹10 lakh into ₹28.55 lakh in 9.1855 years with a max drawdown of −33.46%. Compute CAGR and Calmar.
CAGR = 2.855^(1/9.1855) − 1 = e^(ln 2.855 / 9.1855) − 1 = e^(1.0491/9.1855) − 1 = e^0.11421 − 1 = 12.10%. Calmar = 12.10 / 33.46 = 0.36.
Q4. Win rate 30.4%, average win +3.60R, average loss −1.17R. What is the expectancy, and is a 30% win rate a problem?
E = 0.304 × 3.60 + 0.696 × (−1.17) = 1.094 − 0.814 ≈ +0.28R per trade. Positive, so the low win rate is not a problem in itself. The real problem is psychological: you must sit through long losing streaks (20 in a row in System B) and drawdowns (Monte Carlo median about −36R).
Q5. RSI(2) had a 63.6% win rate and +0.17% average trade before costs, yet lost 6.6% a year after costs. Explain using the cost of a ₹1 lakh delivery round trip.
A round trip costs ₹118.74 + ₹119.08 = ₹237.82 ≈ 0.24% of ₹1 lakh, before slippage. Add 0.1% slippage per side (0.2%) and the friction is about 0.44% per trade, more than double the 0.17% gross edge. Each trade loses about 0.27% on average, and 2,032 trades compound that into a large loss. High-turnover, small-edge systems need an average gross trade comfortably above the round-trip cost.
Q6. Why is a backtest of System B on our 37 stocks biased, and in which direction? What fixes it?
Survivorship (and selection) bias: the list was chosen in 2026 and includes stocks that did very well since 2017, and excludes ones that collapsed. That biases results upwards. Adding just five fallen names (YESBANK, ZEEL, SAMMAANCAP, IDEA, INDUSINDBK) cut CAGR from 12.1% to 8.3%. The fix is a point-in-time universe: on each date use the index constituents (or liquidity-screened stocks) as of that date, including later delistings.
Q7. Out of 100 SMA pairs, 70/100 had the best Sharpe in 2009–2017 (0.49). Out-of-sample it ranked 76th. What does this tell you, and what would you do instead?
The in-sample winner was mostly fitting noise (overfitting); across all pairs, in-sample and out-of-sample Sharpe were even negatively correlated (−0.32). Instead: choose parameters from theory before testing, prefer regions where neighbours also work (plateaus), keep an untouched out-of-sample period, use walk-forward testing if you optimise at all, and accept that many ideas have no edge. Here walk-forward (2.55%) did no better than the fixed rule (4.14%), and both lost to buy-and-hold (11.14%).
Q8. The ICICI/HDFC ratio is 1.8778, the 2-year mean is 1.5416 and SD 0.16358. Compute the density value. Which Method 1 trade does it trigger, and what is wrong with using these statistics?
z = (1.8778 − 1.5416) / 0.16358 = 2.055; density = Φ(2.055) ≈ 0.980, between 0.975 and 0.997 → short the ratio (sell ICICI, buy HDFC), stop at 0.997. Problems: the mean and SD use data after the signal date (look-ahead). With only the past 250 days the density was already 0.998, beyond the stop. The return correlation was only 0.515, and the ratio was trending, not mean-reverting. The real trade lost about ₹26,000 before costs on ₹5 lakh per leg.
Q9. Regressing A on B gives SE(intercept) 42.17 and SE 65.71. B on A gives 130.70 and 90.16. Which is Y? The residual ADF t-stat is −1.88. Trade?
Error ratios: A on B = 42.17/65.71 = 0.642; B on A = 130.70/90.16 = 1.450. Lower is better → A is Y, B is X. ADF t = −1.88 is far above the Engle-Granger 5% critical value of about −3.31 (p ≈ 0.59), so the residuals are not shown to be stationary: do not trade, whatever the z-score says. (These are the real ICICIBANK/HDFCBANK numbers for Dec 2025 – Sep 2026.)
Q10. WIPRO (Y) vs INFY (X) has β = 0.1413. WIPRO's lot is 3,000 and INFY's is 400. How many lots of each for a beta-neutral long-pair trade, and which side do you buy?
Shares of X needed = 0.1413 × 3,000 = 424. One INFY lot (400) is closest. So buy 1 lot WIPRO futures and sell 1 lot INFY futures (long pair = buy Y, sell X). It is slightly under-hedged (400 vs 424). Only enter if ADF p < 0.05 and z ≤ −2.5 on the day, with target z ≥ −1 and stop z ≤ −3.
Q11. Your paper-traded breakout system has lost money on its first 30 trades (average −0.5R), while the backtest says +0.28R. Should you abandon it?
Not on that evidence alone. In System B's history, 36.5% of random 30-trade samples had zero or negative expectancy, and the real first 30 trades averaged −0.51R. Check instead that (a) your fills and signals match the backtest code for the same dates (no bugs, slippage as assumed), (b) the result is within the backtest's range, and (c) your pre-set kill switch has not been hit. Abandon if execution differs from the test, or the kill switch triggers, not because of a normal-looking losing run.
Q12. Exercise: the SBIN calendar spread's mean is ₹5.71 and SD ₹0.94, and a round trip costs about ₹980 per lot (lot 750). What is the minimum spread move to break even, and what does that imply for a ±1 SD system?
Break-even = ₹980 / 750 ≈ ₹1.31 of spread, which is more than one SD (₹0.94). A system that enters at +1 SD and exits near the mean expects a move of about 1 SD, which does not cover costs. Our 5 trades made ₹3,075 gross and lost ₹1,827 net. You would need wider entry bands (fewer trades) or much cheaper execution.

19. Sources

Sources: Zerodha Varsity Module 10 "Trading Systems": module page; 1. What to expect?; 2. Pair Trading logic; 3. PTM1 C1 – Tracking Pairs; 4. PTM1 C2 – Pair stats; 5. PTM1 C3 – Pre trade setup; 6. PTM1 C4 – The Density Curve; 7. PTM1 C5 – The Pair Trade; 8. PTM2 C1 – Straight line equation; 9. PTM2 C2 – Linear Regression; 10. PTM2 C3 – The Error Ratio; 11. PTM2 C4 – The ADF test; 12. Trade Identification; 13. Live Example 1; 14. Live Example 2; 15. Calendar Spreads; 16. Momentum Portfolios. Varsity Module 9 "Risk Management & Trading Psychology": Equity Curve, Expected Returns. Charges: Zerodha charges page (checked 2 Oct 2026: delivery STT 0.1% buy and sell, futures STT 0.05% sell, NSE 0.00307% / 0.00183%, SEBI ₹10/crore, stamp 0.015% / 0.002%, DP ₹15.34). Risk-free rate ≈ 5.5% (91-day T-bill ≈ 5.52%, RBI, Oct 2026, as recorded in framework/config.py). Data: daily OHLCV for 37 NSE stocks and Nifty 50, NSE via Yahoo Finance (data/ohlc/, to 30 Sep 2026); Nifty history from 2007 and five delisted/collapsed stocks (YESBANK, ZEEL, SAMMAANCAP, IDEA, INDUSINDBK) downloaded with yfinance to data/extra/ch12_*_yf.csv; SBIN futures and F&O lot sizes from the NSE F&O UDiFF bhavcopy, Aug–Sep 2026 (data/fno/). Code (in the course repository): course/scripts/ch12_figures.py, course/scripts/ch12_lib.py; all numbers in data/extra/ch12_results.json. Engle-Granger critical values were simulated (4,000 pairs of independent random walks, n = 200).

← PreviousRisk Management & Trading PsychologyNext →Costs & Taxation in India