Multi-Horizon Transformer for Nifty 50 Direction Forecasting
Multi-horizon Transformer for Nifty 50 direction, with rigorous evidence it has no edge.
A single Transformer encoder predicts whether the Nifty 50 index will close higher — simultaneously across 20 forward horizons (1 to 20 days). Trained on 19 engineered features over 4,263 sequences with a strict temporal split. Every number below comes from a real trained model on held-out test data.
What this system does
A shared Transformer encoder outputs 20 independent logits — one per horizon — so short- and long-range signal inform each other through shared gradients instead of 20 separate models.
Chronological 70/15/15 split, no shuffling. StandardScaler is fit on training data only. Targets are strictly forward-looking.
All 20 horizon logits are ensembled into one signal, Platt-calibrated on validation data, and stress-tested as a long/flat timing strategy with India costs, bootstrap confidence intervals, decile attribution, and walk-forward validation.
Paper trading — the strategy, forward, on real prices
This trades the exact primary strategy forward, out-of-sample, on real Nifty closes with the full India futures cost stack. It is not tuned to look good — it under-performs simply holding the index, which is what the no-edge finding predicts. That is the point: honest forward proof, updated as new data arrives.
What the model says today — and what that is worth
| Horizon | P(up) | OOS AUC | 95% CI on AUC — overlap-corrected | Eff. n | Skill |
|---|---|---|---|---|---|
| 1d | 48.2% | 0.443 | 683 | indistinct | |
| 2d | 57.3% | 0.480 | 341 | indistinct | |
| 3d | 57.9% | 0.497 | 227 | indistinct | |
| 4d | 58.1% | 0.460 | 170 | indistinct | |
| 5d | 58.8% | 0.533 | 136 | indistinct | |
| 6d | 53.9% | 0.457 | 113 | indistinct | |
| 7d | 66.1% | 0.468 | 97 | indistinct | |
| 8d | 59.3% | 0.437 | 85 | indistinct | |
| 9d | 71.4% | 0.551 | 75 | indistinct | |
| 10d | 70.3% | 0.479 | 67 | indistinct | |
| 11d | 59.2% | 0.435 | 61 | indistinct | |
| 12d | 59.7% | 0.475 | 56 | indistinct | |
| 13d | 59.3% | 0.585 | 52 | indistinct | |
| 14d | 59.1% | 0.590 | 48 | indistinct | |
| 15d | 60.0% | 0.573 | 45 | indistinct | |
| 16d | 60.0% | 0.567 | 42 | indistinct | |
| 17d | 56.8% | 0.516 | 39 | indistinct | |
| 18d | 56.0% | 0.543 | 37 | indistinct | |
| 19d | 54.7% | 0.539 | 35 | indistinct | |
| 20d | 64.0% | 0.557 | 33 | indistinct |
today's ensemble signal (+1.14) sits at the 87th percentile of the trailing 250 days; the rule goes long above the 70th. The stance is a mechanical rule, not a conviction call: it fires whenever the trailing-quantile condition is met, independent of whether any horizon shows measurable skill. Publishing both together is deliberate — it shows exactly how much (or how little) statistical backing the live position has.
Model architecture
- Lookback window
- 60 days
- Input features
- 19
- d_model
- 64
- Attention heads
- 4
- Encoder blocks
- 2
- FFN hidden dim
- 128
- Pooling
- Attention (learned)
- Output horizons
- 20
- Loss
- BCE (from logits)
Per-horizon predictive skill (test set)
AUC measures ranking ability independent of any threshold; 0.50 is a coin flip. Read these beside the confidence intervals in the Predictions section — a bar above 0.50 is not evidence of skill on its own.
Daily index direction is close to efficient — AUCs hover near 0.5 and IC is small, as expected for a liquid benchmark. The value is in the aggregate ranking of the signal, tested next in a cost-aware backtest.
The model is trained for classification, so its raw sigmoid outputs rank market states well but are not trustworthy probabilities. A Platt scaler is fit per horizon on the validation set only and applied to test predictions, so "P(up) = 0.6" means the market actually rose about 60% of the time at that score. All probability thresholds on this page use the calibrated values; rank metrics (AUC, IC) are unaffected.
Long/flat timing backtest — calibrated 20-horizon ensemble
All 20 logits are z-scored on validation statistics and averaged into one signal. The strategy goes long when that signal clears a trailing percentile, and is charged the full India futures cost stack on every switch.
On a single index, long-short quantile spreads are a cross-sectional idea that does not transfer; the honest framing is market timing: hold the index when the ensemble signal is in its top 30% (threshold fixed on validation data), sit in cash otherwise.
The Nifty has a strong upward drift, so a long-biased signal can look good on its own. The honest test is the excess over passively holding the index, net of costs.
| STT (securities transaction tax) | 2.00 bps |
| Stamp duty | 0.20 bps |
| Slippage (bid-ask + impact) | 6.00 bps |
| Exchange transaction | 0.35 bps |
| Brokerage | 0.80 bps |
| GST | 0.21 bps |
| SEBI turnover | 0.02 bps |
| Round-trip total | 9.58 bps |
| Strategy | Sharpe | Return |
|---|---|---|
| Timing · ensemble | +0.00 | 0.0% |
| Timing · h20 only | +0.00 | 0.0% |
| Timing · best-val h9 | +0.63 | 1.0% |
| Quantile L/S (ref) | -1.08 | -24.6% |
| Sign (ref) | +0.84 | 26.4% |
| Buy & Hold | +0.94 | 30.2% |
Quantile L/S and sign are cross-sectional constructs kept for reference — shorting a structurally drifting index is not a meaningful strategy.
Sharpe explorer — sweep the cost, watch the ratios move
A Sharpe ratio is only meaningful next to its assumptions. Drag the transaction cost and every strategy's net Sharpe and equity curve recompute live from the raw per-trade returns — so you can see exactly how fragile (or robust) each ratio is to costs, and how the strategies re-rank against each other. The default is the model's India per-side cost.
| Strategy | Net Sharpe | Total return |
|---|---|---|
| Timing · rolling thr | +0.63 | 11.2% |
| Timing · frozen thr | +0.00 | 0.0% |
| Vol-targeted | +0.37 | 10.0% |
| Timing · h20 | +0.00 | 0.0% |
| Quantile L/S | -1.08 | -24.6% |
| Sign | +0.84 | 26.4% |
Walk-forward validation
Eight expanding-window folds, each retrained from scratch. If an edge were real and stable, fold Sharpes would cluster above zero. The spread across folds is the honest measure of regime robustness.
The single split can get lucky. Here the model is retrained from scratch on an expanding window and evaluated out-of-sample on the next block — 8 folds, each using the long/flat ensemble timing rule with thresholds fixed on its own training carve. Mean net Sharpe: +0.28 ± 0.36 across folds.
Where direction models can earn: ranking 85 stocks against each other
Timing one near-efficient index failed the honest test above. This track asks a better question: on the same date, which of 85 NSE large caps will do relatively better? The same shared-weight Transformer scores every stock; each rebalance goes long the top 20% and short the bottom 20% — a genuine cross-sectional quantile spread, the construct the single-index section could only imitate. Targets here are relative: did the stock beat the cross-sectional median return that date — the canonical label for a ranking task (absolute direction labels saturate to "up" in a bull window). The model also sees 29 features including cross-sectional ones — momentum and returns demeaned by the universe, per-date percentile ranks, and sector-relative momentum — the relative signals a ranking task needs. Every configuration (absolute/relative targets, per-stock/cross-sectional features, classification/regression head) is published; none is cherry-picked.
- Universe is (mostly) today's large caps backtested into the past: survivorship bias inflates absolute returns; the long-short spread is partially insulated but still favored.
- Daily IC uses overlapping 20-day forward returns (standard practice); all P&L is computed on non-overlapping rebalances.
- Equal-weight benchmark is gross of costs; strategy legs are net.
- Targets are relative. Both formulations were run and both results are published (cross_section.json / cross_section_absolute.json) - no cherry-picking.
Per-stock predictions across all 20 horizons — and pick your risk
The cross-sectional model's latest output for every name in the universe: each row is a stock, each column a forecast horizon (1–20 days), colored by the calibrated probability that the stock outperforms the universe median. Click any stock for its horizon curve. Then choose a risk profile — each is a real backtested construction labelled by its actual historical Sharpe, showing the basket it would hold from today's signal.
What the model attends to
Learning from mistakes — only where a mistake can be identified
Every closed trade is decomposed into signal error, cost drag, and noise. The distinction matters: cost drag is arithmetic and can be acted on immediately, while a loss inside the noise floor is not a mistake at all, and 'learning' from it means fitting randomness.
Cost drag is deterministic - these trades had the direction right and lost to fees. Fewer, longer holds fix it without any model change. This is the only component that can be acted on without a significance test.
P(arm is best) from the posteriors. Coin-flip baseline is 13%. No arm is distinguishable: the leader (sign) is best with only probability 0.41 against a 0.12 coin-flip baseline. The argmax here is noise, not a decision.
The book closed 6 round trips. Gross return summed to +15.70% and costs removed 0.57%.
Hit rate 4/6 = 66.7%, binomial p = 0.688. NOT distinguishable from a coin flip - with 6 trades, nothing here supports 'the model learned from a mistake'. The only reliable lessons in this journal are the deterministic ones (cost drag, exposure).
- 6 trades is far too few to establish skill either way.
- 1 trades fell inside the noise floor and carry no directional information.
- Bandit separability: No arm is distinguishable: the leader (sign) is best with only probability 0.41 against a 0.12 coin-flip baseline. The argmax here is noise, not a decision.
Retraining without manufacturing an edge
Layers are sized by parameter count against the independent observations their cadence actually delivers - not by clock speed. A week carries ~0.25 independent observations at a 20-day horizon, so weekly gradient updates to a 69,589-parameter backbone would fit noise, not signal.
monitors only, never retrains
past-only rolling percentile
trailing window, label-embargoed
purged, embargoed, gated
| Date | Detector | Level before | After | Shift in signal level |
|---|---|---|---|---|
| 2023-02-03 | adwin | 0.41 | 0.36 | |
| 2023-05-22 | adwin | 0.14 | -0.75 | |
| 2023-08-09 | adwin | -0.76 | -0.56 | |
| 2023-12-19 | adwin | -1.03 | -3.43 | |
| 2024-03-04 | adwin | -3.27 | -1.43 | |
| 2024-04-16 | adwin | -3.04 | -0.58 | |
| 2024-11-01 | adwin | -0.44 | -0.63 | |
| 2025-01-17 | adwin | -0.44 | -0.56 | |
| 2025-08-29 | adwin | -0.43 | 0.27 | |
| 2025-09-09 | page-hinkley | -0.22 | 0.00 | |
| 2025-12-04 | adwin | 0.05 | -1.11 | |
| 2026-06-24 | adwin | -0.75 | 1.27 |
Detectors observe only - they never trigger a parameter update on their own. An alarm is evidence to inspect, not permission to retrain: at ~250 observations a year, drift alarms and false alarms are hard to tell apart from a single firing.
n_trials counts every model ever trained in this lineage, including rejected challengers, and is passed to deflated_sharpe. Reporting the best of N retrains without deflating by N is how a retraining schedule manufactures an edge that was never there.
Read the whole thing
Every file, every metric formula, every leakage rule and every number is documented in the repository — written so someone who has never seen the project can reproduce it end to end.
How to read these results — honestly
- Every metric is computed on held-out test data from a freshly trained model.
- India futures costs (9.6bps round-trip: STT, stamp, slippage, brokerage, exchange, GST) are charged on every trade.
- Entry thresholds, signal z-scoring, and Platt calibration are all fit on validation data only — nothing is tuned on the test set.
- Sharpe is reported with a bootstrap 95% CI; results are benchmarked against passively holding the Nifty.
- Inputs are restricted to stationary features; returns are non-overlapping; 8-fold walk-forward retraining checks it is not a single-split fluke.
- A liquid index is near-efficient at daily frequency — skill is thin by design.
- ~30 non-overlapping test trades: the CI is wide and any point estimate is fragile.
- Single-asset backtest; no borrow costs or capacity modeling (slippage is included).
- FinBERT sentiment fusion is wired in (config-gated) but off: NewsAPI can't backfill history, so no fabricated features enter the backtest.
- Research artifact, not a deployed trading system.