Ledger & Candle
Chapter 17 · Capstone

Build Your Own Framework

Reading time ~75 min · Data as of 30 Sep 2026 · Based on Zerodha Varsity Module 10 (Trading Systems), Module 9 (Risk Management & Trading Psychology) and Module 3 (Fundamental Analysis: equity research and the investment checklist)

1. What you'll learn and why it matters

The previous sixteen chapters gave you separate skills: reading charts, measuring trends, reading a balance sheet, valuing a business, pricing a future, sizing a position, paying the right tax, choosing a fund and setting an allocation. Skills on their own do not make anyone a consistent trader or investor. What does is a process: written rules for what you buy, how much, when you get out and how you check yourself, followed the same way on good days and bad days.

This capstone turns the course into that process. It uses the actual Python code in the course repository's framework/ folder (the toolkit built alongside this course; its README.md explains how to install it) and the actual market data up to 30 September 2026. Every output shown below is real: the scanner's output for 30 Sep 2026, the scorecards for TCS, Pidilite, HDFC Bank and ITC, the backtests, and a learner's own strategy idea that we design, test and, in the end, reject.

By the end you will be able to:

Why this matters for you

Retail traders rarely fail for lack of indicators. They fail because they size too big, change rules after every loss, skip the journal and trade products (like F&O) before they have shown an edge anywhere. SEBI's July 2025 study found that 91% of individual equity F&O traders made a net loss in FY2024-25, with aggregate net losses of about Rs 1.06 lakh crore. A framework is how you avoid becoming part of that number: it makes the risky decisions in advance, while you are calm.

Not advice

Every stock named here is a historical, educational example computed from data to 30 Sep 2026. Nothing in this chapter is a recommendation to buy or sell anything. Allocation percentages are teaching examples; your own numbers depend on your income, obligations, tax situation and temperament.

2. What a trading system is, and what your framework contains

Varsity's Module 10 opens with a useful distinction. Many people trade on a hunch, a friend's tip, a TV guest or a broker's call. Even a sensible-sounding "systematic deduction" ("PSU banks jumped, so Bank Nifty overreacted") is still ad hoc if you cannot write it down as a repeatable procedure. A trading system is an approach you can define as a process: it takes inputs (prices, volumes, financial data), applies fixed logic, produces an output (a list of trades, a size, a stop), and you decide, or the system decides, whether to act. Varsity is also clear about two things. A system is not a money machine, because you design it, you feed it and you choose to follow it. And no system is complete until it has been backtested.

The framework/ package is exactly such a system, split into small modules that each correspond to a part of this course:

Course chapterWhat you learnedFramework codeHow you use it
1 Market foundationsNSE, settlement, corporate actionsconfig.py, data.load_ohlc (back-adjusts missed corporate actions)Clean data in, everything else depends on it
2-4 Charts, S/R, indicatorsCandles, trends, RSI, ATR, MAsindicators.py, patterns.pyBuilding blocks for every rule
5 Swing playbookRegime, RS, setups, GTTstrategies.py, scan.pypython -m framework.scan every evening
6-8 Fundamentals, ratios, valuationStatements, DuPont, Piotroski, DCFfundamentals.py, invest.pypython -m framework.invest SYMBOL
9-10 Futures, optionsCarry, OI, margins, hedgingfutures.py, run_futures_backtestHedge calculator, OI context, MTM ledger
11 Risk & psychologyPosition size, R, expectancy, ruinrisk.py, journal.pyEvery order's quantity; your honest scorecard
12 Systems & backtestingNo look-ahead, costs, robustnessbacktest.py, backtest_cli.pypython -m framework.backtest_cli NAME
13 Costs & taxSTT, stamp duty, STCG/LTCGcosts.py (rates only in config.py)Every backtest and journal entry is net of costs
14-16 Funds, fund tax, allocationNAV, TER, SIP and XIRR, fund tax classes, harvesting, rebalancingfunds.pypython -m framework.funds sip, tax, harvest, rebalance

