Claude Skill

backtest-expert

Expert guidance for systematic backtesting of trading strategies on Indian markets (NSE/BSE). Use when developing strategies, testing robustness, avoiding overfitting, or validating trading ideas.

LLM Mart · 0 points · 18 views 36 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download ajeeshworkspace-indian-trading-skills-skills_backtest-expert-dc44698.zip · 29 KB
Part of ajeeshworkspace/indian-trading-skills — 10 skills

Install

skills CLI npx skills add https://github.com/ajeeshworkspace/indian-trading-skills/tree/master/skills/backtest-expert
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ajeeshworkspace-indian-trading-skills@llmmart
Git git clone https://github.com/ajeeshworkspace/indian-trading-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole ajeeshworkspace/indian-trading-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Backtest Expert — Indian Market Strategy Validation

Core Philosophy

"Find strategies that break the least, not profit the most."

A strategy that survives stress testing across multiple market regimes, transaction cost assumptions, and parameter perturbations is far more valuable than one that shows spectacular returns on a single optimized parameter set. Overfitting is the silent killer of trading accounts.


6-Step Backtesting Workflow

Step 1: State the Hypothesis (1 Sentence Edge)

Before writing a single line of code, articulate why the strategy should work in one clear sentence.

Good hypotheses:

  • "Stocks that gap up >3% on above-average volume after consolidation tend to continue higher for 2-5 days on NSE."
  • "Nifty 50 stocks that revert to their 20-day mean after RSI drops below 30 produce positive expectancy within 5 trading sessions."
  • "Selling strangles on Bank Nifty on Wednesday expiry with delta <0.15 captures time decay faster than gamma risk materializes."

Bad hypotheses:

  • "This indicator combination looks good on the chart." (no edge articulated)
  • "I saw someone on Twitter making money with this." (no reasoning)

Ask yourself:

  • What behavioral or structural edge am I exploiting?
  • Why would this edge persist? (Structural > Behavioral > Statistical)
  • Who is on the other side of this trade, and why are they losing?

Step 2: Codify Rules (No Ambiguity)

Every rule must be binary — a computer must be able to execute it without interpretation.

Rule Categories

Category What to Define Example
Universe Which stocks/instruments Nifty 200 constituents, F&O stocks only, market cap >5000 Cr
Entry Exact trigger conditions Close > 20 EMA AND RSI(14) crosses above 40 AND volume > 1.5x 20-day avg
Exit — Target Profit-taking rule Close 3% above entry OR trailing stop of 1.5 ATR
Exit — Stop Loss-cutting rule Close below entry-day low OR 2% fixed stop
Exit — Time Maximum holding period Exit after 10 trading sessions if neither target nor stop hit
Position Sizing How much capital per trade 5% of equity per position, max 10 concurrent positions
Filters When NOT to trade Skip if stock is in F&O ban period, skip 2 days around results

India-Specific Rules to Consider

  • Circuit limits: Stocks hitting upper/lower circuit cannot be exited. Define handling.
  • F&O ban period: Stocks crossing 95% MWPL cannot add fresh F&O positions.
  • T+1 settlement: Cash equity settles next trading day (changed from T+2 in 2023).
  • Pre-open session: 9:00-9:08 AM orders, 9:08-9:15 AM matching. Define if you use pre-open.
  • Muhurat trading: Special Diwali session — include or exclude?
  • Corporate actions: Adjust for splits, bonuses, dividends, rights issues.

Step 3: Run Initial Backtest

Minimum Requirements

Parameter Minimum Recommended
Time period 5 years 8-10+ years
Number of trades 100 200+
Market regimes covered 2 (bull + bear) 4+ (bull, bear, sideways, high-vol)
Data quality Adjusted for corporate actions Survivorship-bias-free universe

Indian Market Regimes to Cover

Regime Period Examples Characteristics
Bull market 2014-2017, 2020-2021 Nifty trending up, broad participation
Bear market 2008, 2020 (Mar), 2022 (Jun) Sharp drawdowns, high correlation
Sideways/Range 2018-2019, 2023 H1 Nifty in 10% range, stock-specific moves
High volatility 2008, 2020, Budget days India VIX > 25
Low volatility 2017, 2021 H2 India VIX < 15
Pre/Post Budget Every Feb 1 Gap moves, policy-driven sectors
Election cycle 2014, 2019, 2024 Uncertainty then rally pattern
Monsoon impact Jun-Sep annually Agri, FMCG, rural economy impact
RBI policy shifts Rate hike/cut cycles Banking, NBFC, rate-sensitive sectors
Global crude shock 2018, 2022 INR weakness, OMC impact, inflation

Key Metrics to Record

Returns: CAGR, total return, monthly returns distribution
Risk: Max drawdown, average drawdown, drawdown duration, Calmar ratio
Efficiency: Sharpe ratio (use 6% risk-free for India), Sortino ratio
Trade quality: Win rate, avg win/loss, profit factor, expectancy per trade
Consistency: % profitable months, worst month, longest losing streak

Step 4: Stress Test (Spend 80% of Your Time Here)

This is where most backtests fail — and where the real value lies.

4a. Parameter Sensitivity

Perturb every parameter by +/-20% and check if performance degrades gracefully or collapses.

Parameter Base -20% -10% +10% +20% Verdict
EMA period 20 16 18 22 24 Stable if all profitable
RSI threshold 40 32 36 44 48 Fragile if only 40 works
Stop loss % 2% 1.6% 1.8% 2.2% 2.4% Check drawdown impact

Rule of thumb: If the strategy only works with exact parameter values, it is overfit. You want a "plateau" of profitability, not a "peak."

4b. Execution Friction (India-Specific Costs)

Apply realistic transaction costs:

Cost Component Delivery (CNC) Intraday (MIS) F&O
Brokerage ~₹20/order or 0.03% ~₹20/order or 0.03% ~₹20/order
STT 0.1% (buy+sell) 0.025% (sell only) 0.0125% (sell, options)
Exchange charges 0.00345% (NSE) 0.00345% (NSE) 0.05% (options)
GST 18% on brokerage+exchange 18% on brokerage+exchange 18% on brokerage+exchange
Stamp duty 0.015% (buy) 0.003% (buy) 0.003% (buy)
SEBI charges 0.0001% 0.0001% 0.0001%
Slippage 0.05-0.1% large-cap 0.1-0.2% mid-cap 0.1-0.3% options

Total round-trip cost estimates:

  • Delivery large-cap: ~0.3-0.5%
  • Intraday large-cap: ~0.1-0.2%
  • F&O (options): ~0.15-0.4%
  • Small-cap delivery: ~0.5-1.0% (wider spreads)

4c. Time Robustness

  • Split data into 3-year rolling windows. Is the strategy profitable in each?
  • Check year-by-year returns. Is any single year driving total performance?
  • Remove the best month. Is the strategy still positive?

4d. Sample Size Validation

  • Minimum 30 trades for any statistical claim (even this is weak)
  • 100+ trades: Moderate confidence
  • 200+ trades: Good confidence
  • Use the t-test: Is average trade return significantly different from zero?

Step 5: Out-of-Sample Validation (Walk-Forward Analysis)

Never skip this step.

Walk-Forward Method for Indian Markets

  1. In-sample period: Train on 5 years of data (e.g., 2015-2019)
  2. Out-of-sample period: Test on next 1-2 years (e.g., 2020-2021)
  3. Roll forward: Move window, retrain on 2016-2020, test on 2021-2022
  4. Combine: Aggregate all out-of-sample periods for true performance estimate

Walk-Forward Efficiency (WFE):

WFE = Out-of-Sample Return / In-Sample Return
  • WFE > 50%: Good — strategy generalizes
  • WFE 30-50%: Acceptable — some overfitting present
  • WFE < 30%: Poor — likely overfit

Paper Trading Validation

Before deploying capital, paper trade for at least:

  • 30 trades minimum
  • 2 months minimum
  • Cover at least one volatile period (expiry week, results season, RBI policy)

Step 6: Evaluate Results (Deploy / Refine / Abandon)

Use the evaluation script to get an objective score:

python3 evaluate_backtest.py \
  --total-trades 150 \
  --win-rate 62 \
  --avg-win-pct 1.8 \
  --avg-loss-pct 1.2 \
  --max-drawdown-pct 15 \
  --years-tested 8 \
  --num-parameters 3 \
  --slippage-tested

Decision Framework

Score Verdict Action
80-100 Deploy Size small initially (25% of intended), scale up over 50+ live trades
60-79 Refine Identify weakest dimension, address it, re-test
40-59 Refine with caution Multiple issues — may not be salvageable
0-39 Abandon Fundamental edge likely does not exist. Document lessons and move on.

Before Deploying

  • Strategy has positive expectancy after ALL costs
  • Survived parameter sensitivity testing
  • Walk-forward efficiency > 50%
  • Maximum drawdown is psychologically tolerable
  • Sample size > 100 trades
  • No more than 3-4 free parameters
  • Slippage and transaction costs included
  • Paper traded for 30+ trades
  • Written trade plan with exact rules
  • Risk management plan for live trading (position sizing, max daily loss, max drawdown circuit breaker)

Using Broker MCP Tools for Backtesting Support

While the MCP tools are not backtesting engines, they support the process. Use whichever broker is connected:

Groww MCP (if connected)

  • fetch_historical_candle_data: Fetch OHLCV data for strategy development and spot-checking
  • get_historical_technical_indicators: Calculate indicators (SMA, EMA, RSI, MACD, Bollinger, SuperTrend, etc.) on historical data
  • get_historical_candlestick_patterns: Identify candle patterns in historical data
  • fetch_stocks_fundamental_data: Screen for universe construction (PE, ROE, market cap filters)
  • fetch_fundamentals_screener: Natural language screening for universe building
  • fetch_technical_screener: Technical screening for strategy ideas
  • get_ltp: Current price for live validation
  • fetch_market_movers_and_trending_stocks_funds: Discover momentum and volume patterns

Zerodha Kite MCP (if connected)

  • get_historical_data: Fetch OHLCV candle data for strategy development
  • get_ltp / get_quotes: Current prices for live validation
  • search_instruments: Find instruments for universe construction
  • get_holdings / get_positions: Verify live portfolio against strategy signals

Quick Reference: Red Flags

Red Flag Why It Matters
CAGR > 50% with no drawdowns Too good to be true — check for look-ahead bias
Win rate > 80% Likely not accounting for slippage or adverse fills
Only works on specific parameters Overfitting — no edge, just noise
< 50 trades in backtest Statistically meaningless
No losing months in 5+ years Data error or survivorship bias
Strategy stops working after 2020 Market structure may have changed (T+1, algo proliferation)
Uses > 5 parameters Degrees of freedom too high — curve-fitted
No transaction costs modeled Real returns could be negative
Tested on Nifty 50 only Survivorship bias in universe selection

Files in This Skill

  • scripts/evaluate_backtest.py — CLI scoring tool for backtest evaluation
  • references/methodology.md — Comprehensive backtesting methodology for Indian markets
  • references/failed_tests.md — Common failure patterns and documentation framework
