# DataPointX research, full text > Honest backtests on 10 years of NIFTY 50 minute data. Popular trading strategies tested with real Indian brokerage costs, point-in-time index members and three separate periods. Education and research only, not investment advice. Quote with attribution and a link to https://datapointx.com. --- # Two published intraday strategies, tested on NIFTY 50 stocks: an edge that fades and one that isn't there > A noise-band breakout from "Beat the Market" and the market intraday momentum effect, applied to every NIFTY 50 stock from 2016. The first had a real edge that shrank every period; the second has almost none. - Published: 2026-10-07 - URL: https://datapointx.com/research/in/academic-strategies-in-india/ - Tags: academic-research, momentum, strategy-test - Verdict: Noise band −₹1.53 Cr, edge 5.7 → 1.5 bps over three periods. Intraday momentum: 0.1 bps before costs. - Source: DataPointX Research (education and research only, not investment advice) Academic papers are a better source of trading ideas than YouTube: the rules are exact and the tests are careful. But they're usually tested on US markets, often on an index fund rather than single stocks. We took two well-known intraday papers and applied their rules to every NIFTY 50 stock from January 2016 to August 2026, with Indian costs. **Not the papers' own tests.** These are our per-stock adaptations on Indian data, not reproductions of the papers' results. Both papers studied the US market through an S&P 500 index fund. ## 1. The noise band ("Beat the Market", Zarattini, Aziz and Barbon, 2024) **The idea:** most intraday moves stay inside a "noise" band that can be estimated from recent days. Price leaving the band suggests real demand, so trade in that direction. **Our rules, per stock:** the band is centred on the larger (or smaller) of today's open and yesterday's close, and is as wide as the stock's average absolute move from the open at the same time of day over the last 14 days. Every half hour from 10:00, go long above the upper band or short below the lower band. Exit when price crosses back through the band or VWAP (or at 15:15). Unlike most setups we test, this one has a real edge: +4.1 bps per trade before costs, and the stocks it picks beat random stocks at the same minute in all 200 of our random runs. But the edge isn't large enough to pay for a round trip, and it has shrunk in each period: A wider band (1.5×) trades less and loses less, −₹71 lakh. Holding to the close instead of trailing doubles the win rate to 47% but still loses ₹95 lakh. All three versions lost the most in 2024–26. ## 2. Intraday momentum (Gao, Han, Li and Zhou, 2018) **The idea:** the market's return in the first half hour predicts its return in the last half hour, and the authors link it to investors who rebalance or trade late in the day. **Our rules, per stock:** take each stock's return from the previous close to 09:45. Near the end of the day, enter in that direction and close at 15:15. We tried no threshold, moves above 0.5% and above 1%, and entries at 14:45 and 14:15. On single NIFTY 50 stocks there's almost nothing there: +0.1 to +0.6 bps per trade before costs, and in 2024–26 the edge is zero or negative in three of the four versions. A 30-minute trade can't pay 6.5 bps of costs from a fraction of a basis point. Every version lost money in all eleven years. | Variant | Trades | Gross / trade | Win % | Net after costs | Net with +3 bps | 2016-20 | 2021-23 | 2024-26 | |---|---:|---:|---:|---:|---:|---:|---:|---:| | Noise band (1x), trailing exit | 1,27,985 | 4.1 bps | 23.8% | −₹1.53 Cr | −₹5.37 Cr | −₹25.2 L | −₹48.3 L | −₹80.0 L | | Noise band (1x), hold to 15:15 | 75,420 | 4.0 bps | 47.3% | −₹94.5 L | −₹3.20 Cr | −₹21.1 L | −₹18.6 L | −₹54.8 L | | Noise band (1.5x), trailing exit | 78,165 | 4.7 bps | 24.7% | −₹71.2 L | −₹3.05 Cr | −₹1.7 L | −₹17.7 L | −₹51.8 L | | Intraday momentum, any move, enter 14:45 | 1,28,299 | 0.1 bps | 41.6% | −₹4.09 Cr | −₹7.93 Cr | −₹1.86 Cr | −₹1.15 Cr | −₹1.07 Cr | | Intraday momentum, move > 0.5%, enter 14:45 | 73,555 | 0.4 bps | 42.3% | −₹2.25 Cr | −₹4.45 Cr | −₹1.08 Cr | −₹61.5 L | −₹54.9 L | | Intraday momentum, move > 1%, enter 14:45 | 36,831 | 0.5 bps | 42.9% | −₹1.09 Cr | −₹2.20 Cr | −₹49.9 L | −₹30.3 L | −₹29.3 L | | Intraday momentum, move > 0.5%, enter 14:15 | 73,555 | 0.6 bps | 44.2% | −₹2.15 Cr | −₹4.35 Cr | −₹1.04 Cr | −₹52.4 L | −₹58.5 L | _₹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._ ## What to take from this - **A real edge can still be untradeable.** The noise band picks better stocks than chance and still loses after costs. Always compare the edge with the cost of a round trip. - **Check whether the edge is shrinking.** A strategy that earned 5.7 bps in 2016–20 and 1.5 bps in 2024–26 is telling you where it's heading. - **An index result doesn't carry over to single stocks automatically.** Both papers studied a US index fund; on individual Indian stocks the effects are weaker or absent. ### The exact code we ran ```python def noise_band(mult=1.0, exit='trail'): """Zarattini, Aziz & Barbon (2024), "Beat the Market", per stock. Noise band around max/min(today's open, previous close): +/- mult x the average absolute move from the open at the same time of day over the last 14 days. At every HH:00 / HH:30 from 10:00, go long above the upper band or short below the lower band. exit='trail': close when price crosses back through max(band, VWAP); 'eod': hold to 15:15.""" def strategy(p): move = (p.close / p.day_open - 1).abs() sigma = move.groupby(p.tod.values).transform(lambda x: x.rolling(14, min_periods=10).mean().shift(1)) pc = p.prev_day('close') upper = np.maximum(p.day_open, pc) * (1 + mult * sigma) lower = np.minimum(p.day_open, pc) * (1 - mult * sigma) end = p.tod + p.minutes check = p.bcast((end >= bt.hm('10:00')) & (end % 30 == 0)) sig = dict(long=check & (p.close > upper), short=check & (p.close < lower)) if exit == 'trail': sig['long_exit'] = p.close < np.maximum(upper, p.vwap) sig['short_exit'] = p.close > np.minimum(lower, p.vwap) return bt.Signals(**sig) return strategy ``` --- # Hammers and shooting stars: the classic reversal candles lose money before costs > Three falling candles, then a hammer at a new low. It is one of the most taught reversal setups. On 26,965 trades across every NIFTY 50 stock since 2016 it loses 1.6 bps per trade before costs, and no filter rescues it. - Published: 2026-10-07 - URL: https://datapointx.com/research/in/candlestick-patterns-tested/ - Tags: candlestick-patterns, reversal, strategy-test - Verdict: 26,965 trades, −₹1.09 Cr. The pattern does worse than a random stock at the same minute. - Source: DataPointX Research (education and research only, not investment advice) Candlestick reversals are the first thing most chart courses teach. The textbook version: after three falling red candles, a **hammer** (a small body with a long lower wick) that makes a new low means sellers are exhausted, so buy. The mirror is a **shooting star** after three rising green candles: sell. We coded the definitions exactly and ran them on every NIFTY 50 stock from January 2016 to August 2026: - **Run:** three candles of one colour, each closing beyond the last. - **Hammer:** range at least half the candle ATR, lower wick at least 2× the body and 55% of the range, upper wick at most 20% of the range, making a new low. The shooting star is the mirror image. - **Trade:** enter at the next candle's open, stop beyond the signal candle, target 2× the risk, close at 15:15. Trades risking more than 3% are skipped. ₹5 lakh per trade. ## Worse than picking a stock at random Most setups we test have a small edge that costs eat. This one has none to eat: before any costs the average trade **loses** 1.6 bps. We also re-ran every trade 200 times on a random NIFTY 50 stock at the same minute and in the same direction. The random picks averaged +0.4 bps, and the hammer beat them in **none** of the 200 runs. So the pattern isn't neutral. After three falling candles, a stock that prints a hammer at a new low tends to keep falling more often than a random stock bought at that moment. In this test, at a 15-minute horizon, these short runs in NIFTY 50 stocks were more likely to continue than to reverse. ## The usual fixes don't rescue it We tried what traders usually add, decided before running anything: wait for a confirming candle, require heavy volume, both, faster 5-minute candles, and a closer 1R target. | Variant | Trades | Gross / trade | Win % | Net after costs | Net with +3 bps | 2016-20 | 2021-23 | 2024-26 | |---|---:|---:|---:|---:|---:|---:|---:|---:| | Hammer / shooting star after 3 candles, 15-min, 2R | 26,965 | −1.6 bps | 34.3% | −₹1.09 Cr | −₹1.90 Cr | −₹48.6 L | −₹34.0 L | −₹26.6 L | | Wait for a confirming candle | 8,669 | −0.3 bps | 39.4% | −₹29.6 L | −₹55.5 L | −₹9.5 L | −₹9.6 L | −₹10.4 L | | Volume at least 1.5x average | 6,303 | 0.9 bps | 37.7% | −₹17.7 L | −₹36.6 L | −₹5.4 L | −₹7.9 L | −₹4.4 L | | Confirmation + volume | 2,544 | −0.2 bps | 40.9% | −₹8.5 L | −₹16.1 L | −₹2.1 L | −₹3.5 L | −₹2.9 L | | 5-min candles | 69,418 | −0.4 bps | 34.9% | −₹2.39 Cr | −₹4.47 Cr | −₹1.06 Cr | −₹70.4 L | −₹62.5 L | | Target 1R | 27,030 | −1.2 bps | 47.4% | −₹1.04 Cr | −₹1.85 Cr | −₹45.8 L | −₹30.8 L | −₹27.1 L | _₹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._ Confirmation and volume filters cut the number of trades sharply (from 26,965 to 2,544 with both), which shrinks the loss, but the average trade still loses money before costs. The volume filter is the only variant with a positive gross edge, +0.9 bps, still far below the cost of a round trip. Five-minute candles trade 2.6 times as often and lose ₹2.39 crore. ## What to take from this - **A pattern that "looks like" a reversal can be a continuation signal.** Test the raw edge before costs first; if it's negative, no filter or exit will fix it. - **Filters that cut trades make the loss smaller, not the edge bigger.** Fewer trades means less money lost to costs, which is easy to mistake for improvement. - **Compare against random picks at the same moment.** It separates "the pattern has information" from "the market moved". ### The exact code we ran ```python def candle_pattern(chart=15, confirm=False, vol=0.0, r=2.0): """Three falling red candles, then a hammer making a new low -> long, stop below the hammer, target r x risk. Mirror: three rising green candles, then a shooting star at a new high -> short. Candles run across days like a chart. confirm: wait for the next candle to close beyond the signal candle. vol: candle volume >= vol x its 20-candle average. Trades risking more than 3% are skipped.""" def strategy(p): c = p.candles(chart) o, h, l, cl, v = (p.candle_series(c, f) for f in ('open', 'high', 'low', 'close', 'volume')) pc = cl.shift(1) atr = np.maximum(h - l, np.maximum((h - pc).abs(), (l - pc).abs())).rolling(14, min_periods=10).mean() body, rng = (cl - o).abs(), h - l lw, uw = np.minimum(o, cl) - l, h - np.maximum(o, cl) big = rng >= 0.5 * atr hammer = big & (lw >= 2 * body) & (lw >= 0.55 * rng) & (uw <= 0.2 * rng) star = big & (uw >= 2 * body) & (uw >= 0.55 * rng) & (lw <= 0.2 * rng) red, green = cl < o, cl > o run_red = red.shift(1) & red.shift(2) & red.shift(3) & (cl.shift(1) < cl.shift(2)) & (cl.shift(2) < cl.shift(3)) run_green = green.shift(1) & green.shift(2) & green.shift(3) & (cl.shift(1) > cl.shift(2)) & (cl.shift(2) > cl.shift(3)) long = run_red.fillna(False) & hammer & (l < l.shift(1).rolling(3).min()) short = run_green.fillna(False) & star & (h > h.shift(1).rolling(3).max()) stop_l, stop_s = l, h if confirm: long, short = long.shift(1, fill_value=False) & (cl > h.shift(1)), short.shift(1, fill_value=False) & (cl < l.shift(1)) stop_l, stop_s = l.shift(1), h.shift(1) if vol: heavy = v >= vol * v.shift(1).rolling(20).mean() sig_heavy = heavy.shift(1, fill_value=False) if confirm else heavy long, short = long & sig_heavy, short & sig_heavy risk = ((cl - stop_l) / cl).where(long, (stop_s - cl) / cl) ok = (risk > 0) & (risk <= 0.03) long, short = long & ok, short & ok return bt.Signals(long=long.reindex(p.index), short=short.reindex(p.index), stop=risk.reindex(p.index), target=(r * risk).reindex(p.index)) return strategy ``` --- # How we backtest, and why most intraday strategies fail our tests > The rules behind every number on DataPointX. Point-in-time NIFTY 50 stocks, next-bar fills, real Indian brokerage and statutory costs, three separate periods, a slippage stress test and a random-stock benchmark. - Published: 2026-10-07 - URL: https://datapointx.com/research/in/how-we-backtest/ - Tags: method - Verdict: Every report shows net after costs, a +3 bps stress test and three periods. No exceptions. - Source: DataPointX Research (education and research only, not investment advice) Most backtests you see online make three mistakes. They test on today's index members, so the stocks that crashed out of the NIFTY 50 are missing. They fill orders at prices nobody could have got. And they skip costs, or treat them as a rounding error. Any one of these can turn a losing strategy into a winning one on paper. This page lists the rules every DataPointX result follows. The same rules are built into [`dpx_bt`](/docs/), the engine behind our posts and the [Ask](/ask/) assistant, and you cannot switch them off. ## 1. The right universe on every day We test only on stocks that were in the NIFTY 50 **on that day**. When YES Bank left the index in March 2020, it leaves our universe that day. When Trent joined in September 2024, it enters that day. Testing today's 50 stocks over ten years quietly drops the losers and adds the winners, and makes almost any long strategy look better than it was. One gap we can't close yet: three of the 79 stocks that were members at some point since 2016 have no minute data in our snapshot: HDFC Ltd (merged into HDFC Bank in July 2023), Cairn India and Tata Motors DVR. On days they were members, the universe is one to three stocks short. ## 2. Fills you could actually get A signal is read when a bar **closes** and the order fills at the **next bar's open**, never at the bar's own close, high or low. The two exceptions are written into the rules: a stop or limit order resting at a known level (an opening-range high, a pivot) fills at that level, or at the open if the price gaps through it. Stops and targets are checked on the bar's high and low from the entry bar on. A gap through the stop fills at the open, not at the stop. Everything is closed at 15:15 (15:10 from 3 August 2026). Positions never carry overnight. ## 3. Real costs on every order We use the charges of a typical Indian discount broker. Zerodha, Groww, Upstox, Angel One and Dhan all charge a flat ₹20 per intraday order at this trade size; the rest are statutory charges every broker passes on. For a ₹5 lakh intraday trade in a ₹1,000 stock (500 shares), one round trip costs: | Item | ₹ | bps of the trade | |---|---:|---:| | Brokerage (₹20 per order, both sides) | 40.0 | 0.80 | | STT, 0.025% on the sell side | 125.0 | 2.50 | | NSE transaction charges, 0.00307% | 30.7 | 0.61 | | Stamp duty, 0.003% on the buy side | 15.0 | 0.30 | | GST at 18% and SEBI fees | 13.9 | 0.28 | | Slippage, 1 bp on each fill | 100.0 | 2.00 | | **Total** | **324.6** | **6.49** | Rates as of October 2026. Brokerage is capped at ₹20 per order and everything else is a percentage, so any ₹5 lakh round trip costs about **6.5 bps**, whatever the stock. A strategy needs to earn more than that on average **before** costs just to break even. Most of the setups we test earn 1–5 bps. We charge slippage as a percentage (1 bp per fill) rather than a fixed amount per share because our price history is adjusted for splits and bonuses: Reliance shows about ₹385 in early September 2017, when it actually traded near ₹1,550. A per-share charge on adjusted prices would overstate costs in older years. We also report every strategy with **3 extra basis points of slippage on each fill**. Real fills, especially on fast breakouts, are often worse than the open of the next bar. If a result flips from profit to loss with 3 bps of slippage, it is too fragile to trade. ## 4. Three periods, judged separately We split the history into **2016–20**, **2021–23** and **2024–26** and report each one separately. A result that only works in one period is a story about that period, not a strategy. When we pick a setting from several, we pick it on 2021–23 and report the other two as untouched tests. We also say how many variants we tried: test 50 settings and one will look good by luck. ## 5. A random-stock benchmark For strategies that enter at the next bar's open, we rerun every trade 200 times on a **randomly chosen NIFTY 50 stock at the same minute, in the same direction**. If the strategy's stocks don't beat these random picks, any profit comes from the market's move that day (timing), not from picking the stock. ## What a report looks like Every backtest, on this site or in [Ask](/ask/), ends with the same table: trades, gross edge per trade, win rate, net after costs, net with +3 bps slippage, each period, profit factor and maximum drawdown, followed by automatic warnings, for example: - *Edge smaller than costs: 4.5 bps gross vs ~6.5 bps of costs per trade.* - *Not robust: loses money in 2024-26.* - *Fragile: 3 bps of extra slippage per fill wipes out the profit.* Most ideas get at least one of these. That's normal, and spotting it before you trade is the point of a backtest. **What we don't publish.** These posts cover setups that **don't** work, and the methods that show it. They are research on historical data, not advice, and nothing here tells you to buy or sell anything. --- # 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 ``` --- # Pivot points: 85,701 trades show they are coin flips > Textbook floor-pivot breakouts and bounces on every NIFTY 50 stock since 2016. Before costs, the breakout earns +1.3 bps and the bounce at the same level loses 1.3 bps. After costs, both lose crores. - Published: 2026-10-07 - URL: https://datapointx.com/research/in/pivot-points-tested/ - Tags: pivot-points, support-resistance, strategy-test - Verdict: Breakout −₹2.21 Cr, bounce −₹3.36 Cr. The two are mirror images, and both lose every year. - Source: DataPointX Research (education and research only, not investment advice) Floor pivots are on almost every Indian trading chart. From yesterday's high (H), low (L) and close (C): - **P** = (H + L + C) / 3 - **R1** = 2P − L, **S1** = 2P − H - **R2** = P + (H − L), **S2** = P − (H − L) Textbooks give two ways to trade them, and we tested both exactly: - **Breakout:** buy when price breaks above R1 (stop at P, target R2); sell when it breaks below S1 (stop at P, target S2). - **Bounce:** buy the first touch of S1 (stop at S2, target P); sell the first touch of R1 (stop at R2, target P). The order rests at the level and fills there (or at the open if price gaps through). Only the first touch counts, and the day must have opened on the other side of the level. One trade per stock per day, ₹5 lakh per trade, every NIFTY 50 stock, January 2016 to August 2026. ## The breakout and the bounce are the same trade, reversed Look at what each strategy does at R1. The breakout **buys** when price touches R1 from below. The bounce **sells** when price touches R1 from below. Same level, same moment, opposite direction. The same holds at S1. So both strategies trade exactly the same 85,701 touches, and their gross results are almost exact mirror images: +1.32 bps for the breakout, −1.35 bps for the bounce. (They aren't exact opposites because the stops and targets sit at different levels.) That mirror is the finding. If pivot levels carried real information, one side would earn a meaningful edge and the other would lose it. Instead the price after a touch is close to a coin flip: a basis point or so either way, against a round-trip cost of about 6.5 bps. ## Every year, both ways Neither strategy had a single profitable year, and neither turned profitable in 2024–26. | Variant | Trades | Gross / trade | Win % | Net after costs | Net with +3 bps | 2016-20 | 2021-23 | 2024-26 | |---|---:|---:|---:|---:|---:|---:|---:|---:| | Breakout: R1/S1, stop P, target R2/S2 | 85,701 | 1.3 bps | 49.7% | −₹2.21 Cr | −₹4.78 Cr | −₹1.13 Cr | −₹59.1 L | −₹49.6 L | | Bounce: S1/R1, stop S2/R2, target P | 85,701 | −1.4 bps | 47.1% | −₹3.36 Cr | −₹5.92 Cr | −₹1.50 Cr | −₹96.3 L | −₹89.3 L | _₹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._ ## Why pivots feel like they work Pivots are drawn from yesterday's range, and most days trade inside a range of a similar size, so prices really do **reach** R1 and S1 often and frequently turn near them. On a chart that looks like the levels "hold". But a level that price often turns at is not the same as a level where turning is **more likely than continuing**. Across 85,701 touches, it isn't. Win rates sit near 50% for the breakout (49.7%) and the bounce (47.1%), close to what you'd expect from noise once the stop and target distances are taken into account. ## What to take from this - **Test both directions at a level.** If a breakout and the fade at the same level both look weak, the level is probably noise. - **On liquid large caps, chart levels are fairly priced.** An edge of ±1 bps is invisible next to a round trip of about 6.5 bps. - **"It held again" is how a coin flip feels.** You remember the turns at R1 and forget the breakouts through it. ### The exact code we ran ```python def pivot(setup='breakout'): """breakout: resting buy stop at R1 (sell stop at S1), stop at P, target R2 (S2). bounce: resting buy limit at S1 (sell limit at R1), stop S2 (R2), target P. Only the first touch counts and the day must have opened on the other side of the level.""" def strategy(p): lv = _pivots(p) o = p.day_open if setup == 'breakout': up, dn = lv['R1'], lv['S1'] long = (p.high >= up) & (o < up) short = (p.low <= dn) & (o > dn) ep_l, ep_s = np.maximum(up, p.open), np.minimum(dn, p.open) stop_l, stop_s = (ep_l - lv['P']) / ep_l, (lv['P'] - ep_s) / ep_s tgt_l, tgt_s = (lv['R2'] - ep_l) / ep_l, (ep_s - lv['S2']) / ep_s else: up, dn = lv['R1'], lv['S1'] long = (p.low <= dn) & (o > dn) short = (p.high >= up) & (o < up) ep_l, ep_s = np.minimum(dn, p.open), np.maximum(up, p.open) stop_l, stop_s = (ep_l - lv['S2']) / ep_l, (lv['R2'] - ep_s) / ep_s tgt_l, tgt_s = (lv['P'] - ep_l) / ep_l, (ep_s - lv['P']) / ep_s first = p.once_per_day(long | short) long, short = long & first, short & first ep = ep_l.where(long, ep_s) stop = stop_l.where(long, stop_s).clip(lower=0.0005) tgt = tgt_l.where(long, tgt_s).clip(lower=0.0005) return bt.Signals(long=long, short=short, entry_price=ep, stop=stop, target=tgt) return strategy ``` --- # Supertrend, Open = Low and Camarilla: three favourite intraday setups, tested on 10 years > Three of the most shared intraday setups in India, run on every NIFTY 50 stock from 2016 to August 2026 with real costs. 217,034 trades between them, and every one of the three loses money in all three periods. - Published: 2026-10-07 - URL: https://datapointx.com/research/in/popular-intraday-setups-tested/ - Tags: supertrend, camarilla, open-high-low, strategy-test - Verdict: Supertrend −₹1.19 Cr, Open=Low −₹31 L, Camarilla −₹2.27 Cr. All three lose in every period. - Source: DataPointX Research (education and research only, not investment advice) Ask any Indian intraday Telegram group for a setup and you'll hear one of these three. We tested each in its most common form, with no tweaking: - **Supertrend (10, 3)** on 15-minute candles. Go long when it flips up, short when it flips down. The opposite flip reverses the position, and everything closes at 15:15. - **Open = Low / Open = High.** At 09:25, if a stock's day low is still its open (within 0.1%) and it's trading above the open, buy, with a stop just under the day's low. Mirror for Open = High shorts. - **Camarilla fade.** From yesterday's range, sell the first touch of H3 (stop at H4) and buy the first touch of L3 (stop at L4). Hold to 15:15. Every NIFTY 50 stock on the days it was in the index, ₹5 lakh per trade, discount-broker and statutory charges plus 1 bp of slippage on each fill. | Variant | Trades | Gross / trade | Win % | Net after costs | Net with +3 bps | 2016-20 | 2021-23 | 2024-26 | |---|---:|---:|---:|---:|---:|---:|---:|---:| | Supertrend (10, 3) on 15-min candles | 70,319 | 3.1 bps | 45.8% | −₹1.19 Cr | −₹3.30 Cr | −₹33.0 L | −₹47.4 L | −₹38.9 L | | Open = High / Open = Low at 09:25 | 48,263 | 5.2 bps | 37.5% | −₹30.6 L | −₹1.75 Cr | −₹37,808 | −₹7.9 L | −₹22.3 L | | Camarilla H3/L3 fade, stop H4/L4 | 98,452 | 1.9 bps | 35.5% | −₹2.27 Cr | −₹5.22 Cr | −₹73.0 L | −₹66.6 L | −₹87.1 L | _₹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._ ## Supertrend: always in the market, always paying Supertrend flips often on 15-minute candles, and every flip is a new round trip: 70,319 trades, about 26 a day across the index. Its gross edge of 3.1 bps is real but less than half the cost, so after costs it lost ₹1.19 crore, in nine of eleven years. ## Open = Low: the best of the three, and still a loser Open = Low / Open = High had the highest gross edge, 5.2 bps per trade. A stock that hasn't traded below its open in the first 15 minutes does lean slightly upward for the rest of the day. But it lands in the same place: about 1 bp short of costs, −₹31 lakh in total, and −₹1.75 crore with 3 bps of extra slippage per fill. It lost money in all three periods, barely in 2016–20 (−₹38,000) and most in 2024–26 (−₹22 lakh). ## Camarilla: fading H3 rarely works The Camarilla fade bets that price touching H3 or L3 will turn back. It made just 35% winners and lost ₹2.27 crore, the worst of the three, losing in ten of the eleven years. NIFTY 50 stocks reaching H3 early in the day are more often trending than stretched. ## What to take from this - **"Works on the chart" is not "works after costs".** All three show a small positive edge before costs, which is why they look convincing on a chart: you can find plenty of examples that worked. - **More signals are not more edge.** The setup that trades most (Supertrend) loses steadily on costs alone. - **Look at 2024–26 separately.** None of the three turned profitable in the most recent period. ### The exact code we ran ```python def open_high_low(check='09:25', tol=0.001): """'Open = Low' buy / 'Open = High' sell: at the close of the `check` bar, if the day's low so far is within `tol` of the open and price is above the open -> long (stop just below the day's low); mirror for shorts.""" def strategy(p): o, lo, hi, c = p.day_open, p.day_low, p.day_high, p.close at = p.bcast(p.at(check)) long = at & (lo >= o * (1 - tol)) & (c > o) short = at & (hi <= o * (1 + tol)) & (c < o) stop = ((c - lo) / c).where(long, (hi - c) / c).clip(lower=0.002) + 0.0005 return bt.Signals(long=long, short=short, stop=stop) return strategy ``` --- # Survivorship bias: testing on today's NIFTY 50 adds 3.4% a year that never existed > The same simple portfolio on the real NIFTY 50, stock list as it was on each day, versus today's 50 stocks back-tested to 2016. Today's list turns ₹1 lakh into ₹4.9 lakh instead of ₹3.6 lakh, because it quietly picks the winners in advance. - Published: 2026-10-07 - URL: https://datapointx.com/research/in/survivorship-bias/ - Tags: survivorship-bias, method, data - Verdict: Today's 50 stocks returned 16.2% a year since 2016; the real index stocks returned 12.8%. - Source: DataPointX Research (education and research only, not investment advice) Most backtesting tools and tutorials start the same way: download the stocks in the NIFTY 50 **today**, and test a strategy on them over the last ten years. It sounds harmless. It isn't, because today's list is a list of survivors. Stocks get into the NIFTY 50 by rising; they leave it by falling. A backtest on today's 50 has been told the winners in advance. To measure how much that matters, we ran the simplest possible portfolio on two versions of the index, from January 2016 to August 2026: an equal-weight buy-and-hold of every stock in the universe, rebalanced daily. - **The real NIFTY 50:** on each day, only the stocks that were in the index that day. - **Today's NIFTY 50:** the 50 current members, back-tested over the whole period (each from the day our data starts for it). ₹1 lakh grows to about ₹3.6 lakh on the real index stocks, but to ₹4.9 lakh on today's list. The gap opens in almost every year, and the biggest gaps come in years when the eventual winners were still small: in 2018 the real index stocks lost 6.1% while today's list made 1.2%, and in 2019 the gap was 8 points (3.0% against 11.0%). ## Where the phantom return comes from Stocks enter the index after they've already gone up. Here are the current members that joined after 2016, and how much each rose in the two years **before** it was added. A backtest on today's list owns all of that rise; the real index owned none of it. | Universe | CAGR | Total return | |---|---:|---:| | Real NIFTY 50 (point-in-time) | 12.8% | 261% | | Today's 50 stocks, back-tested | 16.2% | 395% | | NIFTY 50 index (price) | 11.2% | | | Stock | Joined | Price change in the 2 years before joining | |---|---|---:| | TRENT | 2024-09-30 | +159% | | BAJFINANCE | 2017-09-29 | +111% | | ADANIENT | 2022-09-30 | +101% | | BEL | 2024-09-30 | +58% | | JSWSTEEL | 2018-09-28 | +52% | | NESTLEIND | 2019-09-27 | +26% | | INDIGO | 2025-09-30 | +21% | | HDFCLIFE | 2020-07-31 | +1% | | MAXHEALTH | 2025-09-30 | −3% | | SBILIFE | 2020-09-25 | −19% | | Year | Real NIFTY 50 | Today's 50 | |---|---:|---:| | 2016 | 8.7% | 10.7% | | 2017 | 26.9% | 31.3% | | 2018 | −6.1% | 1.2% | | 2019 | 3.0% | 11.0% | | 2020 | 19.4% | 22.9% | | 2021 | 33.9% | 37.7% | | 2022 | 6.1% | 8.3% | | 2023 | 29.6% | 29.9% | | 2024 | 10.2% | 15.2% | | 2025 | 13.3% | 12.2% | | 2026 | −1.5% | −1.5% | The other half is the stocks that are missing. 29 stocks were in the index at some point since 2016 and aren't today, including YES Bank, Zee Entertainment, Vedanta, IndusInd Bank and BPCL. Several left after large falls. A backtest on today's list never holds them, so it never takes those losses. ## A bonus finding: the gains came overnight The same data shows something most intraday traders don't expect. On the real index, the average stock **fell** 8.1 bps between the open and the close of a typical day, even though the stocks gained 12.8% a year overall. The decade's gains came between one day's close and the next day's open. Buying at the open and selling at the close, every day, would have lost money before any costs. **Limits of this test.** Prices are back-adjusted for splits and bonuses but exclude dividends. Three former members have no minute data in our snapshot (HDFC Ltd, Cairn India, Tata Motors DVR), so the real-index portfolio is one to three stocks short on some days. Two of them left through mergers (HDFC Ltd into HDFC Bank, Cairn India into Vedanta), so their effect on the comparison is likely small. ## What to take from this - **Always test on the stocks that were in the universe on each day.** A "today's list" backtest adds about 3.4% a year here, before any strategy is involved. - **The bias is biggest for long holding periods and momentum ideas,** because the winners are exactly the stocks that rose before joining. - **Every DataPointX result uses point-in-time membership.** It's built into [dpx_bt](/docs/) and can't be switched off. --- # The win-rate trap: 84% winners, and the biggest loss of the lot > Same 120,643 entries, six different profit targets. As the win rate climbs from 43% to 84%, the loss after costs more than doubles. Why chasing win rate is the fastest way to lose money intraday. - Published: 2026-10-07 - URL: https://datapointx.com/research/in/win-rate-trap/ - Tags: win-rate, risk-reward, strategy-test - Verdict: Win rate 43% → 84% while the loss more than doubles, −₹1.98 Cr → −₹4.52 Cr. - Source: DataPointX Research (education and research only, not investment advice) "80% accuracy" is the most common pitch in Indian trading courses and Telegram groups. A high win rate is easy to build: take profits quickly. Whether it makes money is a separate question, and the two are easy to confuse. To separate them we held **everything** fixed except the profit target. The entry is a plain opening range breakout: the first 5-minute close above (or below) the first 15 minutes' range, one trade per stock per day, a 1% stop, ₹5 lakh per trade, every NIFTY 50 stock from January 2016 to August 2026. That gives the same 120,643 trades every time. Only the target changes: 0.1%, 0.25%, 0.5%, 1%, 2%, or none (hold to 15:15). ## Why more winners lose more money A 0.1% target on a ₹5 lakh trade is ₹500. The 1% stop is ₹5,000. Every winner is small and every loser is ten times bigger. To break even you need about 91% winners **before** costs, and with ~6.5 bps (about ₹325) of costs per trade, the ₹500 winner shrinks to roughly ₹175. Tiny targets also make the trade very short (the median trade lasts about 5 minutes), so you pay the full round-trip cost again and again for a move that is mostly noise. As the target widens, fewer trades hit it, but each winner pays for more losers. Once the target is gone, trades are held to 15:15 and keep the full move when the breakout follows through. That version has the lowest win rate and the smallest loss. It still loses: this entry has very little edge to begin with (about 3 bps before costs at best), which the [ORB guide test](/research/in/orb-strategy-guide-tested/) also found. | Variant | Trades | Gross / trade | Win % | Net after costs | Net with +3 bps | 2016-20 | 2021-23 | 2024-26 | |---|---:|---:|---:|---:|---:|---:|---:|---:| | Target 0.1% | 1,20,643 | −1.0 bps | 83.9% | −₹4.52 Cr | −₹8.13 Cr | −₹2.26 Cr | −₹1.26 Cr | −₹1.00 Cr | | Target 0.25% | 1,20,643 | −0.5 bps | 74.0% | −₹4.19 Cr | −₹7.80 Cr | −₹2.05 Cr | −₹1.18 Cr | −₹96.1 L | | Target 0.5% | 1,20,643 | 0.2 bps | 60.5% | −₹3.78 Cr | −₹7.40 Cr | −₹1.79 Cr | −₹1.09 Cr | −₹90.2 L | | Target 1.0% | 1,20,643 | 1.0 bps | 48.1% | −₹3.33 Cr | −₹6.95 Cr | −₹1.51 Cr | −₹93.8 L | −₹88.3 L | | Target 2.0% | 1,20,643 | 2.4 bps | 43.6% | −₹2.49 Cr | −₹6.10 Cr | −₹93.9 L | −₹78.0 L | −₹76.6 L | | No target (hold to 15:15) | 1,20,640 | 3.2 bps | 42.9% | −₹1.98 Cr | −₹5.59 Cr | −₹51.7 L | −₹69.8 L | −₹76.2 L | _₹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._ ## What to take from this - **Win rate alone tells you nothing about profit.** Always ask for the average win, the average loss and the cost per trade. Profit factor (gross wins ÷ gross losses) is a better single number: below 1.0 is a loser, whatever the win rate. - **Small targets make costs dominate.** At ₹5 lakh a round trip costs about ₹325. A ₹500 target spends most of its profit on charges. - **A seller quoting accuracy without these numbers is giving you the number that's easiest to inflate.** ### The exact code we ran ```python def orb_fixed_target(target=None, stop=0.01, or_min=15): """First 5-minute close beyond the 15-minute opening range, stop 1%, fixed % target (None = hold to 15:15).""" def strategy(p): orh, orl = p.opening_range(or_min) long, short = p.close > orh, p.close < orl first = p.once_per_day(long | short) return bt.Signals(long=long & first, short=short & first, stop=stop, target=target) return strategy ``` --- # dpx_bt reference `dpx_bt` runs intraday backtests on 1-minute / 5-minute bars of every stock that was in the NIFTY 50 at any time since 2016 (point-in-time membership, so no survivorship bias). Data snapshot: 2016-01-01 to 2026-08-31. Prices and volumes are back-adjusted for splits and bonuses (by the data source), so multi-day lookbacks are consistent; a 2017 price is the adjusted price, not the one printed on that day. ## Writing a strategy A strategy is a function `strategy(p) -> Signals`. It is called once per calendar year with a `Panel` `p` that holds that year's bars plus ~45 days of warm-up history. Build boolean DataFrames (rows = bar start times, columns = symbols) and return them: ```python import dpx_bt as bt def strategy(p): orh, orl = p.opening_range(15) # 09:15-09:30 high / low, NaN until the range is complete long = p.once_per_day(p.crossed_above(p.close, orh)) short = p.once_per_day(p.crossed_below(p.close, orl)) return bt.Signals(long=long, short=short, stop=0.01, target=0.02) ``` ### Execution rules (fixed; you cannot change them) - A signal is read at the bar's CLOSE and filled at the NEXT bar's OPEN, same day only. - Entries only inside the entry window (default 09:30-14:30 bar starts) and only for stocks that are NIFTY 50 members that day. - Every position is closed at the 15:15 bar's open (15:10 from 2026-08-03). - One position per stock at a time; an opposite signal reverses the position. - Fixed notional per trade (default Rs 5,00,000), whole shares. - Costs: typical Indian discount-broker intraday charges (₹20 per order or 0.03% if lower, plus STT, NSE transaction, SEBI, stamp duty and GST) + 1 bp of slippage on every fill (entry and exit). Reports also show a +3 bps per fill stress test. ### `Signals` fields | field | type | meaning | |---|---|---| | `long`, `short` | bool DataFrame | entry signals (evaluated at bar close) | | `long_exit`, `short_exit` | bool DataFrame | optional exit signals (filled at the next bar's open) | | `stop`, `target` | float or DataFrame | fraction of the fill price (0.01 = 1%). A DataFrame is read at the signal bar, e.g. `0.5 * p.atr() / p.close` | | `trail` | bool | make the stop trailing | | `entry_price` | DataFrame | fill ON the signal bar at this price (stop/limit orders at a known level, e.g. the OR high). The signal must then only use information known before that level trades | ## Panel `p` Wide DataFrames, index = bar start time (IST), columns = symbols: `p.open`, `p.high`, `p.low`, `p.close`, `p.volume`, `p.member` (bool: NIFTY 50 member that day and has a bar). Per-bar Series: `p.day` (date), `p.tod` (minutes since midnight), `p.bar_of_day`, `p.first_bar`. `p.at('10:00')`, `p.between('09:30', '11:00')` return bool Series; `p.bcast(series)` turns a per-bar Series into a panel-shaped frame. `p.symbols`, `p.index`, `p.tf` ('1m'/'5m'), `p.minutes` (bar length). Prior-day values (known before today's open): `p.prev_day('close'|'high'|'low'|'open'|'volume')`, `p.atr(14)`, `p.daily_sma(n)`, `p.daily_ema(n)`. The full daily history is `p.daily.open/high/low/close/volume` (date x symbol; row d contains all of day d: shift(1) before using it on day d) and `p.from_daily(df)` maps a daily frame onto bars. Intraday values known at each bar's close: `p.day_open`, `p.day_high`, `p.day_low` (so far, including this bar), `p.vwap`, `p.cum_volume`, `p.ret_since_open()`, `p.opening_range(minutes)` -> (high, low), `p.rel_volume(minutes, lookback=14)` (first-`minutes` volume vs its average over prior days), `p.rsi(n)`, `p.ema(n)`, `p.sma(n)`, `p.bar_atr(n)`. Helpers: `p.crossed_above(a, b)`, `p.crossed_below(a, b)`, `p.once_per_day(mask)` (first True per stock per day). Cross-sectional ranks work with pandas directly, e.g. `p.ret_since_open().rank(axis=1, ascending=False) <= 3`. ## Running ```python report = bt.backtest(strategy, tf='5m', start='2016-01-01', end='2026-08-31', notional=500_000, window=('09:30', '14:30'), symbols=None, name='my idea') report.summary() # dict: overall, periods (2016-20 / 2021-23 / 2024-26), years, warnings report.markdown() # the standard table report.trades # one row per trade: symbol, entry_time, exit_time, side, qty, entry_price, exit_price, # gross_bps, net, net_3bps, notional report.equity() # cumulative net P&L by day ``` `5m` runs take ~10 s per year; `1m` runs ~5x longer. Use 5m unless the idea needs minute precision. ## Other data - `bt.members()`: NIFTY 50 membership (symbol, instrument, from_date, to_date). - `bt.daily_bars()`: daily OHLCV for the same stocks (columns ts, symbol, open, high, low, close, volume). - `bt.intraday_bars(tf, start, end, symbols=None)`: long-format bars. - `bt.reference(name)` with name one of `in.nse.index_daily` (all NSE indices incl. 'Nifty 50' and 'India VIX'), `in.nse.fo_futures_daily`, `in.nse.fo_participant_oi`, `in.nse.fo_participant_vol`, `in.nse.fii_deriv_stats`, `in.nsdl.fpi_daily` (all daily, column `date`). - `bt.charges(buy_value, sell_value)`, `bt.net_pnl(long, qty, entry, exit, extra_bps=0)` for custom P&L.