Data flows in one direction, which is what keeps the system honest (no module can peek at the future):

 data.load_ohlc ──> indicators / patterns ──> strategies (entry, exit, stop, trail, score per bar)
                                                      │
                     risk.position_size  <────────────┤
                     costs.*_charges     <── backtest.run_backtest ──> trades, equity, metrics ──> reports/
                                                      │
                                     scan.py (today's bar only) ──> you ──> journal.py
 fundamentals.fetch_fundamentals ──> ratios / dupont / piotroski / scorecard / dcf ──> invest.py
 data.load_futures ──> futures.oi_buildup_table / rollover_pct / fair_value / beta_hedge
 funds.load_nav ──> sip_simulate / fund_capital_gains / harvest_plan / rebalance_backtest

Before relying on it, check that the code does what it claims. The framework ships with a test suite, including a test that fails if any strategy uses future data:

$ .venv/bin/python -m pytest framework/tests -q
129 passed in 1.15s

Every command in this chapter runs from the repository's root folder. .venv/bin/python is the project's own Python (a virtual environment created during setup); where a command just says python, use that same interpreter. Each command also accepts --help.

The three things a framework must answer

For every position you ever hold, your framework should already have answered: (1) why this instrument, now (the entry rule), (2) how much (the sizing rule) and (3) what makes me get out, both when wrong (stop) and when right (exit or trail). If any of the three is "I'll see", it is not a framework yet.

3. The three-bucket structure and capital allocation

You told this course you want swing trades, long-term positions and, slowly, futures. These are three different jobs with different edges, holding periods and risks, so they get three separate buckets with their own rules.

Three boxes: CORE long-term 60-80% of capital with fundamental rules; SWING satellite 20-40% with technical systems and 0.5-1% risk per trade; FUTURES 0% until graduated, with lot and stop sizing and 2x margin buffer
Figure 17.1 The three buckets. Arrows show the order in which you build them: core and swing first, futures only after you have proven a swing edge.

Why separate buckets? The logic

An educational allocation example

Before any bucket: keep an emergency fund (commonly six months of expenses) and adequate insurance outside the market. Only money you will not need for at least five years belongs in the core, and only money you can afford to lose part of belongs in the swing bucket. Suppose, after that, you have Rs 10 lakh to invest:

BucketStage 0-1 (first 6-12 months)Stage 2 (proven swing edge)Risk unit
Core (long-term)Rs 8.0 lakh (80%), staggered in over 6-12 monthsRs 7.0 lakh (70%)Max 10% of core per stock, 25-30% per sector
Swing satelliteRs 2.0 lakh (20%), real money only from Stage 1Rs 3.0 lakh (30%)0.25% (Stage 1) to 1% (Stage 2) of the swing bucket per trade
Futures00 until Stage 3Lot-and-stop sizing; at least 2x margin in cash

The swing bucket's risk per trade is a percent of that bucket's capital, not your total wealth. This is Varsity Module 9's point about defining equity capital before you size anything. Varsity describes three ways to measure it: core equity (starting capital minus money committed to open trades), total equity (cash plus open positions marked to market) and reduced total equity (core equity plus only the profit already locked in by trailing stops, as defined in Chapter 5). The framework's backtester uses total equity marked to market at the previous close. Whichever you choose, write it in your plan and use it every time.

Position size (the formula used everywhere in the framework) \[ \text{qty} = \left\lfloor \frac{C \times \frac{r}{100} \times m}{E - S} \right\rfloor \] \[ \text{capped so that}\quad \text{qty}\times E \le C \times \frac{p_{\max}}{100} \]
\(C\)
The capital of the bucket you are trading from (the swing bucket, not your total wealth), in Rs.
\(r\)
Risk per trade as a percent of \(C\): 0.25 at Stage 1, up to 1 at Stage 2. Dividing by 100 turns it into a fraction.
\(m\)
The regime multiplier from the scanner: 1 (Risk-on), 0.5 (Neutral) or 0.25 (Risk-off).
\(E\)
The entry price per share, in Rs. (In section 4, \(E\) means expectancy: a different quantity.)
\(S\)
The stop-loss price per share, in Rs. For a long trade \(E - S\) is the rupees you lose per share if the stop is hit.
\(\lfloor\ \ \rfloor\)
"Floor": round down to a whole number of shares (or whole lots for futures).
\(p_{\max}\)
The largest position allowed, as a percent of \(C\). The framework's backtester and scanner use 20.
qty
The number of shares to buy.
  1. Rupees you are willing to lose on this trade: \(C \times r/100 \times m\).
  2. Divide by the loss per share \((E - S)\) and round down. If the stop is hit, you lose about the planned amount.
  3. Check the cap: if qty × entry is more than \(p_{\max}\)% of the bucket (a very tight stop gives a huge quantity), cut qty to fit. If the answer is 0, the trade is too big for the account.

Example (illustrative entry and stop): Rs 2 lakh bucket, Stage 2 risk 1%, Risk-off regime (\(m\) = 0.25, as on 30 Sep 2026), entry Rs 1,000, stop Rs 950. Risk = 2,00,000 × 1/100 × 0.25 = Rs 500. Qty = 500 ÷ 50 = 10 shares, a Rs 10,000 position, well under the 20% cap of Rs 40,000.

Worked example · Rs 2 lakh swing bucket

How much is "1%"?

  1. Stage 1 risk: \(2{,}00{,}000 \times 0.25\% = \) Rs 500 per trade. At Stage 2 with 1%: Rs 2,000.
  2. Maximum open risk at Stage 2, if you cap it at 6%: \(2{,}00{,}000 \times 6\% = \) Rs 12,000, which is about six full-risk trades at once.
  3. Worst realistic streak: the best system in the framework's backtest (donchian_trend) had a longest run of 18 consecutive losing trades in nine years. At 1% each, that is roughly a 16.5% drawdown of the bucket (\(0.99^{18} = 0.835\)). At 5% per trade it would be about 60% (\(0.95^{18} = 0.397\)). That is the whole argument for small risk per trade.
Drawdowns are asymmetric

Varsity Module 9 shows why protecting capital comes first: after a loss of \(d\), you need a gain of \(\frac{1}{1-d} - 1\) to get back. Nifty's fall from its 2 Jan 2026 high of 26,328.55 to 22,620.45 on 30 Sep 2026 is -14.1%, which needs +16.4% to recover. The donchian_trend backtest's -27.0% drawdown needs +37.0%. A 50% loss needs +100%.

4. Graduating between stages: how many trades is enough?

The biggest beginner question is "when do I start with real money, and when do I increase size?" The honest answer is statistical: only when your sample of trades is large enough to show that your edge is probably real. Here is the ladder used in this course.

Four rising boxes: Stage 0 Paper, Stage 1 Small live at 0.25% risk, Stage 2 Normal at 0.5-1% risk, Stage 3 Futures 1 lot cash-backed, each with its exit criteria
Figure 17.2 The graduation ladder. Each stage has entry size and exit criteria; you can also be demoted.

The logic: expectancy is measured with error

First, the unit. An R-multiple is a trade's profit or loss divided by the rupees you planned to risk on it (quantity x (entry - stop)). If you risked Rs 1,000 and lost Rs 1,000, that trade was -1R; if you made Rs 2,500, it was +2.5R. Expectancy (Chapter 11) is the average R-multiple per trade. With few trades it is very noisy, because swing results are lopsided: many small losses and a few large wins. The uncertainty of your measured expectancy is its standard error:

Standard error of expectancy and the sample you need \[ SE = \frac{s_R}{\sqrt{n}} \qquad n_{\min} \approx \left( \frac{1.645 \times s_R}{E} \right)^2 \]
\(E\)
Your expectancy: the average R-multiple of all your trades so far, in R.
\(s_R\)
The standard deviation of your R-multiples, in R: how widely individual trade results spread around the average.
\(n\)
The number of trades in your sample.
\(\sqrt{n}\)
The square root of \(n\). Four times as many trades halves the error.
\(SE\)
The standard error of the expectancy, in R: roughly how far your measured average could be from the true one, just by luck.
\(1.645\)
The z-value for 95% one-sided confidence (equivalently, a two-sided 90% interval) under a normal curve.
\((\dots)^2\)
"Squared": the bracket multiplied by itself.
\(n_{\min}\)
The number of trades needed before \(E > 0\) is significant at about 95% confidence (one-sided).
  1. Compute \(E\) and \(s_R\) from your trade list (a spreadsheet's AVERAGE and STDEV on the R column).
  2. Divide \(s_R\) by \(\sqrt{n}\) to get \(SE\). Your true edge probably lies within \(E \pm 1.645 \times SE\).
  3. If that range still includes zero, you cannot yet tell your edge from luck. \(n_{\min}\) is the sample size at which the lower end just reaches zero: it is the \(n\) that makes \(1.645 \times SE = E\). Lopsided results (big \(s_R\)) or a thin edge (small \(E\)) need many more trades.
Real example · donchian_trend backtest · Oct 2017 to Sep 2026

How sure can we be about a +0.50R edge?

  1. Trades: \(n = 363\). Expectancy \(E = 0.498\)R. Standard deviation \(s_R = 3.595\)R (one trade made +29.26R; the median trade was -1.04R; only 31.1% of trades won).
  2. \(SE = 3.595 / \sqrt{363} = 0.189\)R, so a 90% interval is \(0.498 \pm 1.645 \times 0.189\) = 0.19R to 0.81R. Positive, but wide.
  3. Trades needed: \((1.645 \times 3.595 / 0.498)^2 = 141.0\), round up to 142 trades. A trend system with a few big winners needs well over a hundred trades before you can trust it.
Funnel chart: range of measured expectancy narrows as trade count grows from 10 to 300; at 10 trades 40.2% of samples show expectancy at or below zero, at 30 trades 23.8%, at 100 trades 6.8%, at 300 trades 0.4%
Figure 17.3 Resampling the 363 real donchian_trend trades: with 30 trades, almost one sample in four shows a losing system even though the full-sample edge is +0.50R. Source: framework/reports/donchian_trend_trades.csv.

This is why the ladder asks for at least 30 paper trades before small live money (to check you can follow the rules and that nothing is broken), and 100+ trades before full size. Thirty trades cannot prove an edge. They can reveal execution problems and rule-breaking.

Demotion: when the edge seems to have gone

Graduation runs both ways. Compare your live results with the backtest's distribution, and step down if live results fall outside it.

Real example · donchian_trend · trades entered 2025-2026 vs 2017-2024

A good system going through a very bad patch

  1. "Backtest" sample (entries before 2025): 275 trades, expectancy +0.75R.
  2. "Live" sample (entries from 2025 to Sep 2026): 88 trades, expectancy -0.29R, win rate 22.7%.
  3. Resample 88 trades from the older 275 many times (50,000 draws): the 5th percentile of the average is +0.10R, the 1st percentile -0.11R. A result of -0.29R or worse came up in only 0.14% of samples.
  4. Verdict: this is not ordinary bad luck. Either the market regime changed (2026 was a falling market, see Figure 17.6) or the edge has decayed. A plan rule such as "if live expectancy over 50+ trades falls below the backtest's 1st percentile, drop one stage and re-research" would have cut size during this period.

The journal can run this comparison for you (Section 10 shows its stats output for these 88 trades).

Kelly says "bet more"; ignore it

Kelly's criterion (Varsity Module 9) gives the growth-maximising fraction to risk: \(f^* = W - \frac{1-W}{R}\). For donchian_trend, \(W = 0.311\) and payoff \(R = 3.78\), so \(f^* = 0.311 - 0.689/3.78 = 0.129\), or 12.9% per trade. That assumes the win rate and payoff are known exactly. They are not (see the interval above, and the 2025-26 slump). Professionals use a small fraction of Kelly; at the learning stage, 0.25-1% is the right size. risk.kelly_fraction computes it so you can see how far above sane sizing it sits.

5. The long-term investing framework (core bucket)

Varsity's Module 3 describes equity research in three stages: understand the business (from annual reports, not search engines), apply a checklist, and value the business to find a price with a margin of safety. The framework turns this into a pipeline you can repeat for any company.

The simpler core: index funds and ETFs

You do not have to pick stocks to have a core. A few low-cost index funds or ETFs, one per asset class and in direct plans (Chapter 14), held at a written target allocation (Chapter 16) and reviewed each March, with new money and the ₹1.25 lakh exemption used before any taxable sale (Chapter 15 on harvesting, Chapter 16 on rebalancing), can be a complete core on its own. It can also sit beside the stock pipeline below, as the part of the core you do not research company by company. framework.funds does the arithmetic: sip for XIRR, tax and harvest for the yearly LTCG harvest, and rebalance to test a mix and a rebalancing rule on real NAVs.

StepWhat you doFramework supportRule of thumb (write your own)
1. UniversePick where you look: Nifty 100, your circle of competence, sectors with tailwindsconfig.UNIVERSE (37 stocks)Businesses you can explain in two sentences
2. ScreenRemove weak businesses quicklyquality_scorecard (0-100)Score at least 70; Piotroski F-score (a 0-9 count of improving fundamentals, Chapter 7) at least 5
3. Research checklistRead annual report, MD&A, related parties, contingent liabilities, concall notes, pledginginvest.py section 7 "Next:"Can you state the moat and what would break it?
4. Valuation and buy zoneDCF, reverse DCF, multiples vs historydcf, reverse_dcf, sensitivity grid; justified P/B for banksBuy only when the price implies growth well below what the business has delivered
5. Size and diversifyWeights, sectors, correlationPortfolio maths (Section 7)10-20 stocks, max 10% each at cost, 25-30% per sector, 3 tranches
6. Review cadenceAfter each quarterly result and annual reportinvest.py --refreshDid the thesis, score or implied growth change?
7. Sell rulesExit for reasons, not price movesScorecard history, reverse DCFThesis broken, governance issue, extreme valuation, much better use of money

Varsity's checklist, mapped to the scorecard

Varsity's ten-point investment checklist (Module 3, "Investment due diligence") looks for: high gross margins (evidence of a moat), revenue growth in line with profit growth, EPS growth consistent with profit growth (no dilution), low debt, sensible inventory, sales not propped up by receivables, positive operating cash flow, high ROE (with debt checked alongside), simple business lines and few subsidiaries. The framework's quality_scorecard automates the measurable items and leaves the judgement items to you:

Varsity checklist itemScorecard rule (weight)Still your job
High margins, moatROCE (return on capital employed) 5y avg (20), OPM (operating profit margin) stability (5)Name the moat: brand, network, cost, licence, switching cost
Revenue and profit growth in lineSales CAGR 5y (10), Profit CAGR 5y (10)Check one-offs in "other income"
EPS consistent, no dilutionPiotroski "no dilution" (inside Piotroski, 10)ESOP and QIP history
Low debtDebt/Equity (15)Off-balance-sheet guarantees
Positive operating cash flow, sales not just receivablesCFO/PAT 5y (15)Receivable days, inventory days trend
ROE above 25% (Varsity) / ROE qualityROE 5y avg (10), DuPont tableIs ROE from margins or from leverage?
Simple business, few subsidiariesNot automatedRead the subsidiary list and related-party notes
Management and governancePromoter holding change 8Q (5)Pledging, auditor changes, past capital allocation

Each rule scores linearly between a "zero" level and a "full marks" level. For example, ROCE earns 0 points at 10% and full 20 points at 25% or above. Banks get lender-specific rules (ROE and ROA instead of ROCE and debt/equity), because a bank's debt is its raw material.

6. Running invest.py on real companies

Here are four real runs on data fetched from screener.in in early October 2026 (prices in the screener header are as of the fetch; the "price context" line uses our local data to 30 Sep 2026). Read them as a checklist for your own judgement, not as verdicts.

TCS: high quality, modest expectations, falling price

$ .venv/bin/python -m framework.invest TCS
=== TCS - Tata Consultancy Services Ltd  (consolidated, Rs crore) ===
    Market Cap: 750,973.00  |  Current Price: 2,075.00  |  High: 3,350.00  |  Low: 1,976.00  |  Stock P/E: 14.00  |  Book Value: 296.00  |  Dividend Yield: 3.08  |  ROCE: 63.00  |  ROE: 51.80  |  Face Value: 1.00
    Source: https://www.screener.in/company/TCS/consolidated/
...
4) QUALITY SCORECARD: 88.1/100  (grade A)
                rule  value  weight  zero_at  full_at  points
       ROCE 5y avg %  60.61      20     10.0     25.0    20.0
        ROE 5y avg %  48.71      10      8.0     20.0    10.0
     Sales CAGR 5y %  10.22      10      0.0     15.0     6.8
    Profit CAGR 5y %   8.72      10      0.0     15.0     5.8
         Debt/Equity   0.11      15      1.0      0.1    14.9
          CFO/PAT 5y   1.01      15      0.5      0.9    15.0
   OPM stdev 5y (pp)   0.57       5      8.0      2.0     5.0
         Piotroski F   5.00      10      0.0      9.0     5.6
Promoter chg 8Q (pp)   0.00       5     -5.0      0.0     5.0
   (points scale linearly between zero_at and full_at; missing rules are excluded and the total rescaled)

5) VALUATION
   Price Rs 2,075.00 | Mcap Rs 750,973 cr | P/E 14.0 | P/B 7.01 | Earnings yield 7.14% | FCF yield 5.98%
   DCF base case: FCF0 = Rs 44,898 cr (3-yr avg), growth 10.2% for 10 yrs, discount 12%, terminal 5%, net debt Rs 11,283 cr
   => intrinsic value Rs 2,691/share (+30% vs price); terminal value is 58% of it
   Reverse DCF: today's price implies 6.7% FCF growth for 10 years
   Sensitivity (Rs/share):
            g=6%   g=10%   g=14%
   r=11%  2348.0  3185.0  4314.0
   r=12%  2001.0  2691.0  3617.0
   r=13%  1742.0  2322.0  3098.0
   Margin of safety rule of thumb: buy only below ~70% of a CONSERVATIVE intrinsic value.

6) PRICE CONTEXT (local data to 2026-09-30)
   Close 2,050.60 | vs 200-DMA -18.7% | from 52w high -38.8% | 1y return -33.3% | 1y RS vs Nifty 0.74 | worst 5y drawdown -56%

