26,965 trades, −₹1.09 Cr. The pattern does worse than a random stock at the same minute.
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.
Show as table
| Year | Trades | Net | Win % |
|---|---|---|---|
| 2016 | 2,268 | −₹7.1 L | 34.3 |
| 2017 | 2,308 | −₹10.8 L | 33.6 |
| 2018 | 2,670 | −₹9.8 L | 35.1 |
| 2019 | 2,597 | −₹7.7 L | 35.1 |
| 2020 | 2,560 | −₹13.2 L | 33.4 |
| 2021 | 2,561 | −₹12.2 L | 33.3 |
| 2022 | 2,563 | −₹10.1 L | 35.1 |
| 2023 | 2,390 | −₹11.7 L | 33.2 |
| 2024 | 2,718 | −₹11.6 L | 34 |
| 2025 | 2,648 | −₹9.2 L | 34.7 |
| 2026 | 1,682 | −₹5.8 L | 36.3 |
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.
The best variant earns under 1 bp before costs. A ₹5 lakh round trip costs about 6.5 bps.
| Variant | Trades | Gross / trade | Win % | Net after costs | +3 bps slippage | 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. Generated 2026-10-07.
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 rancandle_pattern() in dpx_bt · 6 variants · snapshot v2026.08
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 strategyEvery variant in this post is this function with different arguments, run through dpx_bt over Jan 2016 – Aug 2026.
Have a variation in mind?
Ask runs your own idea on the same data and rules, with the same report: net after costs, +3 bps stress test, three periods.
Backtest on historical data (Jan 2016 – Aug 2026), net of discount-broker (₹20 per order) and statutory charges and 1 bp slippage per fill. Education and research only, not investment advice. Past results do not predict future returns. Disclaimer.