Files (indian-trading-skills)
  • references
    • failed_tests.md 14.1 KB
      # Failed Backtests — Patterns, Red Flags, and Documentation Framework
      
      ## Why Failed Backtests Are Valuable
      
      > "Every failed backtest narrows the search space. The trader who has tested and rejected 50 strategies knows far more than the one who deployed the first thing that 'looked good.'"
      
      A properly documented failed backtest:
      1. **Prevents repeating mistakes** — you will not re-test the same idea 6 months later
      2. **Reveals market structure** — failures often point to real market mechanics
      3. **Builds intuition** — pattern recognition for what works and what does not
      4. **Saves time** — a quick reference before starting a new strategy
      5. **Informs related strategies** — a failed momentum strategy might reveal a mean-reversion edge
      
      ---
      
      ## Common Failure Patterns
      
      ### Pattern 1: The Cost Killer
      
      **Symptoms:**
      - Strategy looks profitable before transaction costs
      - After adding brokerage + STT + slippage, expectancy goes to zero or negative
      - High trade frequency (5+ trades/day)
      
      **Why it happens:**
      - Indian delivery trading costs ~0.3-0.5% round-trip
      - Even intraday costs ~0.1-0.2% round-trip
      - A strategy with 0.15% average profit per trade cannot survive 0.2% costs
      
      **Common Indian market examples:**
      - Scalping strategies on mid-cap stocks (spreads too wide)
      - Options buying strategies with small targets (premium decay + costs eat the edge)
      - Pair trading on illiquid stock pairs (slippage on both legs)
      
      **Lesson:** Calculate the minimum edge needed to cover costs BEFORE backtesting.
      
      ```
      Minimum edge per trade:
        Delivery: > 0.5% (to cover costs + provide profit)
        Intraday: > 0.2%
        F&O options: > 1-2% of premium
        F&O futures: > 0.1%
      ```
      
      ---
      
      ### Pattern 2: The Parameter Peak
      
      **Symptoms:**
      - Excellent results at exact parameter values (e.g., RSI=42, EMA=17)
      - Performance collapses with even small changes (+/-10%)
      - Heat map shows a narrow spike, not a plateau
      - Out-of-sample performance is much worse than in-sample
      
      **Why it happens:**
      - The optimizer found noise, not signal
      - With enough parameters and enough data, you can always fit a profitable curve
      - The "optimal" parameters are describing past noise, not a repeatable edge
      
      **Red flag math:**
      ```
      If you test 100 parameter combinations, ~5 will appear "significant" at the 95%
      confidence level purely by chance. This is data snooping.
      ```
      
      **Lesson:** Always check parameter sensitivity before celebrating good results.
      
      ---
      
      ### Pattern 3: The Regime Specialist
      
      **Symptoms:**
      - Strategy performs brilliantly in one market regime (e.g., 2020-2021 bull run)
      - Flat or negative in other regimes
      - Often discovered by testing on a specific "exciting" period
      
      **Indian market examples:**
      - Momentum strategy trained on 2020-2021 (everything went up)
      - Mean-reversion strategy trained on 2018-2019 (sideways market)
      - Short-selling strategy trained on Feb-Mar 2020 (COVID crash)
      
      **Why it happens:**
      - The strategy captures a regime-specific pattern, not a universal edge
      - The regime may not repeat, or may repeat with different characteristics
      
      **Lesson:** A strategy must survive at least 3 different market regimes to be deployable.
      
      ---
      
      ### Pattern 4: The Survivorship Illusion
      
      **Symptoms:**
      - Strategy has high returns because it only trades stocks that are still listed
      - Stocks that went bankrupt, were delisted, or hit lower circuits are excluded
      - Back-tested universe is the "current" Nifty 200
      
      **Indian market examples:**
      - Strategy: "Buy all stocks that fell >50% and hold for recovery"
      - Back-tested on current universe: Works great (survivors recovered)
      - Reality: Many stocks that fell >50% never recovered (DHFL, Jet Airways, Satyam, Yes Bank below Rs 20)
      
      **Lesson:** Always use point-in-time universe data. Include delisted stocks.
      
      ---
      
      ### Pattern 5: The Circuit Limit Trap
      
      **Symptoms:**
      - Strategy uses stop losses that assume you can exit at any price
      - In reality, stocks hit lower circuit and no exit is possible
      - A few circuit-limit events cause catastrophic losses
      
      **Indian market specifics:**
      
      | Category | Circuit Limit | Impact |
      |----------|--------------|--------|
      | Stocks in derivative segment | No circuit for F&O stocks | Price can move freely |
      | Stocks NOT in derivative segment | 5%, 10%, 15%, 20% | Cannot exit if at lower circuit |
      | Index | N/A (no index circuit, but market-wide circuit exists) | Trading halt at 10%, 15%, 20% Nifty move |
      
      **Real examples:**
      - Small-cap stocks frequently hit lower circuit for 3-5 consecutive days
      - Post-earnings gaps can lock stocks at circuit for the entire session
      - During market panics (March 2020), many stocks hit lower circuit simultaneously
      
      **Lesson:** If your strategy trades non-F&O stocks, you MUST model circuit limit behavior. Either:
      1. Skip non-F&O stocks entirely
      2. Add circuit-limit handling (delay exit, accept slippage)
      3. Stress test with 2-3 day exit delays
      
      ---
      
      ### Pattern 6: The Gap Risk Destroyer
      
      **Symptoms:**
      - Strategy works well during market hours
      - Overnight gaps cause unexpected losses
      - Stop loss is 2%, but stock gaps down 8% on bad earnings
      
      **Indian market gap risk factors:**
      - Quarterly results (usually announced after market hours)
      - Global market moves (Nifty correlates with US markets, which trade after Indian close)
      - RBI policy announcements (sometimes mid-session)
      - Government policy changes (tariffs, taxes, regulations)
      - Rating agency downgrades
      - SEBI actions (stock-specific)
      
      **Typical gap magnitudes:**
      | Event | Typical Gap | Extreme Gap |
      |-------|-------------|-------------|
      | Earnings miss | -3% to -8% | -15% to -20% |
      | Earnings beat | +3% to +8% | +10% to +15% |
      | Global sell-off (overnight) | -1% to -3% | -5% to -10% |
      | Government policy | -2% to +2% | -5% to +5% |
      | Rating downgrade | -3% to -10% | -15%+ |
      | Fraud/scandal | -10% to -30% | -50%+ (circuit) |
      
      **Lesson:** If your strategy holds positions overnight, gap risk is your biggest unmodeled risk. Either:
      1. Close all positions intraday (eliminates gap risk but limits strategy types)
      2. Model gaps explicitly (use historical gap data)
      3. Size positions assuming your stop loss will be exceeded by 2-3x on gaps
      
      ---
      
      ### Pattern 7: The F&O Ban Period Blow-Up
      
      **Symptoms:**
      - Strategy trades stocks in F&O segment
      - Does not account for ban period restrictions
      - During ban periods, cannot enter new positions, forced to close losing ones
      
      **What is F&O ban?**
      - When open interest in a stock crosses 95% of Market Wide Position Limit (MWPL)
      - No new F&O positions allowed (can only close existing ones)
      - Stock moves can be extreme during ban period (short squeeze)
      - Ban can last days or weeks
      
      **Recent examples of stocks frequently hitting ban:**
      - IDEA (Vodafone Idea) — frequent ban periods
      - PNB — during NPA crisis
      - Various midcap stocks during momentum phases
      
      **Lesson:** If trading stock F&O, always check MWPL status and handle ban periods in backtest code.
      
      ---
      
      ### Pattern 8: The Liquidity Mirage
      
      **Symptoms:**
      - Strategy shows good results on backtested data
      - In live trading, cannot get fills at backtested prices
      - Especially problematic for options strategies
      
      **Indian market liquidity realities:**
      - Nifty and Bank Nifty options: Liquid at ATM, illiquid 5+ strikes away
      - Stock options: Generally illiquid except top 20-30 names
      - Stock futures: Liquid for current month, illiquid for next month
      - Small-cap stocks: Can have zero bid for minutes at a time
      - Pre-market and post-market: Very thin liquidity
      
      **Lesson:** Check average daily volume and typical bid-ask spread for every instrument in your universe. If your order would be >1% of daily volume, you will move the market.
      
      ---
      
      ## Case Study Documentation Framework
      
      When a backtest fails, document it using this template to build your knowledge base.
      
      ### Template
      
      ```markdown
      # Failed Backtest: [Strategy Name]
      
      ## Date: [YYYY-MM-DD]
      
      ## Hypothesis
      [One-sentence statement of the expected edge]
      
      ## Rules
      - Universe: [what stocks/instruments]
      - Entry: [exact conditions]
      - Exit: [target, stop, time-based]
      - Position sizing: [how much per trade]
      - Parameters: [list all free parameters with values]
      
      ## Backtest Period
      - In-sample: [start] to [end]
      - Out-of-sample: [start] to [end]
      - Total trades: [number]
      
      ## Results Summary
      | Metric | In-Sample | Out-of-Sample |
      |--------|-----------|---------------|
      | CAGR | | |
      | Sharpe | | |
      | Max DD | | |
      | Win Rate | | |
      | Profit Factor | | |
      | Expectancy/trade | | |
      
      ## Why It Failed
      [Primary failure pattern from the list above]
      
      ## Detailed Analysis
      [What specifically went wrong, with data]
      
      ## What I Learned
      [Key insight that applies to future strategies]
      
      ## Related Ideas
      [Did the failure suggest a different approach?]
      
      ## Score (from evaluate_backtest.py)
      [Paste the evaluation output]
      ```
      
      ---
      
      ## Red Flags Checklist
      
      Use this checklist before committing capital to any strategy. A single "critical" red flag should prevent deployment. Three or more "warning" flags should trigger a re-evaluation.
      
      ### Critical Red Flags (Do NOT deploy)
      
      - [ ] **Negative expectancy after costs** — The strategy loses money on average per trade when realistic costs are included
      - [ ] **Fewer than 30 trades** — No statistical basis for any claim
      - [ ] **More than 5 free parameters** — Almost certainly overfit
      - [ ] **Slippage and costs not modeled** — Results are fantasy, not estimates
      - [ ] **Look-ahead bias detected** — Using information not available at trade time
      - [ ] **Survivorship bias in universe** — Only tested on currently listed stocks
      - [ ] **No out-of-sample test** — In-sample results are meaningless alone
      - [ ] **Strategy only works in one regime** — It will fail when the regime changes
      
      ### Warning Red Flags (Proceed with extreme caution)
      
      - [ ] **Win rate above 80%** — Verify fills and check for bias
      - [ ] **CAGR above 50%** — Extraordinary claims require extraordinary evidence
      - [ ] **No losing months in 3+ years** — Something is wrong with the data
      - [ ] **Max drawdown below 5%** — Unrealistic for any strategy with meaningful returns
      - [ ] **Only 30-100 trades** — Directional evidence only, wide confidence intervals
      - [ ] **Less than 5 years tested** — May miss important regime changes
      - [ ] **Drawdown above 30%** — Most traders will abandon the strategy before recovery
      - [ ] **Walk-forward efficiency below 50%** — Significant overfitting present
      - [ ] **Average loss > 2x average win** — Fragile risk/reward structure
      - [ ] **Strategy requires overnight/weekly holding of small-caps** — Gap and circuit risk
      - [ ] **Strategy trades illiquid instruments** — Execution will deviate from backtest
      
      ### Info Flags (Be aware)
      
      - [ ] **Strategy is purely technical** — Consider if fundamental overlay would help
      - [ ] **Strategy has no benchmark comparison** — May just be buying the market with leverage
      - [ ] **Strategy only tested on Nifty 50 universe** — Try Nifty 200 or 500 for robustness
      - [ ] **No Monte Carlo simulation** — Single-path results can be misleading
      - [ ] **No paper trading phase planned** — Skip at your own risk
      
      ---
      
      ## Indian Market-Specific Failure Scenarios
      
      ### 1. Budget Day Surprise
      
      **Scenario:** Strategy is fully invested going into Budget Day (February 1). An unexpected policy change causes a 3-5% gap against positions.
      
      **Affected strategies:** All positional strategies that do not reduce exposure before Budget.
      
      **Mitigation:**
      - Reduce position size by 50% 2 days before Budget
      - Tighten stops by 50%
      - OR: Close all positions before Budget and re-enter after
      
      ### 2. Election Result Volatility
      
      **Scenario:** General election results are announced. If unexpected, Nifty can move 5-8% in a single session.
      
      **Affected strategies:** Any strategy holding overnight positions during election week.
      
      **Mitigation:**
      - Go flat or reduce to 25% exposure during election result week
      - Use options hedges (puts if long)
      - Accept that this is an unmodeled risk if holding positions
      
      ### 3. RBI Surprise Rate Action
      
      **Scenario:** RBI changes rates unexpectedly (outside normal policy meeting or by an unexpected quantum).
      
      **Affected strategies:** Strategies trading banks, NBFCs, housing finance, auto.
      
      **Mitigation:**
      - Check RBI calendar and reduce banking-sector exposure around policy dates
      - Use sector-neutral strategies that are not rate-sensitive
      
      ### 4. Global Contagion (FII Selling Pressure)
      
      **Scenario:** Global risk-off event causes FIIs to sell Rs 3,000-5,000 Cr per day for 10+ consecutive days.
      
      **Affected strategies:** All long-only strategies, especially in FII-heavy large-caps.
      
      **Mitigation:**
      - Monitor FII flow data daily (available T+1 from NSDL)
      - Reduce exposure when FII selling exceeds Rs 2,000 Cr/day for 3+ days
      - Diversify into DII-supported sectors (PSU banks, infrastructure)
      
      ### 5. Stock-Specific Corporate Governance Failure
      
      **Scenario:** Fraud or governance issue discovered (Satyam 2009, DHFL 2019, Adani Group 2023).
      
      **Affected strategies:** Concentrated strategies with large single-stock exposure.
      
      **Mitigation:**
      - Maximum 5% per stock (10% for very high conviction)
      - Diversify across at least 10-15 stocks
      - Use forensic accounting screens (e.g., Beneish M-Score, cash flow vs reported profit)
      - This is fundamentally unhedgeable — position sizing is your only defense
      
      ### 6. NSE System Outage
      
      **Scenario:** NSE systems go down during trading hours (happened February 2021 for ~3.5 hours).
      
      **Affected strategies:** Intraday strategies that need to close positions before market close.
      
      **Mitigation:**
      - Have BSE as a backup exchange for critical exits
      - Size positions assuming you may not be able to exit for a full session
      - Intraday strategies: Use MIS product type (broker will auto-square-off)
      
      ---
      
      ## Key Takeaways
      
      1. **Document every failure.** Your failed-backtest library is more valuable than your strategy library.
      2. **Look for patterns in failures.** If 5 momentum strategies failed due to transaction costs, the insight is: "momentum works but only at low frequency."
      3. **Share failures (anonymized).** The trading community benefits more from shared failures than shared successes.
      4. **Re-visit failures periodically.** Market structure changes. A strategy that failed in 2018 might work in 2025 due to changed microstructure.
      5. **Use the evaluation script.** `python3 evaluate_backtest.py` gives an objective score that removes emotional attachment to a strategy.
      
    • methodology.md 22.5 KB
      # Backtesting Methodology Reference — Indian Markets (NSE/BSE)
      
      ## Table of Contents
      
      1. [Stress Testing Methods](#stress-testing-methods)
      2. [Parameter Sensitivity Analysis](#parameter-sensitivity-analysis)
      3. [Slippage Modeling for NSE](#slippage-modeling-for-nse)
      4. [Commission Structure](#commission-structure)
      5. [Sample Size Guidelines](#sample-size-guidelines)
      6. [Market Regime Definitions](#market-regime-definitions)
      7. [Common Biases](#common-biases)
      8. [Walk-Forward Analysis](#walk-forward-analysis)
      9. [Statistical Validation](#statistical-validation)
      
      ---
      
      ## Stress Testing Methods
      
      Stress testing is the most important phase of backtesting. A strategy that survives stress testing across multiple dimensions is far more likely to perform in live trading.
      
      ### 1. Parameter Perturbation (Heat Map Analysis)
      
      **Goal:** Verify that the strategy works across a range of parameter values, not just one optimized set.
      
      **Method:**
      1. Identify all free parameters (e.g., EMA length, RSI threshold, stop loss %)
      2. Create a grid of values: base +/- 10%, +/- 20%, +/- 50%
      3. Run the backtest for every combination
      4. Plot a heat map of returns (or Sharpe ratio) across the parameter space
      
      **Interpreting heat maps:**
      
      ```
      GOOD: Broad plateau of profitability      BAD: Sharp peak at one value
      +------+------+------+------+             +------+------+------+------+
      | 1.2  | 1.4  | 1.5  | 1.3  |            | -0.2 | 0.1  | 2.8  | -0.1 |
      +------+------+------+------+             +------+------+------+------+
      | 1.3  | 1.5  | 1.6  | 1.4  |            | -0.3 | 0.2  | 0.4  | -0.2 |
      +------+------+------+------+             +------+------+------+------+
      | 1.1  | 1.3  | 1.4  | 1.2  |            | -0.5 | -0.1 | 0.1  | -0.4 |
      +------+------+------+------+             +------+------+------+------+
        Robust strategy                           Overfit strategy
      ```
      
      **Indian market consideration:** Include market-hours parameters. Some strategies use different parameters for the opening auction (9:15-9:30), mid-day, and closing hour (2:30-3:30). Test sensitivity to these time windows.
      
      ### 2. Monte Carlo Simulation
      
      **Goal:** Understand the distribution of possible outcomes, not just the single backtest path.
      
      **Method:**
      1. Take the trade-by-trade results from the backtest
      2. Randomly reshuffle the order of trades (10,000+ iterations)
      3. For each shuffle, calculate drawdown, CAGR, and Sharpe
      4. Plot the distribution of outcomes
      
      **What to look for:**
      - 5th percentile drawdown (your "realistic worst case")
      - Median vs mean CAGR (if very different, a few trades drive returns)
      - Probability of a 12-month loss (should be <20% for deployment)
      
      ### 3. Regime-Based Testing
      
      **Goal:** Verify the strategy works (or at least does not blow up) across different market environments.
      
      **Method:**
      1. Tag each period with its market regime (see regime definitions below)
      2. Run the backtest separately for each regime
      3. The strategy must be profitable in at least 3 of 5 major regimes
      4. Acceptable: flat or small loss in adverse regimes
      5. Unacceptable: catastrophic drawdown in any single regime
      
      ### 4. Synthetic Stress Events
      
      **Goal:** Test against extreme scenarios that may not appear in historical data.
      
      **Scenarios for Indian markets:**
      - Flash crash: Nifty drops 10% intraday (happened in 2012, 2015)
      - Circuit limit lock: Stock hits lower circuit for 3+ consecutive days
      - Sudden gap: Stock gaps down 15% on earnings miss
      - Liquidity freeze: Bid-ask spread widens 5x during panic
      - Exchange outage: NSE systems go down for 3+ hours (happened in 2021)
      - F&O ban: Stock enters ban period, cannot add positions
      - Global contagion: Foreign fund selling exceeding Rs 5,000 Cr/day for 10+ days
      
      ### 5. Walk-Forward Stress
      
      **Goal:** Confirm the strategy generalizes to unseen data.
      
      **Method:**
      1. Divide data into K folds (e.g., 5 x 2-year periods)
      2. Train on K-1 folds, test on the remaining fold
      3. Rotate and repeat
      4. The combined out-of-sample results are the true performance estimate
      
      ---
      
      ## Parameter Sensitivity Analysis
      
      ### What Counts as a Parameter?
      
      | Counts as Parameter | Does NOT Count |
      |---------------------|----------------|
      | Moving average length (20 in EMA-20) | Choice of exchange (NSE vs BSE) |
      | RSI overbought/oversold thresholds | Time of day to trade (if based on market hours) |
      | Stop loss percentage | Universe definition (if rule-based, e.g., "Nifty 200") |
      | Profit target percentage | Direction (long-only, short-only) |
      | Lookback period for any indicator | Position sizing formula (if mathematically derived) |
      | Volume multiplier threshold | |
      | ATR multiplier for stops | |
      
      ### Parameter Count Guidelines
      
      | Parameters | Risk Level | Notes |
      |------------|-----------|-------|
      | 1-2 | Low | Simple, robust strategies. Hard to overfit. |
      | 3-4 | Moderate | Acceptable if each parameter has a logical reason. |
      | 5-6 | High | Must show very strong out-of-sample performance. |
      | 7+ | Very High | Almost certainly overfit. Reduce before proceeding. |
      
      ### Degrees of Freedom Rule
      
      **Rule of thumb:** You need at least 10-20 trades per free parameter for statistical validity.
      
      | Parameters | Minimum Trades | Recommended Trades |
      |------------|---------------|-------------------|
      | 2 | 40 | 100+ |
      | 3 | 60 | 150+ |
      | 4 | 80 | 200+ |
      | 5 | 100 | 250+ |
      
      ### Running Sensitivity Analysis
      
      For each parameter, create a table:
      
      ```
      Parameter: EMA Length
      Base value: 20
      Tested range: 10 to 40, step 2
      
      | Value | CAGR | Sharpe | Max DD | Win Rate | Trades |
      |-------|------|--------|--------|----------|--------|
      | 10    | 12%  | 0.8    | 22%    | 54%      | 310    |
      | 12    | 14%  | 0.9    | 20%    | 55%      | 285    |
      | 14    | 15%  | 1.0    | 18%    | 57%      | 260    |
      | ...   | ...  | ...    | ...    | ...      | ...    |
      | 20    | 16%  | 1.1    | 17%    | 58%      | 220    |  <-- Base
      | ...   | ...  | ...    | ...    | ...      | ...    |
      | 40    | 10%  | 0.7    | 25%    | 52%      | 150    |
      
      Verdict: STABLE — CAGR remains positive across full range.
               Sharpe degrades gracefully. Acceptable for deployment.
      ```
      
      ---
      
      ## Slippage Modeling for NSE
      
      ### What is Slippage?
      
      Slippage is the difference between the price you see on the screen and the price you actually get filled at. It includes:
      1. Bid-ask spread cost
      2. Market impact (your order moving the price)
      3. Latency (price moves between decision and execution)
      
      ### Typical Slippage by Category (NSE)
      
      | Category | Typical Bid-Ask Spread | Slippage Per Side | Round-Trip |
      |----------|----------------------|-------------------|------------|
      | Nifty 50 stocks | 0.02-0.05% | 0.03-0.05% | 0.06-0.10% |
      | Nifty Next 50 | 0.05-0.10% | 0.05-0.08% | 0.10-0.16% |
      | Midcap 150 | 0.10-0.20% | 0.08-0.15% | 0.16-0.30% |
      | Smallcap 250 | 0.20-0.50% | 0.15-0.30% | 0.30-0.60% |
      | Micro/Nano cap | 0.50-2.00% | 0.30-1.00% | 0.60-2.00% |
      | Nifty options (ATM, near expiry) | 0.5-1.0 pts | Rs 2-5 per lot | Varies |
      | Nifty options (OTM, far expiry) | 2-5 pts | Rs 5-15 per lot | Varies |
      | Bank Nifty options (ATM) | 1-3 pts | Rs 5-10 per lot | Varies |
      | Stock options (liquid) | 1-5% of premium | Significant | Varies |
      | Stock futures (liquid) | 0.03-0.08% | 0.05-0.10% | 0.10-0.20% |
      
      ### Slippage Factors
      
      1. **Time of day:**
         - 9:15-9:30 AM: Highest slippage (opening volatility, wider spreads)
         - 9:30-2:30 PM: Normal slippage
         - 2:30-3:15 PM: Moderate (expiry-day squeezes in F&O)
         - 3:15-3:30 PM: Can spike on closing-order imbalances
      
      2. **Order size relative to liquidity:**
         - Order < 1% of daily volume: Minimal impact
         - Order 1-5% of daily volume: Moderate impact (add 0.05-0.10%)
         - Order > 5% of daily volume: Significant impact (add 0.10-0.30%)
      
      3. **Volatility regime:**
         - India VIX < 15: Normal spreads
         - India VIX 15-25: Spreads widen 1.5-2x
         - India VIX > 25: Spreads widen 2-5x
      
      4. **Event days:**
         - Budget day: Spreads 3-5x normal
         - RBI policy day: Spreads 2-3x for banking stocks
         - Election results: Spreads 5-10x, circuit limits possible
         - Monthly F&O expiry: Options spreads widen significantly in last hour
      
      ### How to Model Slippage in Backtests
      
      **Conservative approach (recommended):**
      ```
      For Nifty 50 stocks:     Add 0.05% per side (0.10% round-trip)
      For Nifty 100 stocks:    Add 0.08% per side (0.16% round-trip)
      For Midcap stocks:       Add 0.15% per side (0.30% round-trip)
      For Smallcap stocks:     Add 0.25% per side (0.50% round-trip)
      For Index options (ATM): Add Rs 2-3 per lot per side
      For Stock options:       Add 2-3% of premium per side
      ```
      
      **Aggressive test (break test):**
      Double all the above values. If the strategy is still profitable, it is robust to execution friction.
      
      ---
      
      ## Commission Structure
      
      ### Discount Broker (Zerodha, Groww, etc.) — 2024/2025 Rates
      
      | Component | Delivery (CNC) | Intraday (MIS) | F&O Futures | F&O Options |
      |-----------|----------------|-----------------|-------------|-------------|
      | **Brokerage** | Rs 0 or Rs 20/order | Rs 20/order or 0.03% | Rs 20/order or 0.03% | Rs 20/order flat |
      | **STT** | 0.1% (buy+sell) | 0.025% (sell only) | 0.0125% (sell) | 0.0625% on premium (sell) |
      | **Exchange charges** | 0.00345% (NSE) | 0.00345% (NSE) | 0.002% | 0.05% |
      | **GST** | 18% on (brokerage + exchange) | 18% | 18% | 18% |
      | **Stamp duty** | 0.015% (buy) | 0.003% (buy) | 0.002% (buy) | 0.003% (buy) |
      | **SEBI charges** | 0.0001% | 0.0001% | 0.0001% | 0.0001% |
      | **DP charges** | Rs 15.93/scrip (sell) | N/A | N/A | N/A |
      
      ### Full Service Broker (ICICI Direct, HDFC Securities, etc.)
      
      | Component | Typical Rate |
      |-----------|-------------|
      | Brokerage | 0.25-0.50% or Rs 25-35/order |
      | Other charges | Same as above |
      
      ### Total Cost Examples (Round-Trip)
      
      | Scenario | Trade Value | Total Cost | Cost % |
      |----------|-----------|------------|--------|
      | Delivery, Rs 50,000, discount broker | Rs 50,000 | ~Rs 155 | ~0.31% |
      | Delivery, Rs 2,00,000, discount broker | Rs 2,00,000 | ~Rs 500 | ~0.25% |
      | Intraday, Rs 50,000, discount broker | Rs 50,000 | ~Rs 65 | ~0.13% |
      | Nifty Futures, 1 lot (~Rs 12,00,000) | Rs 12,00,000 | ~Rs 115 | ~0.01% |
      | Nifty Option, 1 lot, premium Rs 200 | Rs 15,000 | ~Rs 55 | ~0.37% |
      | Bank Nifty Option, 1 lot, premium Rs 300 | Rs 4,500 | ~Rs 55 | ~1.22% |
      
      ### Key Takeaway for Backtesting
      
      - **Delivery trades:** Model 0.25-0.40% round-trip cost
      - **Intraday trades:** Model 0.10-0.20% round-trip cost
      - **F&O futures:** Model 0.02-0.05% round-trip cost
      - **F&O options:** Model 0.50-1.50% of premium as round-trip cost (highly variable)
      
      **Always add slippage on top of commissions.**
      
      ---
      
      ## Sample Size Guidelines
      
      ### Minimum Trade Counts
      
      | Confidence Level | Minimum Trades | Notes |
      |-----------------|---------------|-------|
      | **Anecdotal** | < 30 | Cannot draw any statistical conclusions |
      | **Directional** | 30-50 | Can identify if the strategy has a positive or negative edge |
      | **Moderate** | 50-100 | Confidence intervals are still wide (+/- 30-40%) |
      | **Good** | 100-200 | Reasonable confidence, can estimate parameters |
      | **Strong** | 200-500 | Narrow confidence intervals, reliable statistics |
      | **Very Strong** | 500+ | High confidence, can detect small edges |
      
      ### Statistical Tests for Trade Significance
      
      **1. t-test for mean trade return:**
      ```
      H0: Mean trade return = 0 (no edge)
      H1: Mean trade return > 0 (positive edge)
      
      t = (mean_return - 0) / (std_return / sqrt(n))
      
      For 95% confidence:
        n=30:  t must be > 1.70
        n=50:  t must be > 1.68
        n=100: t must be > 1.66
        n=200: t must be > 1.65
      ```
      
      **2. Binomial test for win rate:**
      ```
      H0: Win rate = 50% (random)
      H1: Win rate > 50%
      
      For 60% win rate to be significant at 95%:
        Need ~70 trades minimum
      For 55% win rate:
        Need ~200 trades minimum
      For 52% win rate:
        Need ~600 trades minimum
      ```
      
      **3. Monte Carlo bootstrap:**
      - Resample trades with replacement (10,000 iterations)
      - If 95% of resampled equity curves are positive, the edge is likely real
      
      ### Data Length vs Trade Frequency
      
      | Strategy Frequency | Min Years | Rationale |
      |-------------------|-----------|-----------|
      | Intraday (5+ trades/day) | 2-3 years | Generates 1000+ trades quickly |
      | Swing (2-5 trades/week) | 3-5 years | Need multiple market regimes |
      | Positional (2-5 trades/month) | 5-8 years | Each regime must have enough trades |
      | Long-term (< 1 trade/month) | 8-15 years | Very hard to get enough trades |
      
      ---
      
      ## Market Regime Definitions
      
      ### Regime Classification for Indian Markets
      
      #### 1. Bull Market (Trending Up)
      - **Definition:** Nifty 50 above its 200 DMA, making higher highs and higher lows
      - **India VIX:** Typically < 18
      - **Advance/Decline:** Consistently > 1.5:1
      - **FII flows:** Net positive
      - **Examples:** Apr 2014 - Jan 2018, Apr 2020 - Oct 2021
      - **Characteristics:** Momentum strategies work, mean reversion is risky, broad participation
      
      #### 2. Bear Market (Trending Down)
      - **Definition:** Nifty 50 below its 200 DMA, making lower highs and lower lows
      - **India VIX:** Typically > 22
      - **Advance/Decline:** Consistently < 0.7:1
      - **FII flows:** Net negative
      - **Examples:** Jan 2008 - Mar 2009, Feb 2020 - Mar 2020
      - **Characteristics:** Short-selling works (if allowed), defensive sectors outperform, high correlation
      
      #### 3. Sideways/Range-Bound
      - **Definition:** Nifty 50 oscillating within a 10-15% range around its 200 DMA
      - **India VIX:** 12-18 (calm but uncertain)
      - **Advance/Decline:** Mixed, sector rotation
      - **Examples:** 2018-2019, H1 2023
      - **Characteristics:** Mean reversion works, options selling strategies work, trend-following underperforms
      
      #### 4. High Volatility (Crash/Spike)
      - **Definition:** India VIX > 25, Nifty moving 2%+ daily
      - **Examples:** Sep-Oct 2008, Mar 2020, Budget days with surprises
      - **Characteristics:** Stop losses get hit frequently, gap risk is extreme, option premiums explode
      
      #### 5. Low Volatility (Grind)
      - **Definition:** India VIX < 13, Nifty moving < 0.5% daily for extended periods
      - **Examples:** H2 2017, H2 2019
      - **Characteristics:** Options decay is the dominant strategy, trend-following generates false signals
      
      #### 6. Pre/Post Budget Period
      - **Definition:** 2 weeks before and 1 week after Union Budget (typically Feb 1)
      - **Characteristics:** Sector-specific moves based on expectations and announcements, high gap risk on budget day, defense/infra/rural themes dominate
      
      #### 7. Election Cycle
      - **Definition:** 6 months before and 3 months after general elections
      - **Characteristics:** Uncertainty drives volatility, post-result rally (if continuity), policy-sensitive sectors (defense, infra, PSU banks) are in focus
      
      #### 8. Monsoon Impact Period
      - **Definition:** June - September
      - **Characteristics:** Agriculture and rural economy stocks affected, FMCG rural sales, water/irrigation plays, monsoon deficit/surplus drives sentiment in agri stocks
      
      #### 9. RBI Policy Cycle
      - **Definition:** Rate hike or cut cycles (typically 6-12 months)
      - **Characteristics:** Banking and NBFC stocks are most sensitive, bond yield curve shifts affect valuations, housing finance and auto loans impacted
      
      #### 10. Global Crude Shock
      - **Definition:** Brent crude moves >30% in a quarter
      - **Characteristics:** INR weakens, OMCs (IOC, BPCL, HPCL) directly hit, paints and chemicals (crude derivatives) affected, import-heavy sectors under pressure, export sectors (IT) benefit from weak INR
      
      ---
      
      ## Common Biases
      
      ### 1. Survivorship Bias
      
      **What it is:** Only testing on stocks that exist today, ignoring delisted/merged/bankrupt companies.
      
      **Impact on Indian markets:**
      - NSE has delisted hundreds of companies since 2000
      - Companies that went bankrupt (e.g., Satyam 2009, DHFL 2019, Jet Airways) are missing from current data
      - Back-testing on "current Nifty 50" misses companies that were removed
      - Can inflate returns by 2-5% annually
      
      **How to fix:**
      - Use point-in-time universe data (Nifty 50 as of each rebalancing date)
      - Include delisted stocks with their actual returns up to delisting
      - Use databases that include survivorship-bias-free data (Bloomberg, NSE historical archives)
      
      ### 2. Look-Ahead Bias
      
      **What it is:** Using information that was not available at the time of the trading decision.
      
      **Common Indian market examples:**
      - Using quarterly results data before they were announced
      - Using index rebalancing information before the announcement date
      - Using corporate action (split, bonus) adjusted prices before the ex-date
      - Using FII/DII flow data before the NSDL report is published (usually T+1)
      
      **How to fix:**
      - Use point-in-time data exclusively
      - Add realistic delays: fundamentals available 30 days after quarter end, FII data T+1
      - Never use future price data for any calculation
      
      ### 3. Curve Fitting (Overfitting)
      
      **What it is:** Optimizing parameters to fit historical noise rather than signal.
      
      **How to detect:**
      - Strategy only works with very specific parameter values (no plateau)
      - Performance degrades dramatically with +/-10% parameter changes
      - Out-of-sample performance is much worse than in-sample
      - Strategy has more than 5 free parameters
      - Strategy includes rules that address specific historical events
      
      **How to fix:**
      - Minimize parameters (prefer 2-3)
      - Use walk-forward analysis
      - Test parameter sensitivity
      - Reserve 30% of data for out-of-sample testing
      - Compare performance to a random strategy baseline
      
      ### 4. Data-Snooping Bias
      
      **What it is:** Testing many strategies on the same data and selecting the best one without adjusting for multiple comparisons.
      
      **Example:** Testing 50 indicator combinations on Nifty data, picking the one that works best, and declaring it a "strategy."
      
      **How to fix:**
      - Start with a hypothesis BEFORE testing
      - Apply Bonferroni correction: divide significance threshold by number of tests
      - Use separate datasets for discovery and validation
      - Be honest about how many strategies you tested before finding this one
      
      ### 5. Selection Bias in Universe
      
      **What it is:** Choosing a biased stock universe that inherently favors the strategy.
      
      **Indian market examples:**
      - Only testing on stocks that subsequently performed well
      - Testing on "popular" stocks (which are popular because they went up)
      - Excluding penny stocks that would have triggered entries and then lost money
      
      **How to fix:**
      - Use a predefined, rules-based universe (e.g., "all NSE stocks above Rs 100 with daily volume > 1 Cr")
      - Include the full universe, even stocks that would have been bad trades
      - Use index constituents as of each historical date
      
      ### 6. Execution Bias
      
      **What it is:** Assuming perfect execution that is not achievable in practice.
      
      **Indian market examples:**
      - Assuming fills at the close price (actual close price is auction-determined)
      - Ignoring circuit limits (stock at upper/lower circuit cannot be bought/sold)
      - Ignoring F&O ban periods (MWPL > 95%)
      - Assuming instant fills during high-volatility periods
      - Not accounting for pre-open session mechanics
      
      **How to fix:**
      - Use next-candle-open for entry/exit (not current-candle-close)
      - Add realistic slippage (see Slippage Modeling section)
      - Code circuit limit handling (skip trade or delay)
      - Model F&O ban period restrictions
      
      ---
      
      ## Walk-Forward Analysis
      
      ### The Gold Standard for Out-of-Sample Testing
      
      Walk-forward analysis is the single most important validation technique. It simulates real-world deployment by repeatedly training and testing on different time periods.
      
      ### Step-by-Step Process
      
      ```
      Total data: 2010 ---|---|---|---|---|---|---|---|---|---|---|--- 2024
                          Y1  Y2  Y3  Y4  Y5  Y6  Y7  Y8  Y9  Y10 Y11
      
      Walk 1: Train [Y1-Y5] -----> Test [Y6-Y7]
      Walk 2: Train [Y2-Y6] -----> Test [Y7-Y8]
      Walk 3: Train [Y3-Y7] -----> Test [Y8-Y9]
      Walk 4: Train [Y4-Y8] -----> Test [Y9-Y10]
      Walk 5: Train [Y5-Y9] -----> Test [Y10-Y11]
      
      Combined OOS: Concatenate all test periods for true performance estimate.
      ```
      
      ### Walk-Forward Efficiency (WFE)
      
      ```
      WFE = (Average OOS Return per Walk) / (Average IS Return per Walk) x 100%
      ```
      
      | WFE | Interpretation |
      |-----|---------------|
      | > 70% | Excellent — strategy generalizes very well |
      | 50-70% | Good — acceptable level of overfitting |
      | 30-50% | Fair — some overfitting, but may still be tradeable |
      | < 30% | Poor — strategy is likely overfit to in-sample data |
      | < 0% | Failed — strategy loses money out-of-sample |
      
      ### Indian Market Considerations for Walk-Forward
      
      - **Structural breaks:** Indian markets had major structural changes (T+2 to T+1, algorithm trading growth, SEBI regulation changes). Walk-forward naturally handles these.
      - **Regime coverage:** Ensure each test window covers at least one significant event (budget, election, global crisis).
      - **Recalibration frequency:** For Indian markets, annual recalibration is typical for positional strategies; monthly for intraday.
      
      ---
      
      ## Statistical Validation
      
      ### Key Statistical Metrics
      
      #### 1. Sharpe Ratio (India-Adjusted)
      ```
      Sharpe = (Strategy CAGR - Risk-Free Rate) / Annualized Volatility
      
      Risk-free rate for India: Use 91-day T-bill rate (~6-7% as of 2024)
      Annualized volatility: Daily returns std x sqrt(250)
      ```
      
      | Sharpe | Interpretation |
      |--------|---------------|
      | > 2.0 | Exceptional (verify — may be overfit) |
      | 1.5-2.0 | Excellent |
      | 1.0-1.5 | Good |
      | 0.5-1.0 | Acceptable |
      | < 0.5 | Poor — risk-adjusted returns too low |
      
      #### 2. Calmar Ratio
      ```
      Calmar = CAGR / Max Drawdown
      ```
      
      | Calmar | Interpretation |
      |--------|---------------|
      | > 2.0 | Excellent |
      | 1.0-2.0 | Good |
      | 0.5-1.0 | Acceptable |
      | < 0.5 | Poor — drawdowns too large relative to returns |
      
      #### 3. Expectancy per Trade
      ```
      E = (Win% x Avg Win) - (Loss% x Avg Loss)
      ```
      
      This is the single most important number. It tells you how much you expect to make per trade, on average.
      
      #### 4. System Quality Number (SQN)
      ```
      SQN = sqrt(N) x Expectancy / StdDev(trade returns)
      
      N = number of trades (capped at 100 for this calculation)
      ```
      
      | SQN | Interpretation |
      |-----|---------------|
      | > 7.0 | Holy Grail (verify — likely overfit) |
      | 5.0-7.0 | Superb |
      | 3.0-5.0 | Excellent |
      | 2.0-3.0 | Good |
      | 1.5-2.0 | Below average |
      | < 1.5 | Difficult to trade profitably |
      
      #### 5. Payoff Ratio
      ```
      Payoff = Average Win / Average Loss
      ```
      
      | Win Rate | Min Payoff for Breakeven | Recommended Payoff |
      |----------|------------------------|--------------------|
      | 40% | 1.50 | > 2.0 |
      | 50% | 1.00 | > 1.5 |
      | 60% | 0.67 | > 1.0 |
      | 70% | 0.43 | > 0.8 |
      
      ### Benchmark Comparison
      
      Always compare your strategy against relevant Indian market benchmarks:
      
      | Benchmark | When to Use |
      |-----------|------------|
      | Nifty 50 TRI | Default for large-cap strategies |
      | Nifty Midcap 150 TRI | Midcap strategies |
      | Nifty Smallcap 250 TRI | Smallcap strategies |
      | Nifty 500 TRI | Broad market strategies |
      | Fixed Deposit (7%) | Absolute return benchmark |
      | Buy-and-hold the universe | Most relevant — shows if active management adds value |
      
      **TRI = Total Return Index** (includes dividends). Always use TRI for benchmark comparison, not the price index.
      
  • scripts
    • evaluate_backtest.py 36.4 KB
      #!/usr/bin/env python3
      """
      Backtest Evaluation Scoring Tool for Indian Markets (NSE/BSE)
      
      Evaluates a completed backtest across 5 dimensions (20 points each = 100 total):
        1. Sample Size        — Statistical significance of trade count
        2. Expectancy         — Edge per trade after Indian market costs
        3. Risk Management    — Drawdown control and profit factor
        4. Robustness         — Years tested and parameter parsimony
        5. Execution Realism  — Whether slippage/friction was modeled
      
      Outputs JSON and/or Markdown report with verdict: Deploy / Refine / Abandon.
      
      Usage:
        python3 evaluate_backtest.py \
          --total-trades 150 \
          --win-rate 62 \
          --avg-win-pct 1.8 \
          --avg-loss-pct 1.2 \
          --max-drawdown-pct 15 \
          --years-tested 8 \
          --num-parameters 3 \
          --slippage-tested
      
        python3 evaluate_backtest.py \
          --total-trades 150 \
          --win-rate 62 \
          --avg-win-pct 1.8 \
          --avg-loss-pct 1.2 \
          --max-drawdown-pct 15 \
          --years-tested 8 \
          --num-parameters 3 \
          --slippage-tested \
          --output json \
          --include-india-costs \
          --brokerage-per-trade 20 \
          --avg-trade-value 50000
      """
      
      import argparse
      import json
      import sys
      from dataclasses import dataclass, field, asdict
      from typing import List, Optional
      
      
      # ---------------------------------------------------------------------------
      # Data classes
      # ---------------------------------------------------------------------------
      
      @dataclass
      class DimensionScore:
          """Score for a single evaluation dimension."""
          name: str
          score: float
          max_score: float
          details: str
          sub_scores: dict = field(default_factory=dict)
      
      
      @dataclass
      class RedFlag:
          """A detected red flag in the backtest."""
          severity: str  # "critical", "warning", "info"
          message: str
          recommendation: str
      
      
      @dataclass
      class EvaluationResult:
          """Complete evaluation output."""
          total_score: float
          max_possible: float
          percentage: float
          verdict: str
          verdict_detail: str
          dimensions: List[DimensionScore] = field(default_factory=list)
          red_flags: List[RedFlag] = field(default_factory=list)
          raw_expectancy: float = 0.0
          adjusted_expectancy: float = 0.0
          india_cost_impact: Optional[dict] = None
          input_parameters: dict = field(default_factory=dict)
      
      
      # ---------------------------------------------------------------------------
      # India-specific cost calculations
      # ---------------------------------------------------------------------------
      
      def calculate_india_costs(
          avg_trade_value: float,
          brokerage_per_trade: float = 20.0,
          trade_type: str = "delivery",
      ) -> dict:
          """
          Calculate round-trip transaction costs for Indian markets.
      
          Args:
              avg_trade_value: Average trade value in INR.
              brokerage_per_trade: Flat brokerage per order (default Rs 20 for discount broker).
              trade_type: One of 'delivery', 'intraday', 'fno_options', 'fno_futures'.
      
          Returns:
              Dictionary with cost breakdown and total round-trip cost as percentage.
          """
          costs = {}
      
          # Brokerage (buy + sell)
          brokerage_total = brokerage_per_trade * 2
          costs["brokerage"] = brokerage_total
          costs["brokerage_pct"] = (brokerage_total / avg_trade_value) * 100
      
          # STT (Securities Transaction Tax)
          if trade_type == "delivery":
              stt = avg_trade_value * 0.001 * 2  # 0.1% on buy + sell
          elif trade_type == "intraday":
              stt = avg_trade_value * 0.00025  # 0.025% on sell only
          elif trade_type == "fno_options":
              stt = avg_trade_value * 0.000125  # 0.0125% on sell (options, on premium)
          elif trade_type == "fno_futures":
              stt = avg_trade_value * 0.000125  # 0.0125% on sell
          else:
              stt = avg_trade_value * 0.001 * 2  # Default to delivery
      
          costs["stt"] = stt
          costs["stt_pct"] = (stt / avg_trade_value) * 100
      
          # Exchange transaction charges (NSE)
          if trade_type in ("fno_options",):
              exchange_rate = 0.0005  # 0.05% for options
          else:
              exchange_rate = 0.0000345  # 0.00345% for equity
      
          exchange_charges = avg_trade_value * exchange_rate * 2  # Buy + sell
          costs["exchange_charges"] = exchange_charges
          costs["exchange_charges_pct"] = (exchange_charges / avg_trade_value) * 100
      
          # GST (18% on brokerage + exchange charges)
          gst_base = brokerage_total + exchange_charges
          gst = gst_base * 0.18
          costs["gst"] = gst
          costs["gst_pct"] = (gst / avg_trade_value) * 100
      
          # Stamp duty (on buy side)
          if trade_type == "delivery":
              stamp_duty = avg_trade_value * 0.00015  # 0.015%
          else:
              stamp_duty = avg_trade_value * 0.00003  # 0.003%
      
          costs["stamp_duty"] = stamp_duty
          costs["stamp_duty_pct"] = (stamp_duty / avg_trade_value) * 100
      
          # SEBI charges
          sebi_charges = avg_trade_value * 0.000001 * 2  # 0.0001% buy + sell
          costs["sebi_charges"] = sebi_charges
          costs["sebi_charges_pct"] = (sebi_charges / avg_trade_value) * 100
      
          # Total
          total_cost = sum([
              brokerage_total, stt, exchange_charges, gst, stamp_duty, sebi_charges
          ])
          costs["total_round_trip"] = total_cost
          costs["total_round_trip_pct"] = (total_cost / avg_trade_value) * 100
          costs["trade_type"] = trade_type
      
          return costs
      
      
      # ---------------------------------------------------------------------------
      # Dimension scoring functions
      # ---------------------------------------------------------------------------
      
      def score_sample_size(total_trades: int) -> DimensionScore:
          """
          Dimension 1: Sample Size (0-20 points).
      
          Scoring:
            < 30 trades:    0-5 pts  (statistically meaningless)
            30-49 trades:   5-8 pts  (bare minimum)
            50-99 trades:   8-12 pts (weak but usable)
            100-149 trades: 12-15 pts (moderate confidence)
            150-199 trades: 15-18 pts (good confidence)
            200+ trades:    18-20 pts (strong confidence)
          """
          if total_trades < 30:
              score = max(0, total_trades / 6)  # 0-5
              quality = "Statistically meaningless"
          elif total_trades < 50:
              score = 5 + (total_trades - 30) * (3 / 20)  # 5-8
              quality = "Bare minimum"
          elif total_trades < 100:
              score = 8 + (total_trades - 50) * (4 / 50)  # 8-12
              quality = "Weak but usable"
          elif total_trades < 150:
              score = 12 + (total_trades - 100) * (3 / 50)  # 12-15
              quality = "Moderate confidence"
          elif total_trades < 200:
              score = 15 + (total_trades - 150) * (3 / 50)  # 15-18
              quality = "Good confidence"
          else:
              score = 18 + min(2, (total_trades - 200) / 100)  # 18-20
              quality = "Strong confidence"
      
          score = round(min(20, score), 1)
      
          return DimensionScore(
              name="Sample Size",
              score=score,
              max_score=20.0,
              details=f"{total_trades} trades — {quality}",
              sub_scores={
                  "total_trades": total_trades,
                  "quality_label": quality,
              },
          )
      
      
      def score_expectancy(
          win_rate: float,
          avg_win_pct: float,
          avg_loss_pct: float,
          cost_pct: float = 0.0,
      ) -> DimensionScore:
          """
          Dimension 2: Expectancy (0-20 points).
      
          Expectancy = (win_rate * avg_win) - (loss_rate * avg_loss) - costs_per_trade
      
          Scoring:
            E <= 0:        0 pts   (no edge)
            E 0-0.1%:      0-5 pts (marginal edge, may vanish with costs)
            E 0.1-0.3%:    5-10 pts (small but real edge)
            E 0.3-0.6%:    10-15 pts (solid edge)
            E 0.6-1.0%:    15-18 pts (strong edge)
            E > 1.0%:      18-20 pts (exceptional — verify it is real)
          """
          win_rate_decimal = win_rate / 100.0
          loss_rate_decimal = 1.0 - win_rate_decimal
      
          raw_expectancy = (win_rate_decimal * avg_win_pct) - (loss_rate_decimal * avg_loss_pct)
          adjusted_expectancy = raw_expectancy - cost_pct
      
          e = adjusted_expectancy
      
          if e <= 0:
              score = 0
              quality = "No edge (negative or zero expectancy)"
          elif e <= 0.1:
              score = e / 0.1 * 5  # 0-5
              quality = "Marginal edge — may vanish with real costs"
          elif e <= 0.3:
              score = 5 + (e - 0.1) / 0.2 * 5  # 5-10
              quality = "Small but real edge"
          elif e <= 0.6:
              score = 10 + (e - 0.3) / 0.3 * 5  # 10-15
              quality = "Solid edge"
          elif e <= 1.0:
              score = 15 + (e - 0.6) / 0.4 * 3  # 15-18
              quality = "Strong edge"
          else:
              score = 18 + min(2, (e - 1.0) / 0.5)  # 18-20
              quality = "Exceptional edge — verify it is real"
      
          score = round(min(20, score), 1)
      
          # Profit factor
          gross_profit = win_rate_decimal * avg_win_pct
          gross_loss = loss_rate_decimal * avg_loss_pct
          profit_factor = gross_profit / gross_loss if gross_loss > 0 else float("inf")
      
          return DimensionScore(
              name="Expectancy",
              score=score,
              max_score=20.0,
              details=(
                  f"Raw E = {raw_expectancy:.4f}%, "
                  f"Adjusted E = {adjusted_expectancy:.4f}% (after {cost_pct:.4f}% costs), "
                  f"Profit Factor = {profit_factor:.2f} — {quality}"
              ),
              sub_scores={
                  "raw_expectancy_pct": round(raw_expectancy, 4),
                  "adjusted_expectancy_pct": round(adjusted_expectancy, 4),
                  "cost_pct_per_trade": round(cost_pct, 4),
                  "profit_factor": round(profit_factor, 2),
                  "quality_label": quality,
              },
          )
      
      
      def score_risk_management(
          max_drawdown_pct: float,
          win_rate: float,
          avg_win_pct: float,
          avg_loss_pct: float,
      ) -> DimensionScore:
          """
          Dimension 3: Risk Management (0-20 points).
      
          Two sub-components:
            A. Max Drawdown Score (0-12 points):
               < 10%:  10-12 pts
               10-15%: 8-10 pts
               15-25%: 5-8 pts
               25-35%: 2-5 pts
               > 35%:  0-2 pts
      
            B. Profit Factor Score (0-8 points):
               PF > 2.0:   7-8 pts
               PF 1.5-2.0: 5-7 pts
               PF 1.2-1.5: 3-5 pts
               PF 1.0-1.2: 1-3 pts
               PF < 1.0:   0 pts
          """
          # Sub-score A: Max Drawdown
          if max_drawdown_pct < 10:
              dd_score = 10 + (10 - max_drawdown_pct) / 10 * 2  # 10-12
          elif max_drawdown_pct < 15:
              dd_score = 8 + (15 - max_drawdown_pct) / 5 * 2  # 8-10
          elif max_drawdown_pct < 25:
              dd_score = 5 + (25 - max_drawdown_pct) / 10 * 3  # 5-8
          elif max_drawdown_pct < 35:
              dd_score = 2 + (35 - max_drawdown_pct) / 10 * 3  # 2-5
          else:
              dd_score = max(0, 2 - (max_drawdown_pct - 35) / 15 * 2)  # 0-2
      
          dd_score = round(min(12, max(0, dd_score)), 1)
      
          # Sub-score B: Profit Factor
          win_rate_decimal = win_rate / 100.0
          loss_rate_decimal = 1.0 - win_rate_decimal
          gross_profit = win_rate_decimal * avg_win_pct
          gross_loss = loss_rate_decimal * avg_loss_pct
          profit_factor = gross_profit / gross_loss if gross_loss > 0 else float("inf")
      
          if profit_factor >= 2.0:
              pf_score = 7 + min(1, (profit_factor - 2.0) / 1.0)  # 7-8
          elif profit_factor >= 1.5:
              pf_score = 5 + (profit_factor - 1.5) / 0.5 * 2  # 5-7
          elif profit_factor >= 1.2:
              pf_score = 3 + (profit_factor - 1.2) / 0.3 * 2  # 3-5
          elif profit_factor >= 1.0:
              pf_score = 1 + (profit_factor - 1.0) / 0.2 * 2  # 1-3
          else:
              pf_score = 0
      
          pf_score = round(min(8, max(0, pf_score)), 1)
      
          total_score = round(dd_score + pf_score, 1)
      
          return DimensionScore(
              name="Risk Management",
              score=total_score,
              max_score=20.0,
              details=(
                  f"Max Drawdown: {max_drawdown_pct}% (score {dd_score}/12), "
                  f"Profit Factor: {profit_factor:.2f} (score {pf_score}/8)"
              ),
              sub_scores={
                  "max_drawdown_pct": max_drawdown_pct,
                  "drawdown_score": dd_score,
                  "profit_factor": round(profit_factor, 2),
                  "profit_factor_score": pf_score,
              },
          )
      
      
      def score_robustness(years_tested: float, num_parameters: int) -> DimensionScore:
          """
          Dimension 4: Robustness (0-20 points).
      
          Two sub-components:
            A. Years Tested (0-12 points):
               < 3 years:    0-4 pts
               3-5 years:    4-7 pts
               5-8 years:    7-10 pts
               8-10 years:   10-11 pts
               10+ years:    11-12 pts
      
            B. Parameter Parsimony (0-8 points):
               1-2 params:   7-8 pts (simple, robust)
               3-4 params:   5-7 pts (acceptable)
               5-6 params:   2-5 pts (getting complex)
               7+ params:    0-2 pts (likely overfit)
          """
          # Sub-score A: Years tested
          if years_tested < 3:
              years_score = years_tested / 3 * 4  # 0-4
          elif years_tested < 5:
              years_score = 4 + (years_tested - 3) / 2 * 3  # 4-7
          elif years_tested < 8:
              years_score = 7 + (years_tested - 5) / 3 * 3  # 7-10
          elif years_tested < 10:
              years_score = 10 + (years_tested - 8) / 2 * 1  # 10-11
          else:
              years_score = 11 + min(1, (years_tested - 10) / 5)  # 11-12
      
          years_score = round(min(12, max(0, years_score)), 1)
      
          # Sub-score B: Parameter count
          if num_parameters <= 2:
              param_score = 7 + (2 - num_parameters) * 0.5  # 7-8
          elif num_parameters <= 4:
              param_score = 5 + (4 - num_parameters) / 2 * 2  # 5-7
          elif num_parameters <= 6:
              param_score = 2 + (6 - num_parameters) / 2 * 3  # 2-5
          else:
              param_score = max(0, 2 - (num_parameters - 6) * 0.5)  # 0-2
      
          param_score = round(min(8, max(0, param_score)), 1)
      
          total_score = round(years_score + param_score, 1)
      
          return DimensionScore(
              name="Robustness",
              score=total_score,
              max_score=20.0,
              details=(
                  f"{years_tested} years tested (score {years_score}/12), "
                  f"{num_parameters} parameters (score {param_score}/8)"
              ),
              sub_scores={
                  "years_tested": years_tested,
                  "years_score": years_score,
                  "num_parameters": num_parameters,
                  "parameter_score": param_score,
              },
          )
      
      
      def score_execution_realism(slippage_tested: bool) -> DimensionScore:
          """
          Dimension 5: Execution Realism (0-20 points).
      
          Binary for now — did the backtest model slippage and transaction costs?
            Yes: 20 pts
            No:  5 pts (some credit for doing a backtest at all, but major penalty)
          """
          if slippage_tested:
              score = 20.0
              details = "Slippage and execution friction were modeled — full marks"
          else:
              score = 5.0
              details = (
                  "Slippage/friction NOT modeled — results are likely optimistic. "
                  "For Indian markets, expect 0.1-0.5% round-trip cost reduction."
              )
      
          return DimensionScore(
              name="Execution Realism",
              score=score,
              max_score=20.0,
              details=details,
              sub_scores={"slippage_tested": slippage_tested},
          )
      
      
      # ---------------------------------------------------------------------------
      # Red flag detection
      # ---------------------------------------------------------------------------
      
      def detect_red_flags(
          total_trades: int,
          win_rate: float,
          avg_win_pct: float,
          avg_loss_pct: float,
          max_drawdown_pct: float,
          years_tested: float,
          num_parameters: int,
          slippage_tested: bool,
          adjusted_expectancy: float,
      ) -> List[RedFlag]:
          """Detect red flags and warnings in the backtest results."""
          flags = []
      
          # Critical: Negative expectancy
          if adjusted_expectancy <= 0:
              flags.append(RedFlag(
                  severity="critical",
                  message="Negative or zero expectancy after costs — no trading edge exists.",
                  recommendation=(
                      "Re-examine the hypothesis. Either the edge does not exist, "
                      "or transaction costs destroy it. Consider wider targets or "
                      "lower-frequency trading to reduce cost impact."
                  ),
              ))
      
          # Critical: Tiny sample
          if total_trades < 30:
              flags.append(RedFlag(
                  severity="critical",
                  message=f"Only {total_trades} trades — statistically meaningless.",
                  recommendation=(
                      "Expand the universe, extend the time period, or lower entry "
                      "thresholds to generate more trades. Minimum 100 trades for "
                      "any confidence."
                  ),
              ))
      
          # Critical: Too many parameters
          if num_parameters > 5:
              flags.append(RedFlag(
                  severity="critical",
                  message=f"{num_parameters} parameters — high risk of overfitting.",
                  recommendation=(
                      "Reduce free parameters to 3-4. Each additional parameter "
                      "increases the risk of curve-fitting. Ask: does removing this "
                      "parameter destroy the strategy, or just reduce backtest returns?"
                  ),
              ))
      
          # Critical: Slippage not tested
          if not slippage_tested:
              flags.append(RedFlag(
                  severity="critical",
                  message="Slippage and execution friction not modeled.",
                  recommendation=(
                      "Re-run with realistic costs. For NSE: add 0.05-0.1% slippage "
                      "for large-caps, 0.1-0.3% for mid/small-caps, plus brokerage "
                      "(Rs 20/order), STT (0.1% delivery), and other charges."
                  ),
              ))
      
          # Warning: Suspiciously high win rate
          if win_rate > 80:
              flags.append(RedFlag(
                  severity="warning",
                  message=f"Win rate of {win_rate}% is suspiciously high.",
                  recommendation=(
                      "Verify that fills are realistic. Check for look-ahead bias "
                      "(using future data for entry/exit decisions). High win rates "
                      "often come with large average losses — check risk/reward ratio."
                  ),
              ))
      
          # Warning: Short test period
          if years_tested < 5:
              flags.append(RedFlag(
                  severity="warning",
                  message=f"Only {years_tested} years tested — may miss market regime changes.",
                  recommendation=(
                      "Extend to at least 5 years, ideally 8-10. Indian markets "
                      "had distinct regimes: 2008 crash, 2014-17 bull, 2018-19 "
                      "sideways, 2020 COVID crash/recovery, 2021 broad bull, "
                      "2022 selective, 2023-24 mixed."
                  ),
              ))
      
          # Warning: Large drawdown
          if max_drawdown_pct > 30:
              flags.append(RedFlag(
                  severity="warning",
                  message=f"Max drawdown of {max_drawdown_pct}% is psychologically challenging.",
                  recommendation=(
                      "Most traders abandon strategies during 25%+ drawdowns. "
                      "Consider reducing position size, adding a drawdown circuit "
                      "breaker (e.g., pause after 15% drawdown), or tightening stops."
                  ),
              ))
      
          # Warning: Marginal expectancy
          if 0 < adjusted_expectancy < 0.1:
              flags.append(RedFlag(
                  severity="warning",
                  message=f"Expectancy of {adjusted_expectancy:.4f}% is marginal.",
                  recommendation=(
                      "This edge is close to zero and may disappear with slight "
                      "changes in market conditions, slippage, or costs. Consider "
                      "whether the edge is large enough to be worth the effort."
                  ),
              ))
      
          # Info: Moderate sample
          if 30 <= total_trades < 100:
              flags.append(RedFlag(
                  severity="info",
                  message=f"{total_trades} trades is below the recommended 100+ threshold.",
                  recommendation=(
                      "Results are directionally useful but confidence intervals "
                      "are wide. Try to increase sample size before deploying capital."
                  ),
              ))
      
          # Info: Win/loss asymmetry check
          if avg_loss_pct > avg_win_pct * 2:
              flags.append(RedFlag(
                  severity="warning",
                  message=(
                      f"Average loss ({avg_loss_pct}%) is more than 2x average win "
                      f"({avg_win_pct}%). Risk/reward is inverted."
                  ),
                  recommendation=(
                      "This pattern (high win rate, large losses) is fragile. "
                      "A few bad trades can wipe out many winners. Consider "
                      "tightening stops or widening targets."
                  ),
              ))
      
          return flags
      
      
      # ---------------------------------------------------------------------------
      # Verdict logic
      # ---------------------------------------------------------------------------
      
      def determine_verdict(total_score: float) -> tuple:
          """Return (verdict, detail) based on total score."""
          if total_score >= 80:
              return (
                  "DEPLOY",
                  (
                      "Strategy shows strong evidence of a real edge. Start with 25% "
                      "of intended size and scale up after 50+ live trades confirm "
                      "out-of-sample performance."
                  ),
              )
          elif total_score >= 60:
              return (
                  "REFINE",
                  (
                      "Strategy has potential but needs improvement. Identify the "
                      "weakest scoring dimension and address it. Re-run evaluation "
                      "after changes."
                  ),
              )
          elif total_score >= 40:
              return (
                  "REFINE WITH CAUTION",
                  (
                      "Multiple dimensions are weak. The strategy may be salvageable "
                      "but requires significant rework. Consider whether the "
                      "hypothesis itself is valid before investing more time."
                  ),
              )
          else:
              return (
                  "ABANDON",
                  (
                      "The backtest does not provide evidence of a tradeable edge. "
                      "Document what you learned and move on to a different hypothesis. "
                      "Failed backtests are valuable — they narrow the search space."
                  ),
              )
      
      
      # ---------------------------------------------------------------------------
      # Main evaluation
      # ---------------------------------------------------------------------------
      
      def evaluate_backtest(
          total_trades: int,
          win_rate: float,
          avg_win_pct: float,
          avg_loss_pct: float,
          max_drawdown_pct: float,
          years_tested: float,
          num_parameters: int,
          slippage_tested: bool = False,
          include_india_costs: bool = False,
          brokerage_per_trade: float = 20.0,
          avg_trade_value: float = 50000.0,
          trade_type: str = "delivery",
      ) -> EvaluationResult:
          """Run full 5-dimension evaluation and return result."""
      
          # Calculate India-specific costs if requested
          india_costs = None
          cost_pct_per_trade = 0.0
      
          if include_india_costs:
              india_costs = calculate_india_costs(
                  avg_trade_value=avg_trade_value,
                  brokerage_per_trade=brokerage_per_trade,
                  trade_type=trade_type,
              )
              cost_pct_per_trade = india_costs["total_round_trip_pct"]
      
          # Score each dimension
          d1 = score_sample_size(total_trades)
          d2 = score_expectancy(win_rate, avg_win_pct, avg_loss_pct, cost_pct_per_trade)
          d3 = score_risk_management(max_drawdown_pct, win_rate, avg_win_pct, avg_loss_pct)
          d4 = score_robustness(years_tested, num_parameters)
          d5 = score_execution_realism(slippage_tested)
      
          dimensions = [d1, d2, d3, d4, d5]
          total_score = round(sum(d.score for d in dimensions), 1)
          max_possible = sum(d.max_score for d in dimensions)
          percentage = round(total_score / max_possible * 100, 1)
      
          # Raw and adjusted expectancy for the result
          win_rate_decimal = win_rate / 100.0
          loss_rate_decimal = 1.0 - win_rate_decimal
          raw_expectancy = (win_rate_decimal * avg_win_pct) - (loss_rate_decimal * avg_loss_pct)
          adjusted_expectancy = raw_expectancy - cost_pct_per_trade
      
          # Detect red flags
          red_flags = detect_red_flags(
              total_trades=total_trades,
              win_rate=win_rate,
              avg_win_pct=avg_win_pct,
              avg_loss_pct=avg_loss_pct,
              max_drawdown_pct=max_drawdown_pct,
              years_tested=years_tested,
              num_parameters=num_parameters,
              slippage_tested=slippage_tested,
              adjusted_expectancy=adjusted_expectancy,
          )
      
          # Determine verdict
          verdict, verdict_detail = determine_verdict(total_score)
      
          # Override verdict if critical red flags exist
          critical_flags = [f for f in red_flags if f.severity == "critical"]
          if critical_flags and verdict == "DEPLOY":
              verdict = "REFINE"
              verdict_detail = (
                  f"Score qualifies for deployment, but {len(critical_flags)} critical "
                  f"red flag(s) detected. Address these before deploying: "
                  + "; ".join(f.message for f in critical_flags)
              )
      
          return EvaluationResult(
              total_score=total_score,
              max_possible=max_possible,
              percentage=percentage,
              verdict=verdict,
              verdict_detail=verdict_detail,
              dimensions=dimensions,
              red_flags=red_flags,
              raw_expectancy=round(raw_expectancy, 4),
              adjusted_expectancy=round(adjusted_expectancy, 4),
              india_cost_impact=india_costs,
              input_parameters={
                  "total_trades": total_trades,
                  "win_rate": win_rate,
                  "avg_win_pct": avg_win_pct,
                  "avg_loss_pct": avg_loss_pct,
                  "max_drawdown_pct": max_drawdown_pct,
                  "years_tested": years_tested,
                  "num_parameters": num_parameters,
                  "slippage_tested": slippage_tested,
                  "include_india_costs": include_india_costs,
                  "brokerage_per_trade": brokerage_per_trade,
                  "avg_trade_value": avg_trade_value,
                  "trade_type": trade_type,
              },
          )
      
      
      # ---------------------------------------------------------------------------
      # Output formatters
      # ---------------------------------------------------------------------------
      
      def result_to_dict(result: EvaluationResult) -> dict:
          """Convert result to a JSON-serializable dictionary."""
          return {
              "total_score": result.total_score,
              "max_possible": result.max_possible,
              "percentage": result.percentage,
              "verdict": result.verdict,
              "verdict_detail": result.verdict_detail,
              "raw_expectancy_pct": result.raw_expectancy,
              "adjusted_expectancy_pct": result.adjusted_expectancy,
              "dimensions": [
                  {
                      "name": d.name,
                      "score": d.score,
                      "max_score": d.max_score,
                      "details": d.details,
                      "sub_scores": d.sub_scores,
                  }
                  for d in result.dimensions
              ],
              "red_flags": [
                  {
                      "severity": f.severity,
                      "message": f.message,
                      "recommendation": f.recommendation,
                  }
                  for f in result.red_flags
              ],
              "india_cost_impact": result.india_cost_impact,
              "input_parameters": result.input_parameters,
          }
      
      
      def result_to_markdown(result: EvaluationResult) -> str:
          """Format result as a readable Markdown report."""
          lines = []
      
          # Header
          lines.append("# Backtest Evaluation Report")
          lines.append("")
      
          # Score summary
          bar_filled = int(result.percentage / 5)
          bar_empty = 20 - bar_filled
          bar = "[" + "#" * bar_filled + "-" * bar_empty + "]"
          lines.append(f"## Overall Score: {result.total_score} / {result.max_possible} ({result.percentage}%)")
          lines.append(f"```")
          lines.append(f"  {bar} {result.percentage}%")
          lines.append(f"```")
          lines.append("")
      
          # Verdict
          lines.append(f"## Verdict: {result.verdict}")
          lines.append(f"> {result.verdict_detail}")
          lines.append("")
      
          # Expectancy
          lines.append(f"## Expectancy")
          lines.append(f"- Raw Expectancy: **{result.raw_expectancy}%** per trade")
          lines.append(f"- Adjusted Expectancy: **{result.adjusted_expectancy}%** per trade (after costs)")
          lines.append("")
      
          # Dimension breakdown
          lines.append("## Dimension Scores")
          lines.append("")
          lines.append("| # | Dimension | Score | Max | Details |")
          lines.append("|---|-----------|-------|-----|---------|")
          for i, d in enumerate(result.dimensions, 1):
              lines.append(f"| {i} | {d.name} | {d.score} | {d.max_score} | {d.details} |")
          lines.append("")
      
          # India cost impact
          if result.india_cost_impact:
              costs = result.india_cost_impact
              lines.append("## India Transaction Cost Breakdown")
              lines.append(f"- Trade Type: **{costs['trade_type']}**")
              lines.append(f"- Brokerage: Rs {costs['brokerage']:.2f} ({costs['brokerage_pct']:.4f}%)")
              lines.append(f"- STT: Rs {costs['stt']:.2f} ({costs['stt_pct']:.4f}%)")
              lines.append(f"- Exchange Charges: Rs {costs['exchange_charges']:.2f} ({costs['exchange_charges_pct']:.4f}%)")
              lines.append(f"- GST: Rs {costs['gst']:.2f} ({costs['gst_pct']:.4f}%)")
              lines.append(f"- Stamp Duty: Rs {costs['stamp_duty']:.2f} ({costs['stamp_duty_pct']:.4f}%)")
              lines.append(f"- SEBI Charges: Rs {costs['sebi_charges']:.2f} ({costs['sebi_charges_pct']:.4f}%)")
              lines.append(f"- **Total Round-trip: Rs {costs['total_round_trip']:.2f} ({costs['total_round_trip_pct']:.4f}%)**")
              lines.append("")
      
          # Red flags
          if result.red_flags:
              lines.append("## Red Flags & Warnings")
              lines.append("")
              for flag in result.red_flags:
                  icon = {"critical": "[CRITICAL]", "warning": "[WARNING]", "info": "[INFO]"}
                  lines.append(f"### {icon.get(flag.severity, '[FLAG]')} {flag.message}")
                  lines.append(f"> {flag.recommendation}")
                  lines.append("")
          else:
              lines.append("## Red Flags & Warnings")
              lines.append("No red flags detected.")
              lines.append("")
      
          # Input parameters
          lines.append("## Input Parameters")
          params = result.input_parameters
          lines.append(f"- Total Trades: {params['total_trades']}")
          lines.append(f"- Win Rate: {params['win_rate']}%")
          lines.append(f"- Average Win: {params['avg_win_pct']}%")
          lines.append(f"- Average Loss: {params['avg_loss_pct']}%")
          lines.append(f"- Max Drawdown: {params['max_drawdown_pct']}%")
          lines.append(f"- Years Tested: {params['years_tested']}")
          lines.append(f"- Number of Parameters: {params['num_parameters']}")
          lines.append(f"- Slippage Tested: {'Yes' if params['slippage_tested'] else 'No'}")
          if params.get("include_india_costs"):
              lines.append(f"- India Costs Included: Yes")
              lines.append(f"- Brokerage per Trade: Rs {params['brokerage_per_trade']}")
              lines.append(f"- Average Trade Value: Rs {params['avg_trade_value']}")
              lines.append(f"- Trade Type: {params['trade_type']}")
          lines.append("")
      
          # CLI command to reproduce
          lines.append("## Reproduce This Evaluation")
          lines.append("```bash")
          cmd_parts = [
              "python3 evaluate_backtest.py",
              f"  --total-trades {params['total_trades']}",
              f"  --win-rate {params['win_rate']}",
              f"  --avg-win-pct {params['avg_win_pct']}",
              f"  --avg-loss-pct {params['avg_loss_pct']}",
              f"  --max-drawdown-pct {params['max_drawdown_pct']}",
              f"  --years-tested {params['years_tested']}",
              f"  --num-parameters {params['num_parameters']}",
          ]
          if params["slippage_tested"]:
              cmd_parts.append("  --slippage-tested")
          if params.get("include_india_costs"):
              cmd_parts.append("  --include-india-costs")
              cmd_parts.append(f"  --brokerage-per-trade {params['brokerage_per_trade']}")
              cmd_parts.append(f"  --avg-trade-value {params['avg_trade_value']}")
              cmd_parts.append(f"  --trade-type {params['trade_type']}")
          lines.append(" \\\n".join(cmd_parts))
          lines.append("```")
      
          return "\n".join(lines)
      
      
      # ---------------------------------------------------------------------------
      # CLI
      # ---------------------------------------------------------------------------
      
      def build_parser() -> argparse.ArgumentParser:
          """Build the argument parser."""
          parser = argparse.ArgumentParser(
              description=(
                  "Evaluate a completed backtest across 5 dimensions (100-point scale). "
                  "Designed for Indian market strategies (NSE/BSE)."
              ),
              formatter_class=argparse.RawDescriptionHelpFormatter,
              epilog="""
      Examples:
        # Basic evaluation
        python3 evaluate_backtest.py --total-trades 150 --win-rate 62 \\
          --avg-win-pct 1.8 --avg-loss-pct 1.2 --max-drawdown-pct 15 \\
          --years-tested 8 --num-parameters 3 --slippage-tested
      
        # With India-specific cost analysis
        python3 evaluate_backtest.py --total-trades 200 --win-rate 55 \\
          --avg-win-pct 2.5 --avg-loss-pct 1.5 --max-drawdown-pct 20 \\
          --years-tested 10 --num-parameters 4 --slippage-tested \\
          --include-india-costs --avg-trade-value 100000 --trade-type delivery
      
        # JSON output for programmatic use
        python3 evaluate_backtest.py --total-trades 150 --win-rate 62 \\
          --avg-win-pct 1.8 --avg-loss-pct 1.2 --max-drawdown-pct 15 \\
          --years-tested 8 --num-parameters 3 --output json
      
      Scoring:
        80-100 = DEPLOY    | 60-79 = REFINE
        40-59  = REFINE WITH CAUTION | 0-39  = ABANDON
              """,
          )
      
          # Required parameters
          parser.add_argument(
              "--total-trades", type=int, required=True,
              help="Total number of trades in the backtest",
          )
          parser.add_argument(
              "--win-rate", type=float, required=True,
              help="Win rate as percentage (e.g., 62 for 62%%)",
          )
          parser.add_argument(
              "--avg-win-pct", type=float, required=True,
              help="Average winning trade return in %% (e.g., 1.8 for 1.8%%)",
          )
          parser.add_argument(
              "--avg-loss-pct", type=float, required=True,
              help="Average losing trade return in %% as a positive number (e.g., 1.2 for -1.2%%)",
          )
          parser.add_argument(
              "--max-drawdown-pct", type=float, required=True,
              help="Maximum drawdown in %% (e.g., 15 for 15%%)",
          )
          parser.add_argument(
              "--years-tested", type=float, required=True,
              help="Number of years of historical data tested",
          )
          parser.add_argument(
              "--num-parameters", type=int, required=True,
              help="Number of free/optimizable parameters in the strategy",
          )
      
          # Optional flags
          parser.add_argument(
              "--slippage-tested", action="store_true", default=False,
              help="Flag indicating slippage and execution friction were modeled",
          )
      
          # India cost modeling
          parser.add_argument(
              "--include-india-costs", action="store_true", default=False,
              help="Calculate and include India-specific transaction costs in expectancy",
          )
          parser.add_argument(
              "--brokerage-per-trade", type=float, default=20.0,
              help="Brokerage per order in INR (default: 20 for discount brokers)",
          )
          parser.add_argument(
              "--avg-trade-value", type=float, default=50000.0,
              help="Average trade value in INR (default: 50000)",
          )
          parser.add_argument(
              "--trade-type", type=str, default="delivery",
              choices=["delivery", "intraday", "fno_options", "fno_futures"],
              help="Type of trade for cost calculation (default: delivery)",
          )
      
          # Output format
          parser.add_argument(
              "--output", type=str, default="markdown",
              choices=["markdown", "json", "both"],
              help="Output format (default: markdown)",
          )
      
          return parser
      
      
      def main():
          """Main entry point."""
          parser = build_parser()
          args = parser.parse_args()
      
          # Validate inputs
          if args.win_rate < 0 or args.win_rate > 100:
              parser.error("--win-rate must be between 0 and 100")
          if args.avg_win_pct < 0:
              parser.error("--avg-win-pct must be non-negative")
          if args.avg_loss_pct < 0:
              parser.error("--avg-loss-pct must be non-negative (enter as positive number)")
          if args.max_drawdown_pct < 0:
              parser.error("--max-drawdown-pct must be non-negative")
          if args.total_trades < 1:
              parser.error("--total-trades must be at least 1")
          if args.years_tested <= 0:
              parser.error("--years-tested must be positive")
          if args.num_parameters < 0:
              parser.error("--num-parameters must be non-negative")
      
          # Run evaluation
          result = evaluate_backtest(
              total_trades=args.total_trades,
              win_rate=args.win_rate,
              avg_win_pct=args.avg_win_pct,
              avg_loss_pct=args.avg_loss_pct,
              max_drawdown_pct=args.max_drawdown_pct,
              years_tested=args.years_tested,
              num_parameters=args.num_parameters,
              slippage_tested=args.slippage_tested,
              include_india_costs=args.include_india_costs,
              brokerage_per_trade=args.brokerage_per_trade,
              avg_trade_value=args.avg_trade_value,
              trade_type=args.trade_type,
          )
      
          # Output
          if args.output in ("json", "both"):
              print(json.dumps(result_to_dict(result), indent=2))
      
          if args.output == "both":
              print("\n" + "=" * 80 + "\n")
      
          if args.output in ("markdown", "both"):
              print(result_to_markdown(result))
      
          # Exit code based on verdict
          exit_codes = {"DEPLOY": 0, "REFINE": 1, "REFINE WITH CAUTION": 2, "ABANDON": 3}
          sys.exit(exit_codes.get(result.verdict, 1))
      
      
      if __name__ == "__main__":
          main()
      
  • SKILL.md 11.1 KB
    ---
    name: backtest-expert
    description: >
      Expert guidance for systematic backtesting of trading strategies on Indian markets (NSE/BSE).
      Use when developing strategies, testing robustness, avoiding overfitting, or validating trading ideas.
    ---
    
    # Backtest Expert — Indian Market Strategy Validation
    
    ## Core Philosophy
    
    > **"Find strategies that break the least, not profit the most."**
    
    A strategy that survives stress testing across multiple market regimes, transaction cost assumptions, and parameter perturbations is far more valuable than one that shows spectacular returns on a single optimized parameter set. Overfitting is the silent killer of trading accounts.
    
    ---
    
    ## 6-Step Backtesting Workflow
    
    ### Step 1: State the Hypothesis (1 Sentence Edge)
    
    Before writing a single line of code, articulate why the strategy should work in one clear sentence.
    
    **Good hypotheses:**
    - "Stocks that gap up >3% on above-average volume after consolidation tend to continue higher for 2-5 days on NSE."
    - "Nifty 50 stocks that revert to their 20-day mean after RSI drops below 30 produce positive expectancy within 5 trading sessions."
    - "Selling strangles on Bank Nifty on Wednesday expiry with delta <0.15 captures time decay faster than gamma risk materializes."
    
    **Bad hypotheses:**
    - "This indicator combination looks good on the chart." (no edge articulated)
    - "I saw someone on Twitter making money with this." (no reasoning)
    
    **Ask yourself:**
    - What behavioral or structural edge am I exploiting?
    - Why would this edge persist? (Structural > Behavioral > Statistical)
    - Who is on the other side of this trade, and why are they losing?
    
    ---
    
    ### Step 2: Codify Rules (No Ambiguity)
    
    Every rule must be binary — a computer must be able to execute it without interpretation.
    
    #### Rule Categories
    
    | Category | What to Define | Example |
    |----------|---------------|---------|
    | **Universe** | Which stocks/instruments | Nifty 200 constituents, F&O stocks only, market cap >5000 Cr |
    | **Entry** | Exact trigger conditions | Close > 20 EMA AND RSI(14) crosses above 40 AND volume > 1.5x 20-day avg |
    | **Exit — Target** | Profit-taking rule | Close 3% above entry OR trailing stop of 1.5 ATR |
    | **Exit — Stop** | Loss-cutting rule | Close below entry-day low OR 2% fixed stop |
    | **Exit — Time** | Maximum holding period | Exit after 10 trading sessions if neither target nor stop hit |
    | **Position Sizing** | How much capital per trade | 5% of equity per position, max 10 concurrent positions |
    | **Filters** | When NOT to trade | Skip if stock is in F&O ban period, skip 2 days around results |
    
    #### India-Specific Rules to Consider
    - **Circuit limits:** Stocks hitting upper/lower circuit cannot be exited. Define handling.
    - **F&O ban period:** Stocks crossing 95% MWPL cannot add fresh F&O positions.
    - **T+1 settlement:** Cash equity settles next trading day (changed from T+2 in 2023).
    - **Pre-open session:** 9:00-9:08 AM orders, 9:08-9:15 AM matching. Define if you use pre-open.
    - **Muhurat trading:** Special Diwali session — include or exclude?
    - **Corporate actions:** Adjust for splits, bonuses, dividends, rights issues.
    
    ---
    
    ### Step 3: Run Initial Backtest
    
    #### Minimum Requirements
    
    | Parameter | Minimum | Recommended |
    |-----------|---------|-------------|
    | **Time period** | 5 years | 8-10+ years |
    | **Number of trades** | 100 | 200+ |
    | **Market regimes covered** | 2 (bull + bear) | 4+ (bull, bear, sideways, high-vol) |
    | **Data quality** | Adjusted for corporate actions | Survivorship-bias-free universe |
    
    #### Indian Market Regimes to Cover
    
    | Regime | Period Examples | Characteristics |
    |--------|----------------|-----------------|
    | **Bull market** | 2014-2017, 2020-2021 | Nifty trending up, broad participation |
    | **Bear market** | 2008, 2020 (Mar), 2022 (Jun) | Sharp drawdowns, high correlation |
    | **Sideways/Range** | 2018-2019, 2023 H1 | Nifty in 10% range, stock-specific moves |
    | **High volatility** | 2008, 2020, Budget days | India VIX > 25 |
    | **Low volatility** | 2017, 2021 H2 | India VIX < 15 |
    | **Pre/Post Budget** | Every Feb 1 | Gap moves, policy-driven sectors |
    | **Election cycle** | 2014, 2019, 2024 | Uncertainty then rally pattern |
    | **Monsoon impact** | Jun-Sep annually | Agri, FMCG, rural economy impact |
    | **RBI policy shifts** | Rate hike/cut cycles | Banking, NBFC, rate-sensitive sectors |
    | **Global crude shock** | 2018, 2022 | INR weakness, OMC impact, inflation |
    
    #### Key Metrics to Record
    
    ```
    Returns: CAGR, total return, monthly returns distribution
    Risk: Max drawdown, average drawdown, drawdown duration, Calmar ratio
    Efficiency: Sharpe ratio (use 6% risk-free for India), Sortino ratio
    Trade quality: Win rate, avg win/loss, profit factor, expectancy per trade
    Consistency: % profitable months, worst month, longest losing streak
    ```
    
    ---
    
    ### Step 4: Stress Test (Spend 80% of Your Time Here)
    
    This is where most backtests fail — and where the real value lies.
    
    #### 4a. Parameter Sensitivity
    
    Perturb every parameter by +/-20% and check if performance degrades gracefully or collapses.
    
    | Parameter | Base | -20% | -10% | +10% | +20% | Verdict |
    |-----------|------|------|------|------|------|---------|
    | EMA period | 20 | 16 | 18 | 22 | 24 | Stable if all profitable |
    | RSI threshold | 40 | 32 | 36 | 44 | 48 | Fragile if only 40 works |
    | Stop loss % | 2% | 1.6% | 1.8% | 2.2% | 2.4% | Check drawdown impact |
    
    **Rule of thumb:** If the strategy only works with exact parameter values, it is overfit. You want a "plateau" of profitability, not a "peak."
    
    #### 4b. Execution Friction (India-Specific Costs)
    
    Apply realistic transaction costs:
    
    | Cost Component | Delivery (CNC) | Intraday (MIS) | F&O |
    |----------------|----------------|-----------------|-----|
    | Brokerage | ~₹20/order or 0.03% | ~₹20/order or 0.03% | ~₹20/order |
    | STT | 0.1% (buy+sell) | 0.025% (sell only) | 0.0125% (sell, options) |
    | Exchange charges | 0.00345% (NSE) | 0.00345% (NSE) | 0.05% (options) |
    | GST | 18% on brokerage+exchange | 18% on brokerage+exchange | 18% on brokerage+exchange |
    | Stamp duty | 0.015% (buy) | 0.003% (buy) | 0.003% (buy) |
    | SEBI charges | 0.0001% | 0.0001% | 0.0001% |
    | **Slippage** | **0.05-0.1% large-cap** | **0.1-0.2% mid-cap** | **0.1-0.3% options** |
    
    **Total round-trip cost estimates:**
    - Delivery large-cap: ~0.3-0.5%
    - Intraday large-cap: ~0.1-0.2%
    - F&O (options): ~0.15-0.4%
    - Small-cap delivery: ~0.5-1.0% (wider spreads)
    
    #### 4c. Time Robustness
    
    - Split data into 3-year rolling windows. Is the strategy profitable in each?
    - Check year-by-year returns. Is any single year driving total performance?
    - Remove the best month. Is the strategy still positive?
    
    #### 4d. Sample Size Validation
    
    - Minimum 30 trades for any statistical claim (even this is weak)
    - 100+ trades: Moderate confidence
    - 200+ trades: Good confidence
    - Use the t-test: Is average trade return significantly different from zero?
    
    ---
    
    ### Step 5: Out-of-Sample Validation (Walk-Forward Analysis)
    
    **Never skip this step.**
    
    #### Walk-Forward Method for Indian Markets
    
    1. **In-sample period:** Train on 5 years of data (e.g., 2015-2019)
    2. **Out-of-sample period:** Test on next 1-2 years (e.g., 2020-2021)
    3. **Roll forward:** Move window, retrain on 2016-2020, test on 2021-2022
    4. **Combine:** Aggregate all out-of-sample periods for true performance estimate
    
    **Walk-Forward Efficiency (WFE):**
    ```
    WFE = Out-of-Sample Return / In-Sample Return
    ```
    - WFE > 50%: Good — strategy generalizes
    - WFE 30-50%: Acceptable — some overfitting present
    - WFE < 30%: Poor — likely overfit
    
    #### Paper Trading Validation
    
    Before deploying capital, paper trade for at least:
    - 30 trades minimum
    - 2 months minimum
    - Cover at least one volatile period (expiry week, results season, RBI policy)
    
    ---
    
    ### Step 6: Evaluate Results (Deploy / Refine / Abandon)
    
    Use the evaluation script to get an objective score:
    
    ```bash
    python3 evaluate_backtest.py \
      --total-trades 150 \
      --win-rate 62 \
      --avg-win-pct 1.8 \
      --avg-loss-pct 1.2 \
      --max-drawdown-pct 15 \
      --years-tested 8 \
      --num-parameters 3 \
      --slippage-tested
    ```
    
    #### Decision Framework
    
    | Score | Verdict | Action |
    |-------|---------|--------|
    | **80-100** | **Deploy** | Size small initially (25% of intended), scale up over 50+ live trades |
    | **60-79** | **Refine** | Identify weakest dimension, address it, re-test |
    | **40-59** | **Refine with caution** | Multiple issues — may not be salvageable |
    | **0-39** | **Abandon** | Fundamental edge likely does not exist. Document lessons and move on. |
    
    #### Before Deploying
    
    - [ ] Strategy has positive expectancy after ALL costs
    - [ ] Survived parameter sensitivity testing
    - [ ] Walk-forward efficiency > 50%
    - [ ] Maximum drawdown is psychologically tolerable
    - [ ] Sample size > 100 trades
    - [ ] No more than 3-4 free parameters
    - [ ] Slippage and transaction costs included
    - [ ] Paper traded for 30+ trades
    - [ ] Written trade plan with exact rules
    - [ ] Risk management plan for live trading (position sizing, max daily loss, max drawdown circuit breaker)
    
    ---
    
    ## Using Broker MCP Tools for Backtesting Support
    
    While the MCP tools are not backtesting engines, they support the process. Use whichever broker is connected:
    
    ### Groww MCP (if connected)
    - **`fetch_historical_candle_data`**: Fetch OHLCV data for strategy development and spot-checking
    - **`get_historical_technical_indicators`**: Calculate indicators (SMA, EMA, RSI, MACD, Bollinger, SuperTrend, etc.) on historical data
    - **`get_historical_candlestick_patterns`**: Identify candle patterns in historical data
    - **`fetch_stocks_fundamental_data`**: Screen for universe construction (PE, ROE, market cap filters)
    - **`fetch_fundamentals_screener`**: Natural language screening for universe building
    - **`fetch_technical_screener`**: Technical screening for strategy ideas
    - **`get_ltp`**: Current price for live validation
    - **`fetch_market_movers_and_trending_stocks_funds`**: Discover momentum and volume patterns
    
    ### Zerodha Kite MCP (if connected)
    - **`get_historical_data`**: Fetch OHLCV candle data for strategy development
    - **`get_ltp`** / **`get_quotes`**: Current prices for live validation
    - **`search_instruments`**: Find instruments for universe construction
    - **`get_holdings`** / **`get_positions`**: Verify live portfolio against strategy signals
    
    ---
    
    ## Quick Reference: Red Flags
    
    | Red Flag | Why It Matters |
    |----------|---------------|
    | CAGR > 50% with no drawdowns | Too good to be true — check for look-ahead bias |
    | Win rate > 80% | Likely not accounting for slippage or adverse fills |
    | Only works on specific parameters | Overfitting — no edge, just noise |
    | < 50 trades in backtest | Statistically meaningless |
    | No losing months in 5+ years | Data error or survivorship bias |
    | Strategy stops working after 2020 | Market structure may have changed (T+1, algo proliferation) |
    | Uses > 5 parameters | Degrees of freedom too high — curve-fitted |
    | No transaction costs modeled | Real returns could be negative |
    | Tested on Nifty 50 only | Survivorship bias in universe selection |
    
    ---
    
    ## Files in This Skill
    
    - `scripts/evaluate_backtest.py` — CLI scoring tool for backtest evaluation
    - `references/methodology.md` — Comprehensive backtesting methodology for Indian markets
    - `references/failed_tests.md` — Common failure patterns and documentation framework
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related