7) SUMMARY
   Business quality looks high (88.1/100). Piotroski 5/9.
   Market implies 6.7% FCF growth vs ~10.4% average sales growth over 5 years -> expectations look modest.
   Trend: below the 200-DMA. Long-term investors can stagger buys (SIP-style) instead of waiting for a perfect entry.
   Next: read the annual report (MD&A, related-party transactions, contingent liabilities), concall notes, and check promoter pledging on the exchange site.

How to read it, section by section:

Worked calculation · TCS DCF from the framework

From free cash flow to Rs 2,691 per share

  1. Base: FCF\(_0\) = Rs 44,898.3 cr (average of the last 3 years), \(g\) = 10.22%, \(r\) = 12%, \(g_T\) = 5%.
  2. Year 1: FCF\(_1 = 44{,}898.3 \times 1.1022 = \) Rs 49,485 cr; present value \(49{,}485 / 1.12 = \) Rs 44,183 cr.
  3. Year 10: FCF\(_{10}\) = Rs 1,18,767 cr; PV = Rs 38,240 cr. Sum of the ten PVs = Rs 4,11,480 cr.
  4. Terminal value at year 10: \(\text{TV} = \frac{1{,}18{,}767 \times 1.05}{0.12 - 0.05} = \) Rs 17,81,508 cr; PV = \(17{,}81{,}508 / 1.12^{10} = \) Rs 5,73,598 cr.
  5. Enterprise value = 4,11,480 + 5,73,598 = Rs 9,85,078 cr. Minus "net debt" Rs 11,283 cr = equity value Rs 9,73,795 cr. Note that the framework subtracts total borrowings and does not add back cash and investments (TCS's balance sheet shows Rs 33,988 cr of investments alone), so for cash-rich companies its value is on the conservative side. Adding surplus cash is a judgement call you can make by hand.
  6. Per share = \(9{,}73{,}795 / 361.91\) cr shares = Rs 2,691.

Pidilite: the best quality score, the most demanding price

$ .venv/bin/python -m framework.invest PIDILITIND        (sections 5-7)
5) VALUATION
   Price Rs 1,464.00 | Mcap Rs 148,516 cr | P/E 56.1 | P/B 13.81 | Earnings yield 1.78% | FCF yield 1.40%
   DCF base case: FCF0 = Rs 2,083 cr (3-yr avg), growth 14.9% for 10 yrs, discount 12%, terminal 5%, net debt Rs 417 cr
   => intrinsic value Rs 630/share (-57% vs price); terminal value is 63% of it
   Reverse DCF: today's price implies 26.5% FCF growth for 10 years
   Sensitivity (Rs/share):
          g=11%  g=15%   g=19%
   r=11%  556.0  753.0  1016.0
   r=12%  469.0  630.0   845.0
   r=13%  405.0  539.0   718.0
   Margin of safety rule of thumb: buy only below ~70% of a CONSERVATIVE intrinsic value.

6) PRICE CONTEXT (local data to 2026-09-30)
   Close 1,465.10 | vs 200-DMA -2.3% | from 52w high -14.2% | 1y return -3.5% | 1y RS vs Nifty 1.07 | worst 5y drawdown -27%

7) SUMMARY
   Business quality looks high (96.3/100). Piotroski 7/9.
   Market implies 26.5% FCF growth vs ~15.4% average sales growth over 5 years -> price already assumes faster growth than history.

Pidilite scores 96.3/100 with a Piotroski score of 7/9: superb business by these numbers. But at a P/E of 56.1, the price implies 26.5% FCF growth every year for ten years, against roughly 15% sales growth historically. Even at a 30% assumed growth rate the DCF gives about Rs 1,873. This is the classic "great company, not necessarily a great investment at this price" case. The framework's response is not "sell" or "avoid"; it is "watchlist, and decide in advance what price or what growth would change your mind".

HDFC Bank: why banks get a different layout

$ .venv/bin/python -m framework.invest HDFCBANK          (sections 4-5)
4) QUALITY SCORECARD: 89.3/100  (grade A)
                rule  value  weight  zero_at  full_at  points
        ROE 5y avg %  16.15      25      8.0     18.0    20.4
        ROA 5y avg %   1.87      25      0.5      1.5    25.0
   Revenue CAGR 5y %  22.08      10      0.0     15.0    10.0
    Profit CAGR 5y %  19.98      15      0.0     15.0    15.0
         Piotroski F   5.00      10      0.0      9.0     5.6
Promoter chg 8Q (pp)    NaN       5     -5.0      0.0     NaN
   (points scale linearly between zero_at and full_at; missing rules are excluded and the total rescaled)

5) VALUATION
   Price Rs 721.00 | Mcap Rs 1,110,422 cr | P/E 14.0 | P/B 1.85 | Earnings yield 7.14% | FCF yield 7.45%
   DCF on FCF not meaningful (lender or negative FCF) - use P/B vs ROE instead.
   Justified P/B ~ (ROE - g) / (r - g) = (15.6% - 5%) / (12% - 5%) = 1.52x vs actual 1.85x

For a lender, free cash flow is not meaningful (lending is the operating activity), so the framework switches to the justified price-to-book ratio from Chapter 8: \(P/B = (ROE - g)/(r - g)\). With a 3-year average ROE of 15.6%, \(g\) = 5% and \(r\) = 12%, the justified P/B is 1.52x against the actual 1.85x. Bank asset quality (gross and net NPA, CASA ratio, capital adequacy) is not on screener's main page, so you must read it in the bank's results presentation. That is a checklist item, not something to skip.

ITC: a reminder to read "other income"

$ .venv/bin/python -m framework.invest ITC               (ratio rows)
year                 Mar 2021  Mar 2022  Mar 2023  Mar 2024  Mar 2025  Mar 2026
sales                49257.00  60645.00  70919.00  67932.00  75323.00  78868.00
net_profit           13383.00  15503.00  19477.00  20751.00  35052.00  21018.00
eps                     10.69     12.37     15.44     16.39     27.77     16.51
sales_growth_pct        -0.27     23.12     16.94     -4.21     10.88      4.71
profit_growth_pct      -14.17     15.84     25.63      6.54     68.92    -40.04
opm_pct                 34.64     34.01     36.24     37.08     34.29     34.62
...
cfo_to_pat               0.94      1.02      0.97      0.83      0.50      0.88
fcf                  10693.00  13767.00  16184.00  13724.00  15524.00  16332.00
dividend_payout_pct    101.00     93.00    100.00     84.00     52.00     88.00

Net profit jumped from Rs 20,751 cr (FY24) to Rs 35,052 cr (FY25) and fell back to Rs 21,018 cr (FY26). Operating profit barely moved (Rs 25,188 cr, Rs 25,832 cr, Rs 27,306 cr in the screener data). The jump came from "other income" of Rs 17,803 cr in FY25, against Rs 3,330 cr the year before. FY25 was the year of the ITC Hotels demerger (January 2025), and screener's other-income line holds such exceptional items. CFO/PAT dropped to 0.50 that year for the same reason. An automated ratio screen would read FY25 as a 69% profit jump; a careful reader sees a one-off. The quality score (83.3) uses 5-year averages, which dampens such spikes, but only you can label them.

Buy zones: turning valuation into a plan

A buy zone is a price range decided in advance, at which you would add to a position in tranches. The cleanest way to build one with the framework is to ask: what price corresponds to different growth assumptions?

Two price charts Oct 2024-Sep 2026 with horizontal DCF lines. TCS at Rs 2,051 sits between the 6% growth line (Rs 1,970) and 8% line (Rs 2,284). Pidilite at Rs 1,465 sits above the 25% growth line (Rs 1,314), far above the 15% line (Rs 635)
Figure 17.4 Each dashed line is framework.fundamentals.dcf at a different FCF growth rate (r = 12%, terminal 5%, 10 years). The price's position among the lines tells you what growth the market is pricing in.
Worked example · buy-zone rule (educational)

"Start buying when the price implies at most two-thirds of historical growth"

  1. TCS: historical sales CAGR 10.2%, two-thirds is 6.8%. The price lines are Rs 1,970 at 6% and Rs 2,284 at 8%, so the rule's zone starts below roughly Rs 2,100. The 30 Sep close of Rs 2,050.60 sits inside it. Under this rule an investor whose research checklist was also satisfied would buy the first of three tranches, and add the next two only after the next results confirm the thesis or at lower prices.
  2. Pidilite: historical 14.9%, two-thirds is 9.9%, which corresponds to about Rs 439. Today's Rs 1,465 is more than three times that. The rule says: watchlist only.
  3. Why tranches: the scorecard is backward-looking and your growth estimate may be wrong. Splitting the buy over 3 dates (or 3 price levels) protects you from buying everything just before bad news. For core holdings this is the same idea as a SIP.
Valuation is a range, not a number

The TCS sensitivity grid runs from Rs 1,742 (r = 13%, g = 6%) to Rs 4,314 (r = 11%, g = 14%). A one-point change in the discount rate moves the value by 15-20%. That is why Varsity insists on a margin of safety (around 30%) and why the framework prints the grid next to the base case.

Sell rules for the core

7. Building and sizing the core portfolio

Varsity's Module 9 spends several chapters on portfolio risk: variance, covariance, the correlation matrix, portfolio variance, the equity curve, expected returns and value at risk. The practical lesson for a core portfolio is simple: a portfolio of reasonably uncorrelated stocks swings much less than the stocks in it.

