# We followed a popular ORB strategy guide to the letter on 10 years of NIFTY 50 data

> Opening range breakout with an EMA trend filter, full-body candles and volume confirmation. 23,666 trades, ₹23.7 lakh lost after costs, and none of 8 variants made money.

- Published: 2026-10-07
- URL: https://datapointx.com/research/in/orb-strategy-guide-tested/
- Tags: orb, breakout, strategy-test
- Verdict: 23,666 trades, −₹23.7 lakh after costs. 0 of 8 variants profitable.
- Source: DataPointX Research (education and research only, not investment advice)

The opening range breakout (ORB) is probably the most taught intraday strategy. One widely shared guide adds three
filters to the basic idea, each meant to cut out false breakouts, and quotes win rates of 42–65%. We followed it
exactly on every NIFTY 50 stock from January 2016 to August 2026.

**The rules, as written:**

1. **Range:** the high and low of the first 30 minutes (09:15–09:45).
2. **Trend:** long only when the 50 EMA is above the 200 EMA and the candle closes above both (15-minute chart);
   short only in the mirror case.
3. **Entry:** a 15-minute candle **closes** beyond the range, with a full body (at least 60% of its range) in the
   breakout direction and volume at least 1.5× the average of the previous 20 candles.
4. **Exit:** stop at the far end of the breakout candle, target 2× the risk, anything open is closed at 15:15.

One trade per stock per day, ₹5 lakh per trade, entered at the next bar's open after the candle closes.

## It isn't one bad year

Ten of the eleven years lost money, mostly ₹2–4 lakh each. The one exception was 2020, a year of unusually large
daily moves, which made ₹3.5 lakh (about ₹140 per trade). Every year since has lost money again.

## The filters help, just not enough

The guide's filters do what they claim: each one improves the average trade. Remove all three and the gross edge
falls from 4.5 to 2.3 bps per trade while the number of trades grows 4.6 times, and the loss grows almost tenfold, to −₹2.27 crore. The
breakouts it picks are also better than a random NIFTY 50 stock traded at the same minute in the same direction
(+4.5 vs +3.0 bps).

The problem is size. A ₹5 lakh round trip costs about **6.5 bps** (brokerage, STT, exchange fees, stamp duty, GST and
1 bp of slippage on each fill; [see how we count costs](/research/in/how-we-backtest/)). An edge of 4.5 bps
before costs is a loss of about 2 bps after them, on every trade.

## Eight variants, none profitable

We changed one thing at a time: dropped each filter, used a 15-minute range, moved the target to 1R or 3R, and used
30-minute candles. These were decided before running anything. Here is every result:

| Variant | Trades | Gross / trade | Win % | Net after costs | Net with +3 bps | 2016-20 | 2021-23 | 2024-26 |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| As written: 30-min range, 15-min candle, trend + full body + volume, 2R | 23,666 | 4.5 bps | 41.2% | −₹23.7 L | −₹94.5 L | −₹9.2 L | −₹8.0 L | −₹6.4 L |
| No trend filter | 39,627 | 4.0 bps | 40.9% | −₹50.0 L | −₹1.69 Cr | −₹16.4 L | −₹19.6 L | −₹14.0 L |
| No volume filter | 55,436 | 2.7 bps | 38.8% | −₹1.06 Cr | −₹2.72 Cr | −₹44.4 L | −₹32.5 L | −₹28.8 L |
| No trend, no volume, no body filter | 1,08,182 | 2.3 bps | 38.0% | −₹2.27 Cr | −₹5.51 Cr | −₹86.8 L | −₹74.0 L | −₹65.8 L |
| 15-min range | 27,940 | 3.1 bps | 40.6% | −₹47.3 L | −₹1.31 Cr | −₹20.0 L | −₹15.3 L | −₹12.0 L |
| Target 1R | 23,666 | 2.0 bps | 49.6% | −₹53.0 L | −₹1.24 Cr | −₹25.6 L | −₹16.5 L | −₹10.9 L |
| Target 3R | 23,666 | 5.9 bps | 39.5% | −₹7.4 L | −₹78.3 L | ₹22,345 | −₹2.2 L | −₹5.5 L |
| 30-min candles | 13,961 | 6.4 bps | 44.3% | −₹73,727 | −₹42.6 L | −₹73,779 | ₹79,841 | −₹79,788 |

_₹5 lakh per trade, 5-minute bars, v2026.08 snapshot (Jan 2016 – Aug 2026), NIFTY 50 point-in-time members, discount-broker (₹20/order) and statutory charges + 1 bp slippage per fill._

The closest to break-even was the **30-minute candle** version: −₹74,000 over 13,961 trades, about −₹5 per trade.
It made ₹0.8 lakh in 2021–23 and lost in 2016–20 and 2024–26. It's tempting to call that "the version that works".
It isn't. We'd be choosing it **after** seeing eight results, it didn't make money over the full period, and three
basis points of extra slippage per fill turn it into a ₹43 lakh loss.

## What to take from this

- **A filter that improves win rate or edge is not the same as a filter that makes money.** Check whether the edge
  per trade clears the cost of a round trip.
- **Quoted win rates mean little on their own.** At 2R targets a 41% win rate loses money after costs here. What
  matters is the average gain per trade compared with the average cost.
- **Breakouts on NIFTY 50 stocks are close to fairly priced at this horizon.** The gross edge of a well-filtered ORB
  is a few basis points: real, but smaller than costs.

### The exact code we ran

```python
def orb_pdf(or_min=30, chart=15, trend=True, full_body=True, vol=1.5, r=2.0):
    """Range = first `or_min` minutes. Signal = a `chart`-minute candle CLOSES beyond the range; optional EMA50/200
    trend filter (on chart candles), full-body candle (body >= 60% of range), volume >= vol x 20-candle average.
    Stop at the far end of the breakout candle, target r x risk, one trade per stock per day."""
    def strategy(p):
        c = p.candles(chart)
        cl = p.candle_series(c, 'close')
        cv = p.candle_series(c, 'volume')
        o, h, l, cc, v = c['open'], c['high'], c['low'], c['close'], c['volume']
        orh, orl = p.opening_range(or_min)
        long, short = cc > orh, cc < orl
        if full_body:
            body, rng = (cc - o).abs(), (h - l)
            long &= (cc > o) & (body >= 0.6 * rng)
            short &= (cc < o) & (body >= 0.6 * rng)
        if vol:
            vavg = cv.rolling(20).mean().shift(1).reindex(p.index)
            long &= v >= vol * vavg
            short &= v >= vol * vavg
        if trend:
            e50 = cl.ewm(span=50, adjust=False).mean().reindex(p.index)
            e200 = cl.ewm(span=200, adjust=False).mean().reindex(p.index)
            long &= (e50 > e200) & (cc > e50) & (cc > e200)
            short &= (e50 < e200) & (cc < e50) & (cc < e200)
        first = p.once_per_day(long | short)
        risk = ((cc - l) / cc).where(long, (h - cc) / cc).clip(lower=0.001)
        return bt.Signals(long=long & first, short=short & first, stop=risk, target=r * risk)
    return strategy
```
