# 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
```