Portfolio volatility and 1-day value at risk \[ \sigma_p = \sqrt{w^{\top} \Sigma\, w}\times\sqrt{252} \] \[ \text{VaR}_{95\%} = -\,\text{5th percentile of daily portfolio returns} \times \text{portfolio value} \]
\(w\)
The list ("vector") of portfolio weights, one per stock, adding up to 1. Ten equal weights means each is 0.1.
\(\Sigma\)
Here capital sigma is not "sum": it is the covariance matrix of daily returns, a table with one row and one column per stock. The diagonal holds each stock's variance (its daily volatility squared); the other cells hold how each pair moves together (their correlation × both volatilities).
\(w^{\top} \Sigma\, w\)
(\(\top\) = "transpose", i.e. the weights written as a row.) Shorthand for: for every pair of stocks, multiply weight × weight × their cell in the table, and add everything up. The result is the portfolio's daily variance. It is the two-asset formula from Chapter 11 extended to any number of stocks.
\(\sqrt{252}\)
Turns daily volatility into yearly (252 trading days a year).
\(\sigma_p\)
The portfolio's annualised volatility, in %.
5th percentile
Sort the past daily portfolio returns from worst to best; the 5th percentile is the return with 5% of days worse than it. It is negative, so the minus sign makes VaR a positive loss.
\(\text{VaR}_{95\%}\)
One-day value at risk, in Rs: on 95% of past days the loss was smaller than this.
  1. Volatility: build the covariance table from the daily returns (a spreadsheet's COVARIANCE.S, or numpy.cov), combine it with the weights, take the square root for daily volatility, and multiply by \(\sqrt{252}\).
  2. VaR: this is the historical-simulation method Varsity uses. Compute the portfolio's daily return for every past day, sort them, read off the 5% tail, and multiply by the portfolio value. It says nothing about how bad the worst 5% of days can be; the expected shortfall in the example below covers that.
Real example · illustrative 10-stock equal-weight portfolio · 30 Sep 2025 to 30 Sep 2026

HDFCBANK, ICICIBANK, TCS, ITC, HINDUNILVR, LT, BHARTIARTL, SUNPHARMA, ASIANPAINT, PIDILITIND

  1. Average annualised volatility of the ten stocks over the last 247 sessions: 22.5%.
  2. Average pairwise correlation: 0.27 (highest pair HDFCBANK and LT at 0.54; lowest TCS and BHARTIARTL at 0.02).
  3. Portfolio volatility \(\sqrt{w^\top\Sigma w}\) with \(w_i = 0.1\): 13.1%. Diversification removed about 42% of the swing.
  4. 1-day 95% historical VaR on Rs 10 lakh: Rs 14,741 (1.47%); the average loss on the worst 5% of days (expected shortfall) was Rs 19,016.
  5. Portfolio beta to Nifty: 0.90 (beta measures how much the portfolio tends to move for a 1% move in the Nifty; 0.90 means about 0.9%). One-year return -8.9% vs Nifty -8.2%. Diversification lowers volatility; it does not stop a portfolio falling with the market.
Left: correlation heatmap of ten stocks with values mostly between 0.1 and 0.5. Right: bar chart of each stock's volatility from 19.6% to 28.5% with the portfolio's 13.1% marked well below the 22.5% average
Figure 17.5 Correlations and volatilities for an illustrative core portfolio (dividend-adjusted daily returns). The portfolio's 13.1% volatility is far below the 22.5% average of its members.

Core sizing rules (write your own numbers)

8. The swing trading framework (satellite bucket)

The swing bucket follows the eight-gate playbook from Chapter 5, now automated by the scanner. The order matters: the market first, then the stock, then the trade.

StepRule in the frameworkWhy
1. Market regimeScore 0-4: Nifty above 200-DMA (+1), 200-DMA rising over 20 days (+1), more than 50% of the universe above its 200-DMA (+1), India VIX below 18 (+1) or above 25 (-1). Score 4 = Risk-on (x1.0 risk), 2-3 = Neutral (x0.5), 1 or less = Risk-off (x0.25)Breakouts and pullback buys fail more often in falling markets
2. ScannerRS rating 1-99 (40% 3-month + 60% 6-month relative strength vs Nifty); trend_pullback and breakout_52w setups; watchlist of pullbacks in progressLeaders keep leading (momentum); trade with the trend
3. Setup checkOpen the chart: results date, events, sector exposure, gapsThe code cannot read news or an earnings calendar
4. Sizeposition_size with regime multiplier, capped at 20% of capitalSame rupee risk on every trade
5. OrdersBuy order for the next session; GTT (good till triggered, a standing broker-side order) stop-loss as soon as it fillsThe stop exists even if you are busy at work
6. ManagementTrail only upwards, and only after +1R (chandelier: 22-day high minus 3 ATR); exit on the strategy's exit ruleGive winners room, protect open profit
7. JournalEvery entry and exit on the same dayYour real R-multiples, not your memory of them
8. Weekly reviewjournal stats vs backtest; trailing stops; RS leadersCatch drift early

Step 1 in action: the regime over the last two years

Nifty and 200-DMA from July 2024 to September 2026 with background shaded green, amber or red by regime score; lower panel shows percentage of universe above 200-DMA falling to 19% at the end
Figure 17.6 The scanner's regime score recomputed for every day since July 2024. Over the twelve months to 30 Sep 2026 the regime was Risk-off on 53.7% of days, Risk-on on 32.5% and Neutral on 13.8%.

On 30 Sep 2026 the score was 1 out of 4: Nifty at 22,620.45 was 7.2% below its 200-DMA (24,372.85), the 200-DMA was falling, only 19% of the universe was above its own 200-DMA, and only the calm VIX (13.49) earned a point. Two things to notice. First, the label changed 42 times in the 27 months shown, so the multiplier is a dial, not an on/off switch. Second, a low VIX in a falling market is not a reason for comfort; it just means the fall has been orderly.

9. Walking through the 30 Sep 2026 scan

This is the actual output of the scanner on the last day of data, with the default Rs 5 lakh capital and 1% base risk:

$ .venv/bin/python -m framework.scan
=== EOD swing scan for 2026-09-30  |  capital Rs 500,000  |  base risk 1.0% ===

1) MARKET REGIME
   Nifty 22,620.45  (-7.2% vs 200-DMA, -5.4% vs 50-DMA, 200-DMA falling, 1-month -6.1%)
   Breadth: 8% of universe above 50-DMA, 19% above 200-DMA
   India VIX 13.49 (65th percentile of last year)
   => score 1/4: Risk-off. Risk multiplier x0.25 -> effective risk 0.25% per trade

2) RELATIVE STRENGTH LEADERS (vs Nifty; rating 1-99)
              close  ret_3m_pct  ret_6m_pct  from_52w_high_pct  above_50dma  above_200dma  rs_rating
symbol                                                                                              
ADANIENT     2903.4        -8.6        59.3              -10.5        False          True         99
ETERNAL       320.0        14.4        37.2              -13.1         True          True         96
DIXON       13100.0         5.9        30.8              -29.1        False          True         94
HAL          4668.0         5.3        30.1               -9.4        False          True         91
KOTAKBANK     417.0         4.4        13.9               -8.0         True          True         88
TITAN        4587.0         2.4        15.2              -11.6        False          True         86
BAJFINANCE    959.4        -5.8        13.7              -18.4        False         False         83
PIDILITIND   1465.1        -8.7        11.4              -14.2        False         False         80
CDSL         1264.0        -6.4         7.9              -24.5        False         False         78
ICICIBANK    1321.7        -5.6         7.1              -10.7        False         False         75
   ...laggards: HINDUNILVR(14), NTPC(12), ONGC(9), WIPRO(6), INFY(4) 

3) SETUPS (signal on close; plan the order for the next session)
   No setups. Doing nothing is a position.

4) WATCHLIST (uptrend + RSI<45: pullback in progress; trigger = close above trigger_above)
   symbol   close  rsi14  vs_50dma_pct  trigger_above  rs_rating
  ETERNAL   320.0  44.48          0.21          329.0         96
    DIXON 13100.0  39.63         -6.32        13425.0         94
      HAL  4668.0  42.70         -3.04         4701.0         91
    TITAN  4587.0  29.49         -7.37         4698.0         86
SUNPHARMA  1816.6  38.95         -4.85         1863.6         57

Checklist before any order: chart looks right? results/events in the next 2 weeks? position <= 20% of capital? total open risk <= 5-6%? Log it: python -m framework.journal add ...

Read it top-down:

  1. Regime: Risk-off. Base risk 1% becomes 0.25% per trade. In practice, most good swing traders simply take very few trades in such a market.
  2. RS leaders. ADANIENT (99), ETERNAL (96), DIXON (94), HAL (91) and KOTAKBANK (88) have held up best against the Nifty. Notice that most leaders are below their 50-DMA: even leaders are falling, just less. INFY, WIPRO, ONGC, NTPC and HINDUNILVR are the laggards.
  3. Setups: none today. "Doing nothing is a position" is a rule, not a slogan.
  4. Watchlist. Five stocks are in long-term uptrends (above the 200-DMA, 50-DMA above 200-DMA) with RSI below 45. The trigger is a close above today's high (for TITAN, Rs 4,698). These are the names to look at first if the market turns.
Scanner update

After this chapter was written, framework/scan.py was changed to drop any bar where the strategy's exit rule is also true, the same rule run_backtest uses. If you run --days 2 now, the ADANIENT signal below no longer appears. The walkthrough keeps it because it is a good lesson: a scanner hit is only a candidate, and you check it against the system's own rules before you place an order.

Widening the window to two sessions (--days 2) showed this setup from 29 Sep (scanner output before the fix):

$ .venv/bin/python -m framework.scan --days 2        (sections 3 and 5)
3) SETUPS (signal on close; plan the order for the next session)
  symbol          setup signal_date  entry_ref    stop  stop_pct  target_2R  qty  position_value  risk_rs  rs_rating
ADANIENT trend_pullback  2026-09-29     2903.4 2776.65      4.37    3156.91    9         26130.6  1140.79         99
   entry_ref = last close. If tomorrow opens below the stop, skip; if it gaps up >1 ATR, re-size or skip.
5) F&O POSITIONING of setup stocks (near-month futures, latest bhavcopy)
           close  price_chg_pct  oi_chg_pct         buildup  lot_size
symbol                                                               
ADANIENT  2902.5           -0.8       -0.28  Long unwinding       309
ADANIENT daily candles mid-June to 30 Sep 2026 with EMA20, SMA50, SMA200, entry reference 2,903.40, stop 2,776.65 and 2R target 3,156.91; annotation notes the 29 Sep trigger close was below the SMA50, the strategy's exit rule
Figure 17.7 The only setup: ADANIENT's trend_pullback trigger on 29 Sep 2026. Source: NSE data via Yahoo Finance.
Real example · ADANIENT · signal 29 Sep 2026

Checking the setup by hand

  1. Trend: close 2,972.90 is above the 200-DMA (2,562.17), which is rising; the 50-DMA (3,004.41) is above the 200-DMA. Pass.
  2. Pullback: RSI(14) fell to 38.1 within the last five sessions (rule: below 45). Pass.
  3. Trigger: the close of 2,972.90 is above the previous day's high (2,924.00) and above the 20-EMA (2,963.00). Pass.
  4. Stop: the lower of (5-day low 2,793.70 minus 0.2 x ATR 85.27) = 2,776.65 and (close minus 1 ATR) = 2,887.63, so 2,776.65.
  5. But: the trigger close of 2,972.90 is below the 50-DMA (3,004.41). The same strategy's exit rule is "close below the 50-DMA". The backtester never buys on a bar where the exit rule is also true; the scanner, as first written, did not apply that filter. So the scanner showed a trade that your backtested system would not have taken.
  6. Next day: on 30 Sep the stock closed at 2,903.40, down 2.3%, 3.2% below its 50-DMA. The F&O data shows "long unwinding" (futures price -0.8%, open interest -0.28%): longs closing, not fresh buying.
Lesson: the scanner and the backtest must agree

Whatever you trade live must be exactly what you tested. Here the honest response is to skip the trade, and to fix the scanner so it matches the backtest. That fix is now in the framework: scan.find_setups keeps a signal only when entry is true and exit is false, the same rule run_backtest uses. Finding gaps like this between your live process and your tested process is a big part of what the paper-trading stage is for.

Worked example · sizing the ADANIENT plan (had you taken it)

