A market-neutral statistical arbitrage strategy for the Indian equity market (NSE), built with cointegration-based pair selection and validated through rolling walk-forward testing.
The strategy identifies cointegrated stock pairs within the same NSE sector and exploits short-term deviations in their spread. Positions are market-neutral (long one stock, short the other), so returns are driven purely by relative price movements rather than broad market direction.
Key design choices:
- Dual cointegration testing (ADF + Engle–Granger) with rolling stability checks
- Rolling hedge ratio (OLS β) recomputed on a 120-day window to avoid stale estimates
- Velocity filter on entries to confirm the spread is reverting before opening a position
- 3% per-pair drawdown stop and hard z-score stop-loss to control tail risk
- Walk-forward validation across 13 out-of-sample periods (Jan 2023 – Apr 2026)
.
├── src/ # Core strategy library
│ ├── data_loader.py # Downloads & caches NSE price data via yfinance
│ ├── pair_selector.py # Cointegration tests, stability scoring, pair ranking
│ ├── signal_generator.py # Rolling β, spread, z-score & signal logic
│ ├── backtester.py # P&L computation and trade log extraction
│ └── metrics.py # Sharpe, CAGR, drawdown, Calmar, win rate
│
├── research/ # Experiment & parameter tuning scripts
│ ├── tune_strategy.py # Grid search over entry/exit/stop parameters
│ ├── tune_walk_forward.py # Walk-forward parameter sweep (1,280 combos)
│ └── ... # Earlier walk-forward iterations (v2–v5)
│
├── notebooks/ # Exploratory analysis
│ ├── 01_data_exploration.ipynb
│ ├── 02_pair_selection.ipynb
│ ├── 03_signal_generation.ipynb
│ ├── 04_backtesting.ipynb
│ └── 05_long_only_constraint.ipynb # NSE overnight short restriction analysis
│
├── docs/
│ └── learn.md # Technical reference (statistical methods)
│
├── backtest.py # Runs the full walk-forward backtest
├── dashboard.py # Streamlit dashboard for interactive analysis
├── config.py # All strategy parameters
└── requirements.txt
1. Install dependencies
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt2. Generate backtest results
Downloads ~5 years of NSE price data, runs the walk-forward backtest, and saves outputs to data/.
python backtest.py3. Launch the dashboard
streamlit run dashboard.pyAll parameters are defined in config.py. Values were selected via grid search over the rolling walk-forward sweep (research/tune_walk_forward.py).
| Parameter | Value | Description |
|---|---|---|
BETA_LOOKBACK_WINDOW |
120 days | Rolling OLS window for hedge ratio |
Z_LOOKBACK_WINDOW |
30 days | Rolling window for z-score normalisation |
Z_ENTRY_THRESHOLD |
2.2 | Enter when spread exceeds ±2.2σ |
Z_EXIT_THRESHOLD |
0.5 | Exit when spread returns within ±0.5σ |
Z_STOP_LOSS |
3.5 | Hard stop — cointegration likely broken |
TRAIN_WINDOW_DAYS |
504 | Training window (~2 years) per period |
REBALANCE_DAYS |
126 | Out-of-sample window (~6 months) |
MAX_PAIRS |
5 | Maximum concurrent pairs per period |
MIN_STABILITY_PCT |
60% | Minimum rolling cointegration stability |
PAIR_DRAWDOWN_STOP |
3% | Per-pair drawdown circuit breaker |
Tested across 7 out-of-sample periods from January 2023 to April 2026, benchmarked against the Nifty 50.
| Metric | Portfolio | Nifty 50 |
|---|---|---|
| Total Return | +42.9% | +35.1% |
| CAGR | +11.7% | +9.8% |
| Sharpe Ratio | 1.54 | 0.30 |
| Max Drawdown | -1.3% | -15.8% |
| Win Rate | 52.6% (114 trades) | — |
| Profitable Periods | 7/7 | — |
2025–2026 out-of-sample period (single period test)
| Metric | Portfolio (Theoretical) | Long-Only (NSE Constraint) | Nifty 50 |
|---|---|---|---|
| Total Return | +21.55% | +0.88% | -0.62% |
| CAGR | +21.55% | +0.88% | -0.62% |
| Sharpe Ratio | 1.91 | -4.15 | -0.43 |
| Max Drawdown | -3.37% | -0.49% | -15.18% |
| Volatility | 7.23% | 1.23% | 13.26% |
| Calmar Ratio | 6.40 | 1.80 | -0.04 |
streamlit run dashboard.py| Tab | Description |
|---|---|
| Portfolio Performance | Equity curves, drawdown profile, per-period table |
| Pair Signal Explorer | Reconstruct spread, z-score & signals for any traded pair |
| Trade Logs | Filter and analyse all executed trades |
| Parameter Sweep | Visualise the 1,280-combination grid search results |
| Reference | Technical notes on the statistical methods used |
See docs/learn.md for a detailed walkthrough of the statistical methods: cointegration vs. correlation, stationarity, ADF testing, OLS hedge ratios, z-score signals, half-life estimation, walk-forward validation, and transaction cost modelling.
NSE does not allow retail investors to carry short equity positions overnight — intraday only. This means the market-neutral backtest results above assume access to the F&O (Futures & Options) segment, where short positions can be held via stock futures.
- 140 of 154 tickers in the universe have NSE F&O contracts, so the strategy is largely executable using futures for the short leg
- The 14 tickers without F&O are mostly smaller names (CUB, IOB, STARCEMENT, PGHH, etc.) that rarely appear in the final pair selection
- A live implementation would short the future and long the cash equity, introducing basis risk and rollover costs not modelled here
For a version of the strategy that works within the overnight short constraint (long leg only, no futures required), see notebooks/05_long_only_constraint.ipynb. This sacrifices market neutrality but allows trading entirely with cash equity.
The walk-forward backtest was re-run under this constraint (Jan 2023 – Apr 2026), executing only the long leg when the spread diverges. Surprisingly, the long-only strategy shows improved returns and Sharpe ratio compared to the theoretical two-legged strategy, driven by the strong structural uptrend of the Indian stock market during this period:
| Metric | Full Pairs (Theoretical) | Long-Only (NSE Constraint) | Nifty 50 B&H |
|---|---|---|---|
| Total Return (%) | 42.90% | 44.70% | 35.10% |
| CAGR (%) | 11.73% | 12.17% | 9.81% |
| Sharpe Ratio | 1.27 | 1.32 | 0.33 |
| Max Drawdown (%) | -1.35% | -1.82% | -15.77% |
| Volatility (%) | 4.06% | 4.24% | 12.79% |
| Calmar Ratio | 8.71 | 6.69 | 0.62 |
| Correlation with Nifty 50 | 0.012 | 0.227 | 1.000 |
| Market Beta | 0.004 | 0.075 | 1.000 |
Key Takeaways:
- Beta Exposure: By executing only the long leg, the strategy loses pure market neutrality. Correlation with the Nifty 50 rises from 0.012 to 0.227, and market beta rises from 0.004 to 0.075.
- Improved Performance: This small directional exposure acted as a tailwind in the recent bull market, raising the Sharpe ratio from 1.27 to 1.32 and absolute returns to 44.70%.
- Risk Control: Despite losing the short hedge, the maximum drawdown only increases to -1.82% (compared to -1.35% for full pairs and -15.77% for Nifty 50), demonstrating that cointegration-based entry signals and tight stop-loss rules remain highly effective at controlling risk even without a short leg.
This project is for research purposes only. It is not financial advice. Past backtest performance does not guarantee future returns.
Built with assistance from Antigravity (AI coding assistant).