Risk-off sizing, then costs

  1. Risk budget: \(5{,}00{,}000 \times 1\% \times 0.25 = \) Rs 1,250.
  2. Risk per share: \(2{,}903.40 - 2{,}776.65 = 126.75\). Quantity: \(1{,}250 / 126.75 = 9.86 \Rightarrow\) 9 shares (always round down).
  3. Actual risk: \(9 \times 126.75 = \) Rs 1,140.75. Position value \(9 \times 2{,}903.40 = \) Rs 26,130.60, only 5.2% of capital. A wide stop makes for a small position.
  4. Costs from costs.equity_delivery_charges: if stopped out, charges are Rs 72.29 (STT Rs 51.12, DP charge Rs 15.34, stamp duty Rs 3.92, the rest small), so the net loss is Rs 1,213.04 = -1.06R. If the 2R target of 3,156.91 is hit, charges are Rs 75.84 and the net profit Rs 2,205.75 = +1.93R.
  5. Costs take about 6.3% of the risk amount on a position this small. The Rs 15.34 DP charge is flat per sell, so very small positions are expensive in percentage terms. Chapter 13 covers this in detail.

10. Orders, management and the journal: a full replay

Because 30 Sep 2026 is the end of our data, we cannot see what happens next to ADANIENT. So let us replay a complete trade from a risk-on day, using the scanner's ability to run on a past date:

$ .venv/bin/python -m framework.scan --date 2025-06-30 --days 3        (sections 1 and 3)
=== EOD swing scan for 2025-06-30  |  capital Rs 500,000  |  base risk 1.0% ===

1) MARKET REGIME
   Nifty 25,517.05  (+5.9% vs 200-DMA, +3.1% vs 50-DMA, 200-DMA rising, 1-month +3.1%)
   Breadth: 73% of universe above 50-DMA, 62% above 200-DMA
   India VIX 12.79 (7th percentile of last year)
   => score 4/4: Risk-on. Risk multiplier x1.0 -> effective risk 1.00% per trade
...
3) SETUPS (signal on close; plan the order for the next session)
  symbol        setup signal_date  entry_ref   stop  stop_pct  target_2R  qty  position_value  risk_rs  rs_rating
HDFCBANK breakout_52w  2025-06-26    1000.75 982.46      1.83    1037.33   99        99074.25  1810.88         65
   entry_ref = last close. If tomorrow opens below the stop, skip; if it gaps up >1 ATR, re-size or skip.
Real example · HDFCBANK · 26 Jun to 18 Jul 2025

A textbook breakout that failed

  1. Regime 4/4, Risk-on: full 1% risk. Signal: on 26 Jun 2025 HDFCBANK closed at a new 52-week high on about 50 million shares, more than 1.5 times its 50-day average volume.
  2. Size: \(5{,}000 / (1{,}000.75 - 982.46) = 273\) shares by risk, but 273 shares would be Rs 2.7 lakh, above the 20% cap (Rs 1 lakh). The cap wins: 99 shares, risking only Rs 1,811. A tight stop is capped by the position-size limit.
  3. Orders placed the evening of 30 Jun: a limit buy for 1 Jul, and once it fills, a GTT sell with trigger 982.46. Entry: 1 Jul open at 1,002.50 (risk 20.04 per share; 2R target 1,042.58).
  4. What happened: the best high before exit was 1,011.35 (only +0.44R). On 18 Jul the low touched 976.50, the GTT triggered, exit at the stop: -1R.
  5. Was the stop right? By 30 Sep 2025 the stock was at 951.00 after trading as low as 939.10 in Aug-Sep. Following the rule turned a potential -6% into a clean -1R.
HDFCBANK candles May to Sep 2025: 52-week-high breakout on 26 Jun, entry 1 Jul at 1,002.50, GTT stop at 982.46 hit on 18 Jul, price later falls toward 940
Figure 17.8 A losing trade done right. The loss was planned, sized and executed by a standing GTT order.

GTT orders: the mechanics

Logging it in the journal

The journal is a CSV file you own (journal/trades.csv). Here is the HDFCBANK trade logged and closed (run with --path scratch.csv so the real journal stays empty; drop --path for your real trades):

$ python -m framework.journal --path scratch.csv add HDFCBANK --entry 1002.5 --stop 982.46 --qty 99 --setup breakout_52w --target 1042.58 --date 2025-07-01 --notes "scan 30-Jun-2025, regime Risk-on"
Added trade #1. Risk = Rs 1,984
$ python -m framework.journal --path scratch.csv close 1 --exit 982.46 --date 2025-07-18 --reason stop
Closed trade #1: net P&L Rs -2,218.05 after Rs 234.09 charges | R = -1.118 net (-1.00 before charges)

Notice the difference between the R before charges (-1.00, the price move from entry to stop) and the net R (-1.118, after Rs 234.09 of charges on two legs of about Rs 1 lakh each). The journal stores the net figure in its r_multiple column, because that is what your account actually lost.

The stats command is the weekly review. Here it is on a demo journal holding the 88 donchian_trend trades entered in 2025-2026 (the "live" sample from Section 4):

$ python -m framework.journal --path demo.csv stats
     closed_trades: 88
       open_trades: 0
          win_rate: 0.227
         avg_win_r: 2.251
        avg_loss_r: 1.037
      expectancy_r: -0.290
     profit_factor: 0.682
         total_pnl: -438365.130
 followed_plan_pct: 100.000
                trades  win_rate  avg_r        pnl
setup                                             
donchian_trend      88     0.227  -0.29 -438365.13

Profit factor 0.68, expectancy -0.29R, win rate 22.7%: on its own a terrible scorecard. Next to the backtest (+0.75R before 2025) it is the clearest possible signal to cut size and investigate. This is the main reason to keep a journal: it is the only honest comparison between the system you tested and the one you are actually running.

The weekly review questions

11. The futures framework (last bucket)

Futures (Chapter 9) give leverage, cheap shorting and an efficient hedge. They also turn a normal daily move into a large rupee gain or loss that is settled into your account the same evening. Use them for one of two reasons only, and decide which before you trade.

Hedging (protect the core)Directional (swing with leverage)
PurposeReduce the core's market risk for a few weeks (event risk, a regime turning Risk-off) without selling stocks and paying taxExpress a tested swing signal, long or short
When allowedAny stage, once you understand MTM and marginsStage 3 only: 6+ months of proven swing results
SizeLots = portfolio value x beta / contract valueLot-and-stop sizing (below); 0 lots = do not trade
Success measurePortfolio drawdown reduced, at the cost of carry and any upside you give upSame R-multiple journal as swing trades

Sizing by lot and stop

The same formula applies, but quantity must be a whole number of lots. position_size(..., lot_size=65, max_position_pct=None) rounds down to whole lots, and it often returns zero.

Real example · NIFTY Oct 2026 future · 30 Sep 2026

Can a Rs 10 lakh account trade one Nifty lot at 1% risk?

  1. Nifty ATR(14) on 30 Sep 2026: 209.0 points. A 2-ATR stop is 418.0 points.
  2. Risk per lot: \(2 \times 209.0 \times 65 = \) Rs 27,173.
  3. At 1% risk, capital needed for one lot: \(27{,}173 / 0.01 = \) Rs 27.2 lakh. With a tighter 1-ATR stop, Rs 13.6 lakh.
  4. position_size returns 0 lots for Rs 5 lakh, Rs 10 lakh and even Rs 25 lakh. That is the framework telling you the instrument is too big for your account at your risk level.
  5. Stock futures are similar: an ADANIENT lot is 309 shares (about Rs 8.97 lakh of stock); with the scan's stop, one lot risks \(309 \times 126.75 = \) Rs 39,166, almost eight times the 1% budget (Rs 5,000) of a Rs 5 lakh account.

Margin buffer: why "2x margin in cash"

One Nifty lot (65 x 22,707.3 = Rs 14,75,974) needed about Rs 1,67,370 of initial margin on 30 Sep 2026 (SPAN Rs 1,37,851, the exchange's worst-case one-day scenario margin, from NSE's risk file, plus a 2% exposure margin of Rs 29,519): 8.8x leverage. Margin is set to cover a bad day, not a bad month:

Bar chart of 1-in-100 and worst historical Nifty losses on one lot over 1, 5, 10 and 20 days, against lines for the initial margin of Rs 1.67 lakh and a 2x buffer of Rs 3.35 lakh; worst 20-day loss is Rs 5.46 lakh in March 2020
Figure 17.9 Ten years of Nifty moves applied to one lot at today's price. A 1-in-100 twenty-day fall (-10.9%) uses up almost the entire initial margin; the March 2020 crash would have taken more than three times it.

Rollover and OI context

NSE index and stock futures expire on the last Tuesday of the month (the previous trading day if it is a holiday). Positional futures traders roll (close the expiring contract and open the next month's) in the last few days. Open interest (OI) is the number of contracts still outstanding; futures.rollover_pct measures the share of OI already in the next months:

Date (Sep 2026 series)24 Sep25 Sep28 Sep29 Sep (expiry)Aug expiry day (25 Aug)
NIFTY rollover %40.150.163.274.377.4
RELIANCE rollover %46.463.483.793.794.7

Nifty's September rollover (74.3% on expiry day) was a little lower than August's (77.4%) during a falling month, a mild sign that fewer positions were carried forward. On 30 Sep the OI table showed Short buildup in NIFTY futures (price -0.69%, OI +5.67%) and in HDFCBANK (price -0.91%, OI +0.82%), and Long buildup in BANKNIFTY (price +0.20%, OI +6.09%). Across all F&O symbols: 101 short buildup, 49 long buildup, 36 long unwinding, 28 short covering. Treat this as context that agrees or disagrees with your chart, never as a signal on its own (Chapter 9 explains the four-way OI matrix).

Pricing check: by cost of carry (ignoring dividends due before expiry), fair value for the Oct future with 27 days left is \(22{,}620.45 \times (1 + 0.055 \times 27/365) = 22{,}712.5\); it traded at 22,707.3, a basis of 86.85 points (0.38%), which implies a carry rate of 5.19% a year.

The hedge calculator

Index-futures hedge \[ \text{lots} = \frac{\text{portfolio value} \times \beta}{F \times \text{lot size}} \quad\text{(rounded to whole lots)} \]
Portfolio value
The current market value of the holdings you want to protect, in Rs.
\(\beta\)
The portfolio's beta to the Nifty: how much it tends to move for a 1% Nifty move (0.90 for the core portfolio in section 7).
\(F\)
The price of the Nifty futures contract you short, in index points.
Lot size
Units per futures contract (65 for Nifty).
\(F \times \text{lot size}\)
The rupee value of one futures lot.
Lots
The number of futures lots to sell to offset the portfolio's market risk.

How it works: portfolio value × beta is the "Nifty-equivalent" exposure: how many rupees of Nifty your portfolio behaves like. Divide by one lot's value to see how many lots match it. You can only trade whole lots, so the hedge is usually more or less than 100%; the example below shows how large the rounding error is for small portfolios.

Real example · core portfolio beta 0.90 · NIFTY Oct future 22,707.3 x 65

How many lots to short to hedge the core?

  1. Contract value: \(22{,}707.3 \times 65 = \) Rs 14,75,974.5.
  2. Rs 10 lakh core: \(10{,}00{,}000 \times 0.90 / 14{,}75{,}974.5 = 0.61\) lots. Rounded to 1 lot = 164% hedged (you would be net short the market).
  3. Rs 15 lakh: 0.915 lots, 1 lot = 109% hedged. Rs 25 lakh: 1.52 lots, 2 lots = 131%. Rs 50 lakh: 3.05 lots, 3 lots = 98.4%.
  4. Rs 5 lakh: 0.305 lots rounds to 0. It cannot be hedged with Nifty futures at all.
Line chart of percentage hedged versus portfolio value from 2 to 60 lakh showing a sawtooth, 0% below about 8 lakh, jumping to 200%, and settling within 80-120% only above about 30 lakh
Figure 17.10 With a 65-unit lot, hedges are coarse. One lot fits a Rs 16.4 lakh portfolio at beta 0.9; below about Rs 30 lakh, the rounding error is large. Options (Chapter 10) offer finer protection for small portfolios.
Futures rules for your plan

12. The backtest evidence, read honestly

The framework's built-in systems were backtested on the 37-stock universe from Oct 2017 to Sep 2026 with Rs 10 lakh, 1% risk, at most 8 positions and 20% per position, all Indian delivery charges and 0.1% slippage per side (full report in framework/reports/backtest_summary.md; Chapter 12 explains the method).

SystemCAGRMax DDSharpeTradesWin %Exp. (R)2017-21 CAGR2022-26 CAGR
trend_pullback4.3%-20.7%-0.0856829.8%0.108.5%0.6%
breakout_52w7.1%-26.4%0.1938843.3%0.1215.5%0.2%
donchian_trend13.8%-27.0%0.5836331.1%0.5023.4%5.7%
ma_crossover (Nifty)2.6%-42.8%-0.17742.9%n/a1.9%2.9%
Nifty 50 buy & hold (price index)9.6%-38.2%0.32---14.2%5.3%
Universe equal-weight buy & hold21.2%-35.8%0.93-----
Random entries, same exits (20 runs)1.2% to 9.3% (median 4.9%)-------

How to read the columns: CAGR is the compound annual growth rate of the equity curve; Max DD is the largest fall from a previous peak; Sharpe is return above the 5.5% T-bill rate divided by volatility (negative means it earned less than T-bills); Exp. (R) is the average trade in R-multiples, net of costs. Chapter 12 derives each one.

Scatter of CAGR against maximum drawdown: universe equal-weight 21.2% at -35.8%, donchian_trend 13.8% at -27%, Nifty 9.6% at -38.2%, breakout_52w and the template near 7%, trend_pullback 4.3%, the chapter's idea 3.8%, ma_crossover 2.6%; grey band for the random-entry cloud from 1.2% to 9.3%
Figure 17.11 Every system in one picture. Anything inside the grey band has not shown that its entry rule beats random entries with the same exits.

What this evidence does and does not say

Varsity's momentum portfolio, rebuilt

Module 10's last chapter describes a momentum portfolio: rank a tracking universe by 12-month return, buy the top 10-12 in equal weights, hold for a month, then re-rank and rebalance. Varsity suggests a universe of 150-200 stocks; with only 37 stocks here, treat the result as a demonstration. The figures script rebuilt it with monthly rebalancing and about 0.22% cost per side of turnover:

Log growth chart Oct 2017 to Sep 2026: momentum top-10 ends at 7.05x, universe equal-weight 5.65x, Nifty 2.29x
Figure 17.12 Top-10 by 12-month return, rebalanced monthly, after estimated costs. CAGR 24.3% (max drawdown -37.5%) vs 21.2% for the equal-weight universe and 9.7% for the Nifty price index over the same days.

The momentum portfolio beat the equal-weight universe in both halves (30.9% vs 29.5% in 2017-21, a thin margin; 18.4% vs 14.0% in 2022-26), with turnover costs of about 1.0% a year. It is the one idea in this chapter that beat the survivorship-biased benchmark rather than just the Nifty. It still fell 37.5% at its worst, and the universe problem remains, so the next step would be to rerun it on a point-in-time Nifty 100 or Nifty 200 universe before trusting it. Also notice what "momentum" means in a falling market: on 30 Sep 2026 the top-10 list ran from TITAN (+36.7% over 12 months) down to KOTAKBANK (+4.8%), below the T-bill rate. Many momentum investors add an absolute filter (hold cash for a slot if the 12-month return is below the risk-free rate).

Other systems in Varsity Module 10

Module 10 also covers pair trading (two correlated stocks drift apart and you bet on convergence, using correlation, linear regression, the ADF test and the error ratio) and calendar spreads (trading the price gap between two futures expiries). Both are mean-reversion systems that need F&O, shorting and careful statistics; Chapter 12 introduces them. They belong after Stage 3, not before.

13. Design and test your own strategy, step by step

This is the skill that makes the framework yours. We will do it once, completely, with an idea a learner might reasonably have after reading this course, and we will accept the result even though it is disappointing.

Step 1: write the hypothesis and rules before touching data

Learner's idea · written before the first backtest

"RS leader breakout, only in a healthy market"

Step 2: copy the template and keep the contract

The template (framework/my_strategies/template.py) shows the contract every strategy must honour: return a DataFrame indexed like the input, with entry and exit booleans (decided on the close, filled at the next open), a stop level, an optional trail and target, and a score for ranking. The learner's version (saved for this chapter as course/scripts/ch17_my_idea.py; in your own copy it would live in framework/my_strategies/) is shown below; the file's imports are numpy as np, pandas as pd, framework.indicators as ind and framework.risk.chandelier_stop:

def signals(df: pd.DataFrame, bench: pd.DataFrame | None = None, breakout_n: int = 20, rs_n: int = 126,
            stop_atr: float = 2.0, use_regime: bool = True) -> pd.DataFrame:
    out = df[["open", "high", "low", "close"]].astype(float).copy()
    c = out["close"]

    # --- indicators (all causal: rolling windows and shift(+1) only)
    a = ind.atr(df, 14)
    sma50, sma200 = ind.sma(c, 50), ind.sma(c, 200)
    prior_high = ind.donchian(df, breakout_n)["upper"]            # previous n bars, already shifted
    if bench is not None:
        rs = ind.relative_strength(df, bench, rs_n)["rs_ratio"]
        nb = bench["close"].reindex(c.index).ffill()
        regime = nb > ind.sma(bench["close"], 200).reindex(c.index).ffill()
    else:                                                          # no benchmark: absolute momentum
        rs = (c / c.shift(rs_n)) * 1.0
        regime = pd.Series(True, index=c.index)
    if not use_regime:
        regime = pd.Series(True, index=c.index)

    # --- rules
    leader = (c > sma200) & (rs > 1.0)
    trigger = c > prior_high
    out["entry"] = (regime & leader & trigger).fillna(False).astype(bool)
    out["exit"] = (c < sma50).fillna(False)
    out["stop"] = c - stop_atr * a
    out["trail"] = chandelier_stop(df, 22, 3.0)
    out["target"] = np.nan
    out["score"] = rs
    return out

Every line uses only rolling() windows and donchian(), which is already shifted by one bar. Nothing uses shift(-n) or a whole-series maximum, which would let the strategy see the future.

Step 3: prove it is causal

The framework's test computes the signals on the full history and on history cut off at 31 Mar 2021, and checks they are identical up to that date. If any rule peeks ahead, the two differ:

# run from the repository root with: PYTHONPATH=course/scripts .venv/bin/python check_causal.py
import pandas as pd
from framework.data import load_ohlc
import ch17_my_idea as m
df, b = load_ohlc("RELIANCE"), load_ohlc("NIFTY")
full = m.signals(df, bench=b)
part = m.signals(df.loc[:"2021-03-31"], bench=b.loc[:"2021-03-31"])
cols = ["entry", "exit", "stop", "trail", "score"]
pd.testing.assert_frame_equal(full.loc[part.index, cols], part[cols])   # passes: no look-ahead

In your own copy, add "framework.my_strategies.my_idea:signals" to the parameter list of framework/tests/test_backtest.py::test_strategies_are_causal and run pytest.

Step 4: backtest it with the CLI

$ PYTHONPATH=course/scripts .venv/bin/python -m framework.backtest_cli ch17_my_idea:signals --out my_reports
ch17_my_idea:signals on 37 symbols, 2017-10-03 -> 2026-09-30
              CAGR: 3.8%
               Vol: 12.7%
            Sharpe: -0.07
           Sortino: -0.09
             MaxDD: -31.1%
            Calmar: 0.12
            Trades: 540
          Win rate: 40.7%
     Profit factor: 1.14
    Expectancy (R): 0.04
   Avg hold (days): 30
          Exposure: 83.6%
      Avg invested: 65.1%
        Nifty CAGR: 9.7%
       Nifty MaxDD: -38.4%
Wrote trades/equity/drawdown/metrics to my_reports

--out sends the report files to a folder of your choice; without it they go to framework/reports/ and replace any files with the same name.

Step 5: read the metrics

3.8%
CAGR
-31.1%
Max drawdown
0.04R
Expectancy
540
Trades

Step 6: robustness checks

Variant (one change at a time)CAGRMax DDTradesExp. (R)PF2017-212022-26
Base (n = 20, 2 ATR, regime on)3.8%-31.1%5400.041.1412.5%-3.5%
Regime filter OFF3.0%-42.9%645-0.011.0816.6%-8.0%
Breakout window 10 days3.5%-28.4%6140.001.137.4%0.0%
Breakout window 55 days2.5%-36.2%4930.001.109.9%-3.9%
Stop 3 ATR3.9%-30.9%4840.071.1710.8%-2.1%
Log growth chart 2017-2026: universe equal-weight 5.65x, donchian_trend 3.19x, Nifty 2.28x, the learner's idea 1.40x and the no-regime version 1.30x; lower panel drawdowns with the regime version shallower
Figure 17.13 The learner's idea against benchmarks. Flat stretches are the regime filter keeping the strategy in cash (most of 2026).

Step 7: verdict

Rejected, by the criteria written in Step 1. Every variant sits inside the random cloud, and every variant made money in 2017-21 and lost or broke even in 2022-26, which is the market doing the work, not the rule. One useful finding survives: the regime filter cut the maximum drawdown from -42.9% to -31.1% at almost no cost in CAGR. That is worth carrying into other systems (and it is what the scanner's risk multiplier does).

The template's own example (a Bollinger squeeze breakout above the 200-DMA) fares the same way: 7.2% CAGR, -29.3% max drawdown, 595 trades, 0.01R expectancy, and 19.8% in 2017-21 against -3.0% in 2022-26. Most ideas fail. Finding that out for Rs 0 in a backtest, instead of for real money over two years, is the point of the exercise.

The overfitting trap

The tempting next step is to try 50 more variants until one shows 15% CAGR. With enough tries something always does, by chance. If you test variants, count them, keep the rules you wrote first as the "official" result, and treat the best variant as a new hypothesis to be tested on data it has not seen (another universe, or the next 12 months of paper trading).

Step 8: if it had passed

  1. Paper-trade it for at least 30 signals with the journal (setup name = your strategy name).
  2. Register it in strategies.STRATEGIES and add it to scan.SETUPS so the evening scan shows its signals. The scanner already applies Section 9's rule (no entry on a bar where the exit rule is true), so scan and backtest agree.
  3. Go live at Stage 1 size (0.25%) and compare the journal with the backtest every week.

14. Your written trading plan (fill-in template)

Copy this into a text file, fill in every blank in your own words and numbers, sign and date it. Change it only on the monthly review day, never during market hours or right after a loss.

MY TRADING & INVESTING PLAN                         version ___   date ___________

0. WHY AND HOW MUCH
   Emergency fund (months of expenses) kept outside the market: ____
   Total investable capital: Rs ________   Core ____%   Swing ____%   Futures ____%
   Current stage (0 paper / 1 small / 2 normal / 3 futures): ____

1. CORE (LONG-TERM) RULES
   Universe: ______________________________________________
   Screen: quality score >= ____, Piotroski >= ____, my extra filters: __________
   I must be able to explain the moat and the main risk in two sentences: yes / no
   Buy zone rule: price implies <= ____% of historical growth (reverse DCF), or ________
   Tranches: ____ buys over ____ months.   Max weight per stock ____%   per sector ____%
   Review: after every quarterly result; annual-report read by ________ each year
   Sell rules: thesis broken = ________; governance red flags = ________;
               trim above ____% weight; NOT a reason to sell: ________________

2. SWING RULES
   Systems I trade (tested, with backtest file): ______________________
   Regime filter: ________   Risk per trade: ____% of swing capital x regime multiplier
   Max open risk: ____%   Max positions: ____   Max per position: ____% of swing capital
   Entry order type and time: ________   Skip if gap above entry by > ____ ATR
   Stop: always a GTT, placed ________; I never move a stop down.
   Trail: ________________ (starts after +____R)
   No new trade within ____ days before results / known events.

3. FUTURES RULES (only at stage 3)
   Purpose: hedge / directional.  Max lots: ____.  Cash buffer: >= ____ x margin
   Size: position_size(..., lot_size) must return >= 1 lot at ____% risk
   Roll by ____ sessions before expiry; never hold stock futures into expiry.

4. REVIEW AND DISCIPLINE
   Journal every entry and exit the same day: yes / no
   Weekly: journal stats vs backtest on ________ (day)
   Demotion trigger: live expectancy below backtest's 1st percentile over 50+ trades,
                     or drawdown > ____%, or rule adherence < 90% in a month
   Daily loss stop: if I break a rule, no new trades for ____ sessions.
   Things I know I do wrong (biases): ____________________________

Signed: ______________

15. Routines: daily, weekly, monthly, quarterly

WhenTimeWhatCommands
Daily (after 4 pm IST, trading days)15-30 minRefresh data; read the regime line first; review setups on the chart (results dates, events, sector); size; place next-day orders and GTT stops; journal any fills or exitspython -m framework.data update
python -m framework.scan --capital <swing capital> --risk 1 --save framework/reports/scan_today.txt
python -m framework.journal add/close ...
Weekly (weekend)~1 hourWeekly charts of holdings and watchlist; raise trailing stops; RS leaders and laggards, sector rotation; journal stats vs backtest; if trading futures, OI buildup and rollover in expiry weekpython -m framework.journal stats
futures.oi_buildup_table(data.load_futures())
Monthly (first weekend)1-2 hoursCompare live results with backtest; investigate gaps; update the plan if needed (only now); check open risk and bucket weights; rebalance a momentum sleeve if you run onepython -m framework.backtest_cli all
Quarterly (results season)Half a dayRe-run the scorecard on every core holding and watchlist name after its results; has the quality score, Piotroski or reverse-DCF growth changed? Read results presentation and concall notes; decide on tranches and trimspython -m framework.invest SYMBOL --refresh
Yearly (after the 1 Feb Budget and 31 Mar year-end)Half a dayUpdate charges and tax rates in config.py and bump AS_OF; review the universe; tax-loss harvesting decisions before 31 Mar; in March, harvest equity LTCG up to the Rs 1.25 lakh exemption and rebalance the core back to its target mix, new money first (Chapters 15-16); read annual reports of core holdingspython -m pytest framework/tests -q
python -m framework.funds harvest --code <scheme> --lots my_lots.csv
python -m framework.funds rebalance --help

Psychology: the routine protects you from yourself

Varsity's two chapters on trading biases (Module 9) describe the illusion of control (believing that more screen time or more indicators gives you control over outcomes), recency bias (over-weighting the last few events or trades), anchoring (fixating on your buy price or an old high), functional fixedness (using a tool only the way you first learned it), confirmation bias (seeking only evidence that agrees with you) and attribution bias (crediting wins to skill and blaming losses on the market). Varsity's suggested cure for the last one is a trading journal. Each routine above defends against one of them: fixed rules against the illusion of control, weekly statistics over many trades against recency, a valuation range written in advance against anchoring, the backtest-vs-journal comparison against attribution, and the written plan against all of them. Chapter 11 adds loss aversion, overconfidence and revenge trading, and covers each in depth.

16. The 12-week learning roadmap

Gantt chart of eleven tasks across weeks 1-12, with milestones: plan written at end of week 6, own backtest done at end of week 10, go/no-go gate at week 12
Figure 17.14 Twelve weeks from setup to a go/no-go decision. Real money is not part of these twelve weeks.
WeeksFocusMilestone you can check
1Install, run tests, load data; reread Chapters 1-2pytest passes; you can plot any stock's weekly chart
1-3Charts, support/resistance, indicators on 5 stocks you knowFor each stock: trend, key levels, ATR, RSI written down
3-12Evening scan every trading dayA dated folder of saved scan files
4-12Paper trades in the journal, at the size the plan would use30+ paper trades, every one with a stop and a setup name
5-7Fundamentals: invest.py on 10 companies; read 2 annual reportsA one-page note per company: moat, risks, buy zone
6Write trading plan v1 (Section 14)M1: signed plan
7-8Risk and psychology; sizing drills until automaticSize 20 hypothetical trades by hand, then check with position_size
8-10Backtest one idea of your own (Section 13)M2: written verdict with robustness table
9-10Costs and tax; build the core watchlistNet-of-cost P&L for your paper trades; 10-name watchlist with buy zones
10-11Futures and options theory onlyCompute margin, MTM and a hedge for your own numbers
12Review paper stats against the backtest; plan v2M3: go/no-go. Go to Stage 1 only if the Stage 0 exit criteria are all met

The core bucket can start earlier than the swing bucket. Staggered buying of a few high-quality businesses in tranches, or an index fund while you learn (Chapter 14), does not need a proven trading edge. It needs the research checklist and patience.

17. Keeping data fresh and extending the framework

Daily prices

# after ~4 pm IST (Yahoo is sometimes a day late)
.venv/bin/python -m framework.data update                 # every CSV in data/ohlc
.venv/bin/python -m framework.data update TCS NIFTY       # just these

update_ohlc re-downloads the last ten days from Yahoo Finance and overwrites them, so provisional bars get corrected. Two cautions. First, vendors sometimes miss corporate actions: the framework found three by scanning for overnight gaps above 20% (the Tata Motors demerger in Oct 2025, a TRENT adjustment and a CDSL listing-day bar) and back-adjusts them through config.PRICE_ADJUSTMENTS. Run that gap check after every update of a new symbol. Second, never mix adjusted and unadjusted prices in the same calculation: charts use split-adjusted closes, while backtests use dividend-adjusted (adjust=True) total-return prices. A third, smaller one: an incremental update does not rewrite older Adj Close values when a new dividend goes ex, so the adjusted series slowly drifts from Yahoo's. Once or twice a year, move a CSV aside and run update for that symbol to re-download its full 10-year history.

F&O bhavcopy

NSE publishes a daily F&O bhavcopy (UDiFF format) at a URL of the form https://nsearchives.nseindia.com/content/fo/BhavCopy_NSE_FO_0_0_0_YYYYMMDD_F_0000.csv.zip. Download it with a browser User-Agent, keep the futures rows and append them to data/fno/. data.load_futures() reads the file named in config.FUTURES_FILE, so point that setting at your updated file. Lot sizes change periodically; always take them from the latest bhavcopy rather than from memory.

Fundamentals

fetch_fundamentals caches each company in data/fundamentals/SYMBOL.json for 7 days; use --refresh after results. It tries the consolidated page first and falls back to standalone. Screener's "+" breakdown rows are not fetched, so items like receivables days and NPA ratios come from the company's own filings.

Rates and rules

Every charge and tax rate lives only in framework/config.py, with a source comment and an AS_OF date (currently 2 Oct 2026). As of that date it encodes: delivery STT 0.1% on buy and sell, futures STT 0.05% on sell (from 1 Apr 2026), options STT 0.15% of premium on sell, stamp duty 0.015% on delivery buys, a Rs 15.34 DP charge per sell, STCG 20% and LTCG 12.5% above Rs 1.25 lakh a year, plus 4% cess. The fund rules used by framework.funds (the three tax classes and their holding periods, 0.005% stamp duty on fund purchases, 0.001% STT on equity-fund and equity-ETF sales) sit in the same file under the MF_ names. After every Budget or SEBI circular, check the broker's charges page and the brokerage calculator, update config.py, bump AS_OF and rerun the tests.

Extension points

18. Try it yourself

These snippets need a copy of the course repository (set up as in its framework/README.md). Save each as a .py file and run it from the repository's root folder, for example .venv/bin/python snippet.py. Outputs shown are from the course data to 30 Sep 2026.

A. Size and cost a planned swing trade

import sys; sys.path.insert(0, ".")          # run from the repository root
from framework import risk, costs

capital, base_risk, regime_mult = 500_000, 1.0, 0.25      # Risk-off on 30 Sep 2026
entry, stop = 2903.40, 2776.65                            # ADANIENT plan from the scan
qty = risk.position_size(capital, base_risk * regime_mult, entry, stop, max_position_pct=20)
target = 3156.91                                          # 2R target printed by the scan
print("qty", qty, "| risk Rs", round(qty * (entry - stop), 2), "| target", round(target, 2))
for label, exit_px in [("stop", stop), ("target", target)]:
    c = costs.equity_delivery_charges(entry, exit_px, qty)
    print(label, "charges", c["total"], "net P&L", c["net_pnl"])
qty 9 | risk Rs 1140.75 | target 3156.91
stop charges 72.29 net P&L -1213.04
target charges 75.84 net P&L 2205.75

B. Screen companies on quality and implied growth

import sys; sys.path.insert(0, ".")          # run from the repository root
from framework import fundamentals as F

for sym in ["TCS", "PIDILITIND", "ITC"]:
    fin = F.fetch_fundamentals(sym)                 # uses the 7-day cache in data/fundamentals/
    sc = F.quality_scorecard(sym, fin)
    v = F.valuation_snapshot(fin)                   # r = 12%, terminal 5%, 10 years
    print(f"{sym:<11} quality {sc['score']:5.1f} ({sc['grade']})  P/E {v['pe']:5.1f}  "
          f"implied FCF growth {v['implied_growth']:.1%}  vs 5y sales CAGR {v['growth_assumed']:.1%}")
TCS         quality  88.1 (A)  P/E  14.0  implied FCF growth 6.7%  vs 5y sales CAGR 10.2%
PIDILITIND  quality  96.3 (A)  P/E  56.1  implied FCF growth 26.5%  vs 5y sales CAGR 14.9%
ITC         quality  83.3 (A)  P/E  16.2  implied FCF growth 9.7%  vs 5y sales CAGR 9.9%

C. How sure are you about a system's edge?

import sys; sys.path.insert(0, ".")          # run from the repository root
import numpy as np, pandas as pd
from framework.risk import expectancy

t = pd.read_csv("framework/reports/donchian_trend_trades.csv")
r = t["r_multiple"].dropna()
e = expectancy(r)
se = r.std() / np.sqrt(len(r))
print(f"n={e['n']}  E={e['expectancy']:.2f}R  win={e['win_rate']:.1%}  payoff={e['payoff']:.2f}")
print(f"standard error {se:.3f}R -> 90% interval {e['expectancy']-1.645*se:.2f}R to {e['expectancy']+1.645*se:.2f}R")
print("trades needed for E>0 at 95% confidence:", int(np.ceil((1.645 * r.std() / r.mean()) ** 2)))
n=363  E=0.50R  win=31.1%  payoff=3.78
standard error 0.189R -> 90% interval 0.19R to 0.81R
trades needed for E>0 at 95% confidence: 142

D. Core portfolio beta and the hedge

import sys; sys.path.insert(0, ".")          # run from the repository root
import pandas as pd
from framework import data, futures as fu

core = ["HDFCBANK", "ICICIBANK", "TCS", "ITC", "HINDUNILVR", "LT", "BHARTIARTL", "SUNPHARMA", "ASIANPAINT", "PIDILITIND"]
nifty = data.load_ohlc("NIFTY")["close"]
betas = pd.Series({s: fu.beta(data.load_ohlc(s, adjust=True)["close"], nifty, 250) for s in core})
port_beta = betas.mean()                                   # equal weights
print(betas.round(2).to_string()); print("portfolio beta", round(port_beta, 2))
print(fu.beta_hedge(1_500_000, round(port_beta, 2), fut_price=22707.3, lot=65))
HDFCBANK      1.25
ICICIBANK     0.93
TCS           0.80
ITC           0.65
HINDUNILVR    0.76
LT            1.43
BHARTIARTL    0.63
SUNPHARMA     0.48
ASIANPAINT    1.01
PIDILITIND    1.09
portfolio beta 0.9
{'hedge_value': 1350000.0, 'contract_value': 1475974.5, 'lots_exact': 0.9146499482206502, 'lots': 1, 'hedged_pct': 109.33144444444444}

Exercise: replace the trades CSV in snippet C with your own journal (journal/trades.csv, column r_multiple) once you have 30 closed paper trades.

19. Framework checklist

20. Key takeaways

Key takeaways

21. Quiz and exercises

Q1. Your swing bucket is Rs 3 lakh, the regime is Neutral, base risk is 1%. Entry 1,250, stop 1,190. How many shares, and what is the rupee risk?
Risk budget \(= 3{,}00{,}000 \times 1\% \times 0.5 = \) Rs 1,500. Risk per share \(= 1{,}250 - 1{,}190 = 60\). Quantity \(= \lfloor 1{,}500/60 \rfloor = 25\) shares. Position value \(25 \times 1{,}250 = \) Rs 31,250 (10.4% of the bucket, below the 20% cap). Rupee risk \(= 25 \times 60 = \) Rs 1,500.
Q2. A paper-trading record shows 30 trades with expectancy +0.6R and an R standard deviation of 2.5. Is the edge proven?
\(SE = 2.5/\sqrt{30} = 0.456\)R. The 90% interval is \(0.6 \pm 1.645 \times 0.456\), about -0.15R to +1.35R, which includes zero. Trades needed: \((1.645 \times 2.5 / 0.6)^2 = 47.0\), so about 47 trades if the true edge really were 0.6R. Not proven yet; it is enough to move to Stage 1 (small size) if the other Stage 0 criteria are met, not to full size.
Q3. On 30 Sep 2026, run with --days 2, the original scanner listed ADANIENT as a trend_pullback setup from 29 Sep. Give two reasons from this chapter to skip it.
(1) The trigger close (2,972.90) was below the 50-DMA (3,004.41), which is the strategy's own exit rule; the backtester never enters on such a bar, so the trade is outside the tested system. (2) The regime was Risk-off (score 1/4) and the stock closed 2.3% lower the next day with long unwinding in its futures. A third reason: the wide stop (126.75 points, about 1.5 ATR below the entry reference) gives a tiny 9-share position whose fixed costs eat about 6% of the risk.
Q4. The reverse DCF for a company says the price implies 22% FCF growth for 10 years. Its 10-year sales CAGR is 12%. What does your framework say to do?
The price assumes growth nearly twice the company's history. Under the buy-zone rule (price should imply at most about two-thirds of historical growth, here about 8%), it is a watchlist name, not a buy. If you already own it and it has grown above your weight cap, trim to the cap. Do not sell solely because it is expensive if the thesis is intact and the weight is within limits. Like Pidilite in Section 6, quality and price are separate questions.
Q5. Why does the framework use a different valuation for HDFC Bank than for TCS? Compute the justified P/B for ROE 17%, g = 5%, r = 12%.
For a bank, borrowing and lending is the operating business, so free cash flow (CFO minus capex) does not measure what shareholders can take out. Banks are valued on book value and the return on it. Justified \(P/B = (0.17 - 0.05)/(0.12 - 0.05) = 0.12/0.07 = 1.71\)x.
Q6. A Rs 20 lakh core portfolio has beta 1.1. NIFTY futures trade at 22,707.3 with a lot of 65. How many lots hedge it, and what percentage is hedged?
Hedge value \(= 20{,}00{,}000 \times 1.1 = \) Rs 22,00,000. Contract value \(= 22{,}707.3 \times 65 = \) Rs 14,75,974.5. Lots \(= 22{,}00{,}000/14{,}75{,}974.5 = 1.49\), rounded to 1 lot, which hedges \(1/1.49 = 67\%\) of the beta exposure (2 lots would be 134%). Either choice is approximate; note it in the plan.
Q7. Your backtest shows 18 consecutive losses at worst. What drawdown does that imply at 0.5%, 1% and 3% risk per trade?
\(1 - 0.995^{18} = 8.6\%\); \(1 - 0.99^{18} = 16.5\%\); \(1 - 0.97^{18} = 42.2\%\). Recovery from 42.2% needs \(1/0.578 - 1 = 73\%\). Small risk per trade is what makes long losing streaks survivable.
Q8. A new strategy returns 8.5% CAGR on the course universe vs 9.6% for Nifty buy & hold and 21.2% for the equal-weight universe. The random-entry cloud is 1.2% to 9.3%. What do you conclude?
It is inside the random-entry cloud, so its entry signal has not shown any value beyond what random entries with the same exits achieve. It also trails both benchmarks. Conclusion: no evidence of an edge. Do not trade it; at most, treat a specific feature that improved drawdown as a hypothesis for another system.
Q9. Your live journal after 60 trades shows expectancy -0.4R. The backtest's bootstrap for 60 trades has a 1st percentile of -0.15R. What does your plan say?
Live results are worse than 99% of what the backtest would produce by chance, so the demotion rule triggers: drop one stage (for example from 1% to 0.25% risk, or back to paper) and investigate. Check execution (slippage, skipped trades, rule breaks via followed_plan_pct), regime (is the market in a phase the backtest rarely saw?) and whether the scanner matches the tested rules.
Q10. In Section 13 the regime filter did not raise CAGR much (3.0% to 3.8%). Why might you still keep it?
It cut the maximum drawdown from -42.9% to -31.1% and reduced trades from 645 to 540 (lower costs), for slightly higher return. Drawdown is what makes people abandon systems, and a smaller drawdown needs a smaller recovery (+45% instead of +75%). A filter that improves risk at no return cost is worth keeping, but it does not turn a no-edge entry into an edge.
Q11. Exercise: run python -m framework.scan --date 2025-06-30 --days 3 and --date 2026-09-30. What changed in the regime inputs?
30 Jun 2025: Nifty 25,517.05, 5.9% above a rising 200-DMA, breadth 62% above the 200-DMA, VIX 12.79, score 4/4 Risk-on (x1.0). 30 Sep 2026: Nifty 22,620.45, 7.2% below a falling 200-DMA, breadth 19%, VIX 13.49, score 1/4 Risk-off (x0.25). VIX was similar in both; trend and breadth made the difference.
Q12. Exercise: list three ways the framework's backtests could overstate real results, and the fix for each.
(1) Survivorship bias: the universe was chosen in 2026. Fix: a point-in-time index-constituent universe. (2) Execution: opens and stops assumed fillable with 0.1% slippage, no circuit-limit days. Fix: larger slippage for small caps, skip trades on circuit days, compare with journal slippage. (3) Data snooping: rules or parameters adjusted after seeing results (the "+1R trail" rule was). Fix: write rules first, count variants, test on unseen data or paper-trade before trusting. Also: the Nifty benchmark is the price index (understates the hurdle by about 1.2-1.5% a year of dividends), and tax is estimated without loss carry-forward.

22. Sources

Zerodha Varsity, Module 10 "Trading Systems": module page, ch. 1 What is a trading system? (What to expect), ch. 2 Pair trading logic, ch. 15 Calendar spreads, ch. 16 Momentum portfolios.
Zerodha Varsity, Module 9 "Risk Management & Trading Psychology": module page, Risk (Part 1), Risk (Part 2) variance and covariance, Risk (Part 4) correlation matrix and portfolio variance, Equity curve, Expected returns, Position sizing for active traders, Part 2, Part 3, Kelly's criterion, Trading biases, Trading biases (Part 2).
Zerodha Varsity, Module 3 "Fundamental Analysis": Investment due diligence (the checklist), Equity research (Part 1), Equity research (Part 2): DCF and intrinsic value.
Framework code and reports (in the course repository): framework/README.md, framework/reports/backtest_summary.md (backtests to 30 Sep 2026), framework/config.py (charges and tax rates with sources, AS_OF 2 Oct 2026).
Data: daily OHLCV for NSE stocks and indices via Yahoo Finance (to 30 Sep 2026); NSE F&O UDiFF bhavcopy Aug-Sep 2026 (NSE); NSE SPAN risk parameters for 30 Sep 2026 (via Chapter 9's data file); fundamentals from screener.in TCS, PIDILITIND, HDFCBANK, ITC (fetched Oct 2026).
Rules and rates: Zerodha charges and brokerage calculator; ClearTax, STT rates after Budget 2026; ICICI Direct, STT changes in Budget 2026; Zerodha support, GTT validity; Zerodha Z-Connect, Introducing GTT; Business Standard on SEBI's FY25 F&O study (July 2025).
Figures and numbers: course/scripts/ch17_figures.py (writes data/extra/ch17_numbers.json) and course/scripts/ch17_my_idea.py.

← PreviousAsset Allocation & Tax-Aware RebalancingFinish →Back to Contents