Strategies we backtested chart pattern strategy

Tested by BacktestingMax

7 well-known chart patterns, backtested and ranked

Do chart patterns work?

No, not with the usual breakout rules. In our backtest, 7 well-known chart patterns, traded as one strategy with one trade at a time, ended close to zero after trading costs. We wrote exact rules for double tops and bottoms, head and shoulders, ascending and descending triangles, symmetrical triangles, flags, wedges and the cup and handle. A trade opened when a candle closed beyond the pattern's breakout line, like the neckline of a head and shoulders. The stop-loss was at the last swing point on the other side of that line. The take-profit was the measured move: the pattern's height, counted from the breakout line. We ran 30 backtests from 2019 to 2025: 10 markets, each on 1-hour, 4-hour and daily charts. R is the amount a trade risks: 1% of the account. Together, the patterns lost 0.009R per trade over 7,503 trades. They made money in 14 of 30 tests, but 7 of those were daily tests with only 19 to 34 trades each. Alone, the symmetrical triangle made the most per trade (+0.125R), but on too few trades to be sure. Waiting for price to come back to the breakout line helped a little: +0.039R per trade, or +0.021R without the trades where price only touched the order.

30 backtests 2019–2025 Trading costs included Tested 26 October 2026

Where it worked 30 backtests
Made money
7
of 30 tests, when all 7 patterns are traded together
Made money in both periods
2
2019–2022 and again 2023–2025
Best backtest
+49%
S&P 500, 4-hour chart: account growth 2019–2025
Double top and double bottom only
1
tests made money in both periods, compared with 2 when all 7 patterns are traded together

Made money in both periods

  • USD/CAD, 4-hour
  • S&P 500, 4-hour

The results

Results on every market

Each box shows one backtest of the chart pattern strategy when all 7 patterns are traded together, from 2019 to 2025, after trading costs: the % result, and a small chart of the account balance. The solid line is the account balance and the dashed line is the starting balance. Green results made money; red results lost money. Under each result: whether it made money in both 2019–2022 and 2023–2025, or passed all four of our checks (see "How a test passes" further down).

Market 1-hour 4-hour daily
EUR/USD −10% Lost money −11% Lost money +10% Too few trades
GBP/USD −30% Lost money −12% Lost money +5% Too few trades
USD/JPY +9% Made money overall, lost in one period −10% Lost money +6% Too few trades
AUD/USD −47% Lost money −5% Lost money +4% Too few trades
USD/CAD −23% Lost money +7% Made money in both periods −10% Too few trades
Gold −6% Lost money −9% Lost money +11% Too few trades
S&P 500 −27% Lost money +49% Passed all four checks +4% Too few trades
Nasdaq 100 +12% Made money overall, lost in one period −4% Lost money +1% Too few trades
Dow Jones −21% Lost money +10% Made money overall, lost in one period −0.6% Too few trades
DAX +1% Made money overall, lost in one period +5% Made money overall, lost in one period −7% Too few trades
All the numbers, test by test
Market Chart Trades Win rate Profit factor Result Max drawdown 2019–2022 2023–2025 Monte Carlo drawdown Trading costs
EUR/USD 1-hour 542 39.9% 0.97 −9.8% −27.6% −2.9% −7.1% −45.9% $1,653
EUR/USD 4-hour 170 42.9% 0.87 −11.1% −18.6% −9.5% −1.8% −26.5% $249
EUR/USD daily 30 50% 1.73 +10.3% −4.6% +2.0% +8.1% −7.0% $15
GBP/USD 1-hour 562 39.9% 0.89 −30.2% −45.8% −24.1% −8.1% −53.1% $1,663
GBP/USD 4-hour 172 37.8% 0.88 −11.5% −19.4% −3.9% −8.0% −29.5% $240
GBP/USD daily 24 58.3% 1.53 +5.0% −3.3% +6.3% −1.2% −5.2% $11
USD/JPY 1-hour 604 42.1% 1.03 +9.0% −27.1% −3.5% +12.9% −40.7% $1,830
USD/JPY 4-hour 148 39.9% 0.88 −9.6% −17.4% −13.6% +4.7% −26.4% $237
USD/JPY daily 25 48% 1.47 +5.7% −5.2% +3.5% +2.1% −6.4% $16
AUD/USD 1-hour 616 40.9% 0.83 −46.8% −52.9% −31.6% −22.2% −59.8% $1,827
AUD/USD 4-hour 173 44.5% 0.95 −5.3% −25.7% +9.7% −13.7% −25.1% $304
AUD/USD daily 21 47.6% 1.34 +3.5% −3.6% +2.1% +1.4% −6.1% $54
USD/CAD 1-hour 603 40.1% 0.94 −22.8% −42.6% +11.6% −30.8% −55.7% $2,765
USD/CAD 4-hour 173 44.5% 1.07 +6.5% −11.6% +1.4% +5.1% −22.6% $339
USD/CAD daily 34 32.4% 0.53 −9.6% −11.5% −7.9% −1.8% −14.2% $25
Gold 1-hour 556 40.5% 0.98 −5.7% −40.5% −11.9% +7.0% −43.6% $1,762
Gold 4-hour 174 36.8% 0.90 −8.6% −18.2% −11.3% +3.0% −28.6% $247
Gold daily 22 63.6% 2.80 +10.7% −1.2% +8.1% +2.4% −3.2% $8
S&P 500 1-hour 530 40% 0.90 −27.3% −33.3% −13.6% −15.9% −49.3% $1,058
S&P 500 4-hour 158 50% 1.50 +48.7% −9.2% +18.2% +25.8% −14.7% $175
S&P 500 daily 27 55.6% 1.33 +3.6% −3.3% −0.7% +4.3% −5.8% $7
Nasdaq 100 1-hour 508 40% 1.03 +11.7% −34.3% +47.4% −24.2% −40.6% $1,380
Nasdaq 100 4-hour 161 39.8% 0.96 −3.6% −17.6% +12.5% −14.3% −23.0% $140
Nasdaq 100 daily 22 40.9% 1.12 +1.2% −4.2% +2.9% −1.7% −6.0% $7
Dow Jones 1-hour 512 40.6% 0.91 −21.4% −33.1% −14.4% −8.2% −45.6% $515
Dow Jones 4-hour 165 42.4% 1.11 +10.1% −17.1% +25.1% −12.0% −19.9% $103
Dow Jones daily 19 36.8% 0.95 −0.6% −4.2% +1.8% −2.3% — $11
DAX 1-hour 567 40.7% 1.00 +1.3% −24.4% −9.5% +11.9% −40.5% $644
DAX 4-hour 155 43.9% 1.06 +4.7% −8.8% −2.0% +6.8% −18.9% $85
DAX daily 30 30% 0.53 −7.4% −8.0% −2.3% −5.2% −11.0% $11

Profit factor is the money won divided by the money lost. Above 1 means the test made money. Max drawdown is the biggest fall from the highest balance. 2019–2022 and 2023–2025 show the result in each period on its own. Monte Carlo drawdown: we shuffled the trades into a random order 1,000 times; in 95% of those runs, the worst drawdown was this number or less. Result is the change in the account, with the risk of each trade set to 1% of the account at that time.

The account balance

The best test and the middle test

Best test S&P 500, 4-hour chart +48.7% from 2019 to 2025
Middle test (median): half of the 30 did better, half did worse Dow Jones, daily chart −0.6% from 2019 to 2025

What we found

What the 30 backtests show

  1. Together, the 7 patterns ended close to zero: they lost 0.009R per trade over 7,503 trades, after trading costs. For every $100 a trade risked, it lost about 90 cents on average. Before costs, the trades made +0.016R. Charts where each candle covers more time did better: −0.020R per trade on 1-hour charts (5,600 trades), +0.014R on 4-hour charts (1,649 trades) and +0.087R on daily charts (254 trades). So the overall result mostly shows the 1-hour charts: 5,600 out of the 7,503 trades came from them. On 1-hour charts, trading costs took 0.029R per trade, more than the +0.009R the trades made before costs. Patterns on daily charts were rare: each daily test made only 19 to 34 trades in 7 years. The 'Where it worked' box counts only tests with at least 100 trades, so it shows 7 of the 20 tests on 1-hour and 4-hour charts.

  2. Traded alone, the symmetrical triangle made the most per trade: +0.125R over 542 trades. Added up over all 30 tests, its trades made money in 2019–2022 (+0.148R per trade) and again in 2023–2025 (+0.095R). But none of its tests reached the 100 trades we need to judge one test. The wedges did worst: −0.040R per trade over 3,271 trades (see the ranking below). Wedges paid the most in trading costs, 0.039R per trade, because their stop-loss was often close to the entry. Before costs, they broke even. The buy form (the bullish form) of every pattern did better than its sell form (the bearish form), and all 7 sell forms lost money. The gap was biggest on the 6 markets that rose the most from 2019 to 2025: gold, the 4 stock indices and USD/JPY. There, the buy forms made +0.088R per trade, and the sell forms lost 0.053R. So part of the buy forms' result may come from these markets rising, not from the patterns.

  3. A high win rate (the share of trades that made money) did not mean more money. The measured move put the take-profit at a different distance for each pattern. For double tops and bottoms, it was usually closer than the stop-loss: the median trade aimed to make 0.78R, so half of the trades aimed for less. For flags, the target is the whole pole (the move just before the flag): the median trade aimed for 3.47R. So double tops and bottoms won 59.0% of their trades and still lost 0.005R per trade. Flags won only 23.4% of their trades, but made +0.028R per trade.

  4. Many breakouts failed within 3 candles. In 2,975 out of 7,503 trades (39.7%), a candle closed back inside the pattern within 3 candles of the breakout. Those trades lost 0.422R per trade on average, and only 25.4% of them won. The other trades made +0.262R per trade. You cannot know in advance which breakouts will fail, so we tested a rule traders use against them: wait for a second candle to close beyond the line. Then 26.8% of the trades closed back inside within 3 candles of the second close, compared with 39.7%. But the trade opened later, further from the line, so the take-profit was closer: the median trade aimed for 1.22R, compared with 1.45R. The average trade still lost 0.003R, and 10 of 30 tests made money, compared with 14.

  5. Waiting for a retest helped a little, but part of the gain may not hold in live trading. A retest is when price comes back to the breakout line after the breakout. In this change, after the breakout close, we placed a limit order (an order that waits at a set price) at the breakout line's price, for up to 10 candles. Of the 7,965 orders we placed, 6,375 filled. The 7 patterns together then made +0.039R per trade, compared with −0.009R. The entry was closer to the stop-loss and further from the take-profit, so the trades won less often (32.9%, compared with 41.0%), but each win made more. In live trading, a limit order often does not fill when the market only touches its price. In 630 trades, price went less than one spread past the order's price, and those trades made +0.207R per trade. Without them, the retest made +0.021R per trade, still better than −0.009R. So in live trading, the result would probably be between +0.021R and +0.039R per trade.

The ranking

Every pattern traded alone, ranked by its average trade

Each row is one pattern traded on its own, on all 30 tests (10 markets, each on 1-hour, 4-hour and daily charts), from 2019 to 2025, after trading costs. It uses the rules under 'The rules we tested', further down. 'Won' is the share of trades that made money. 'Take-profit distance' is how far the take-profit was from the entry, in R, for the median trade: half of the trades aimed for less, half for more. R is the amount a trade risks: 1% of the account when the trade opens. The average trade counts every trade from all 30 tests together. 'Buy form / sell form' splits the average trade into the bullish forms and the bearish forms. The bullish forms are the double bottom, inverse head and shoulders, ascending triangle, symmetrical triangle broken upward, bull flag, falling wedge and cup and handle. The bearish forms are their mirror images: the double top, head and shoulders, descending triangle, symmetrical triangle broken downward, bear flag, rising wedge and inverted cup and handle. 'Tests that made money' counts all 30 tests, including those with fewer than 100 trades.

Pattern Trades Won Take-profit distance Average trade Buy form / sell form Tests that made money
Symmetrical triangle 542 31.9% 2.85R +0.125R +0.272R / −0.023R 18 of 30
Bull flag and bear flag 1,002 23.4% 3.47R +0.028R +0.190R / −0.170R 16 of 30
Ascending and descending triangle 1,423 44.4% 1.33R +0.007R +0.032R / −0.025R 14 of 30
Double top and double bottom 3,087 59.0% 0.78R −0.005R +0.028R / −0.040R 15 of 30
Cup and handle 596 32.6% 2.15R −0.019R +0.101R / −0.183R 12 of 30
Head and shoulders 712 42.0% 1.43R −0.035R +0.064R / −0.145R 10 of 30
Rising and falling wedge 3,271 33.4% 1.97R −0.040R −0.032R / −0.046R 11 of 30

Traded alone, a pattern also takes the breakouts it missed while another pattern's trade was open. So the rows add up to more than the 7,503 trades of all 7 patterns together. Most patterns were rare on daily charts: in 7 years on 10 markets, the cup and handle made 16 daily trades, the symmetrical triangle 17 and the flags 22. Every test of the head and shoulders, the symmetrical triangle and the cup and handle had fewer than 100 trades, the number we need to judge one test. So the places in this ranking could easily change with more years of data. The symmetrical triangle did make money in both halves, +0.148R per trade in 2019–2022 and +0.095R in 2023–2025, but on few trades.

By chart

Daily charts did best, and the retest entry did better on 1-hour, 4-hour and daily charts

Each bar is the average trade of all 7 patterns together, on all 10 markets, from 2019 to 2025, after trading costs. R is the amount a trade risks: 1% of the account when the trade opens. 'Breakout close' is our standard entry: the trade opens at the next candle's open after the first candle that closes beyond the breakout line. 'Retest' waits with a limit order at the breakout line's price on the breakout candle, for up to 10 candles.

  • 1-hour: breakout close5,600 trades −0.020R
  • 1-hour: retest4,748 trades +0.016R
  • 4-hour: breakout close1,649 trades +0.014R
  • 4-hour: retest1,411 trades +0.100R
  • Daily: breakout close254 trades +0.087R
  • Daily: retest216 trades +0.148R

Daily charts gave only 19 to 34 trades per market in 7 years, so their averages are less certain. The retest bars count every trade the backtest filled, including the 630 trades where price went less than one spread past the order's price.

The entry

Three ways to enter a breakout, tested on all 7 patterns together

Each row uses the same patterns, stop-loss and take-profit, on the same 30 tests, from 2019 to 2025, after trading costs. Only the entry changes. R is the amount a trade risks: 1% of the account when the trade opens. 'Made money in both periods' and 'Passed all four checks' only count tests with at least 100 trades: the 20 tests on 1-hour and 4-hour charts. A test made money in both periods if it made money in 2019–2022 and again in 2023–2025. The four checks are explained under 'How a test passes', further down.

Entry Trades Won Average trade Tests that made money Made money in both periods Passed all four checks
Breakout close: the next candle's open after the first close beyond the line (our standard rules) 7,503 41.0% −0.009R 14 of 30 2 of 20 1 of 20
Two closes: the next candle's open after a second close beyond the line 6,082 44.8% −0.003R 10 of 30 2 of 20 1 of 20
Retest: a limit order at the breakout line's price on the breakout candle, for up to 10 candles 6,375 32.9% +0.039R 19 of 30 6 of 20 2 of 20

With two closes, if the second candle closed back inside the pattern, there was no trade. With the retest, the limit order was cancelled if price reached the take-profit first. A limit order pays no spread when it opens a trade, so a retest trade that reaches its take-profit pays no trading cost in this backtest. In live trading, a limit order is not always filled when the market only touches the order's price. In 630 out of 6,375 retest trades, price went less than one spread past it. The retest made money in both periods on 6 tests: EUR/USD, AUD/USD, the S&P 500 and the DAX on 4-hour charts, and gold and the DAX on 1-hour charts. It passed all four checks on the S&P 500 and the DAX, both on 4-hour charts.

The strategy

What the chart pattern strategy is

On this page, the chart pattern strategy means all 7 patterns traded as one strategy, on one account, with one trade at a time. A chart pattern is a shape that price draws over many candles, made of swing highs (peaks) and swing lows (dips). Traders read some patterns as a sign that the trend will turn, like the double top and the head and shoulders. They read others as a sign that the trend will go on, like the flag and the cup and handle. Every pattern has a breakout line: a price level, or a trendline (a straight line through two swing points). When a candle closes beyond that line, the pattern is complete, and traders open a trade in that direction. The usual target is the measured move: the pattern's height, added to the breakout price. Most traders find patterns by eye, so two traders can see different patterns on the same chart. To test them, we wrote exact rules for each pattern, built from swing points. A swing high is a candle whose high is higher than the highs of the 3 candles before it and the 3 candles after it. A swing low is the mirror image: a low lower than the lows of the 3 candles on each side. So a swing point is only known 3 candles later, and the backtest waits for those 3 candles. Swing highs and swing lows must come one after the other. If two swing highs come with no swing low between them, only the higher one counts. Of two swing lows in a row, only the lower one counts. Our rules test the shapes alone: they do not check the trend before a pattern, the size or speed of a flag's pole, or volume. The table under 'Can the chart pattern strategy be improved?', further down, gives the exact rules for each of the 7 patterns.

Patterns
7 patterns, each with a buy form (bullish) and a sell form (bearish). They are: double top and bottom, head and shoulders, ascending and descending triangle, symmetrical triangle, bull and bear flag, rising and falling wedge, cup and handle.
Swing points
A candle whose high is higher (or whose low is lower) than those of the 3 candles on each side. Known 3 candles later.
About the same price
Two swing points differ by at most 15% of the pattern's height. A line through two such points counts as flat.
Widest pattern
The two tops of a double top, and the two rims of a cup, are at most 60 candles apart
Pattern expires
When the next swing point is known
Entry
At the open of the candle after the first close beyond the breakout line
Stop-loss
At the last swing point on the other side of the pattern. It does not move.
Take-profit
The measured move: the pattern's height, counted from the breakout line
Risk per trade
1% of the account
Skipped candles
We ignored two kinds of candles. In some years, a few minutes of index prices after the Friday close make a tiny extra candle. And candles with no price movement mark market holidays in our data.
Smallest trade size
Every test starts with $10,000, so a 1% risk starts at $100. When even the smallest trade allowed would have risked more than 1%, the backtest skipped the trade. With all 7 patterns together, this happened 74 times, 50 of them on the Dow Jones. On the Dow Jones daily chart, the backtest skipped 23 trades and made 19, so that test says little. On 53 trades, the stop-loss was so close that a 1% risk needed a bigger trade than the largest trade the account could open. Those trades were smaller, so they could lose less than 1R.
Trading costs
The spread is the gap between the buy price and the sell price. We used 0.8 pips on EUR/USD, 1.2 on GBP/USD, 0.9 on USD/JPY, 1.0 on AUD/USD and 1.3 on USD/CAD (a pip is 0.0001 in price, or 0.01 on USD/JPY). We used $0.30 on gold, and 0.4 points on the S&P 500, 1.5 on the Nasdaq 100, 2 on the Dow Jones and 1 on the DAX. A trade pays half the spread when it opens at the market price, and half when the stop-loss closes it. The take-profit is a limit order (an order that waits at a set price), so it pays no spread: the backtest fills it as soon as price touches it. In 92.0% of the take-profit exits, price went at least one spread past it, so those exits would very likely have filled in live trading too. There is no commission.

The rules we tested

  1. Find the swing points: candles whose high is higher than the highs of the 3 candles on each side, or whose low is lower than their lows. Each time a new swing point is known, check whether the newest swing points form one of the 7 patterns. The table under 'Can the chart pattern strategy be improved?' gives the rules for each pattern.
  2. Trade a pattern only if a candle closes beyond its breakout line before the next swing point is known. After that, drop the pattern. The trade opens at the next candle's open. Buy after a close above the line. Sell short after a close below it: selling short means you sell first and buy back later, so the trade makes money if price falls.
  3. Skip the pattern if a candle had already closed beyond the breakout line before the pattern was known. Also skip it if a candle first closes beyond the pattern's other side: the stop-loss level, or for a triangle or a wedge, its other line. Skip a triangle or a wedge if its two lines cross first.
  4. Put the stop-loss at the last swing point on the other side of the pattern, exactly at that swing point's high or low, with no extra room. For example: the second top of a double top, or the right shoulder (not the head) of a head and shoulders. The stop-loss does not move.
  5. Put the take-profit at the measured move: the pattern's height, added to the breakout line's price on the breakout candle, in the direction of the trade. The table further down says how we measured each pattern's height.
  6. Size every trade so that a loss at the stop-loss costs 1% of the account balance when the trade opens. That amount is 1R.
  7. Only one trade is open at a time. A pattern that breaks out while a trade is open is skipped. If two patterns break out in opposite directions on the same candle, there is no trade.
  8. One shape counts as one pattern. If the newest swing points form two patterns that trade in the same direction, only one counts. It is the first of them in this list: head and shoulders, cup and handle, flag, ascending or descending triangle, symmetrical triangle, wedge, double top or bottom. The swing points of a flag never count as a triangle or a wedge. The same rule applies when a pattern trades alone.

The method

How we tested it

The test

Markets
EUR/USD, GBP/USD, USD/JPY, AUD/USD, USD/CAD, Gold, S&P 500, Nasdaq 100, Dow Jones and DAX
Charts
1-hour, 4-hour and daily candles
Dates
January 2019 to December 2025
Account
$10,000, risking 1% of the account on each trade
Trading costs
Included. Every trade pays that market's normal trading cost: the spread, which is the gap between the buy price and the sell price.

How a test passes

We call a test passed only if it meets all four checks. A strategy can make money in one backtest and still fail one of these checks.

  • At least 100 trades. With fewer trades, a good result could be luck.
  • It made money from 2019 to 2022 and again from 2023 to 2025. This shows whether the strategy kept working in the newer years, not only in the older ones.
  • A profit factor (the money won divided by the money lost) of at least 1.2 from 2023 to 2025, after trading costs.
  • In 95% of 1,000 random shuffles of the trades, the account must not fall more than 30% from its highest point.

Improvements

Can the chart pattern strategy be improved?

Can one pattern on its own do better than all 7 together? Here each pattern trades alone, with the rules above, on the same 30 tests. The table also gives the exact rules for each pattern. If you pick the best pattern and then judge it on the same years, it will almost always look good. So we picked the best pattern using 2019–2022 only: the pattern that made money in the most tests. If two patterns tied, we took the one with the better middle test, the test in the middle when its 30 results for 2019–2022 are ranked. Then we checked that pattern on 2023–2025, the years we did not use to pick it. Most patterns are rare, so most of their tests had fewer than 100 trades. Those tests do not count in 'Made money in both periods' or 'Passed all four checks', but they did count when we picked the best pattern.

Version Made money 2019–2022years used to pick the best version Made money 2023–2025years not used to pick it Made money in both periods Passed all four checks
All 7 patterns together Every pattern below, traded with the rules above as one strategy. One trade at a time, so a pattern that breaks out while a trade is open is skipped. 14 of 30middle test −1.4% 13 of 30middle test −1.8% 2 1
Double top and double bottom onlyBest on 2019–2022 Double top, a sell signal: two swing highs at about the same price, at most 60 candles apart. The second is the newest swing high, and every swing high between them is lower than both. Sell when a candle closes below the lowest swing low between the two tops. Stop-loss: the second top, even when the first top was higher. Height: from the higher top down to that low. Double bottom, a buy signal: the mirror image. 16 of 30middle test +0.6% 14 of 30middle test −0.2% 1 0
Head and shoulders only Head and shoulders, a sell signal: 5 swing points in a row. They are a high (the left shoulder), a low, a higher high (the head), a low, and a lower high (the right shoulder). The two shoulders are at about the same price and above the neckline, the line through the two lows. Sell when a candle closes below the neckline. Stop-loss: the right shoulder. Height: from the head down to the neckline. Inverse head and shoulders, a buy signal: the mirror image. 12 of 30middle test −0.3% 10 of 30middle test −0.5% 0 0
Ascending and descending triangles only Ascending triangle, a buy signal: 2 swing highs and 2 swing lows in a row. The two highs are at about the same price, so the top line is flat. The second low is higher than the first by more than 15% of the height. Buy when a candle closes above the top line. Stop-loss: the last swing low. Height: the gap between the two lines at the first of the 4 swing points, where the triangle is widest. Descending triangle, a sell signal: the mirror image, with a flat bottom line and falling highs. 11 of 30middle test −0.2% 17 of 30middle test +1.0% 1 0
Symmetrical triangle only 2 swing highs and 2 swing lows in a row. The second high is lower than the first, and the second low is higher than the first, both by more than 15% of the height. So the two lines get closer and would cross later. Buy when a candle closes above the top line, or sell when a candle closes below the bottom line, whichever comes first. Stop-loss: the last swing point on the other side. Height: the gap between the lines at the first swing point. 14 of 30middle test 0.0% 14 of 30middle test 0.0% 0 0
Bull flag and bear flag only Bull flag, a buy signal: a swing low, then a swing high (the rise between them is the pole). Then a pullback gives back at most half of the pole, and a lower swing high follows. Buy when a candle closes above the line through the two highs. Stop-loss: the pullback's low. Height: the pole. Bear flag, a sell signal: the mirror image. 14 of 30middle test 0.0% 12 of 30middle test −0.4% 0 0
Rising and falling wedges only Rising wedge, a sell signal: 2 swing highs and 2 swing lows in a row, the second of each higher than the first by more than 15% of the height. The lows rise faster than the highs, so the two lines get closer and would cross later. Sell when a candle closes below the bottom line. Stop-loss: the last swing high. Height: the gap between the lines at the first swing point. Falling wedge, a buy signal: the mirror image. 15 of 30middle test 0.0% 7 of 30middle test −3.0% 2 0
Cup and handle only Cup and handle, a buy signal: two swing highs at about the same price (the rims), at most 60 candles apart, with every swing high between them lower than both. The cup must be rounded, not a V: more than half of the candles between the rims close in the cup's lower half. Then a pullback (the handle) gives back at most half of the cup's depth. Buy when a candle closes above the right rim. Stop-loss: the handle's low. Height: from the right rim down to the cup's lowest swing low. Inverted cup and handle, a sell signal: the mirror image. 12 of 30middle test −0.1% 9 of 30middle test −0.1% 0 0

No. The pattern we picked on 2019–2022 was double tops and bottoms. Of its tests with at least 100 trades, only 1 made money in both periods: the Nasdaq 100 on the 1-hour chart. All 7 patterns together did so on 2 tests. Double tops and bottoms made money in 16 of 30 tests in 2019–2022, the years we used to pick. In 2023–2025, the years we kept for the check, they made money in 14 of 30, and their middle test lost 0.2%. No pattern traded alone passed all four checks on any test, and all 7 together passed on 1. Counting every test, even those with fewer than 100 trades, no pattern made money in both periods in more than 7 of its 30 tests (the ascending and descending triangles, and the symmetrical triangle). Added up over all 30 tests, the symmetrical triangle's trades made money in both periods: +0.148R per trade in 2019–2022 and +0.095R in 2023–2025. But none of its tests reached 100 trades, so it shows 0 in 'Made money in both periods'. Wedges did worst in 2023–2025: 7 of 30 tests made money, and their average trade lost 0.088R.

Questions

Questions traders ask

Do chart patterns work?

No, not with the usual breakout rules, in our backtest. We traded 7 well-known chart patterns on 10 markets (5 currency pairs, gold and 4 stock indices), from 2019 to 2025, after trading costs. Each market was tested on 1-hour, 4-hour and daily charts. Each trade risked 1% of the account, an amount we call 1R. Traded together, the patterns lost 0.009R per trade over 7,503 trades. On daily charts they made +0.087R per trade, but over only 254 trades. Traded alone, the symmetrical triangle made +0.125R per trade and the wedges lost 0.040R. The sell forms of all 7 patterns, like the double top and the head and shoulders, lost money, in years when most of these markets rose. Waiting for price to come back to the breakout line made +0.039R per trade, or +0.021R without the trades where price only touched the order.

What is the best chart pattern?

No pattern was clearly best in our backtest of 7 chart patterns on 10 markets, from 2019 to 2025, after trading costs. Each trade risked 1% of the account, an amount we call 1R. The symmetrical triangle made the most per trade, +0.125R over 542 trades, but none of its tests had the 100 trades we need to judge a test. Most of that came from upside breakouts: +0.272R per trade, compared with −0.023R for downside breakouts. Double tops and bottoms won most often, 59.0% of their trades, but lost 0.005R per trade, because their take-profit was usually closer than their stop-loss. The wedges did worst: −0.040R per trade over 3,271 trades.

What is the success rate of chart patterns?

In our backtest, 7 chart patterns traded together won 41.0% of their 7,503 trades, from 2019 to 2025 on 10 markets. A win is a trade that made money after trading costs. The win rate depended on how far away the take-profit was. Double tops and bottoms won most often, 59.0%: for half of their trades, the take-profit was less than 0.78 times the distance to the stop-loss. Flags won least often, 23.4%: their target is the whole move before the flag, for half of the trades more than 3.47 times the distance to the stop-loss. A higher win rate did not mean more money: the flags made a small profit and the double tops and bottoms a small loss.

Should you wait for a retest after a breakout?

Yes, a little, in our backtest of 7 chart patterns on 10 markets, from 2019 to 2025. After a candle closed beyond the pattern's breakout line, we placed a limit order (an order that waits at a set price) at that line for up to 10 candles. Of the 7,965 orders, 6,375 filled. Each trade risked 1% of the account, an amount we call 1R. The patterns then made +0.039R per trade, compared with −0.009R when we entered at the next candle's open. With the retest, 19 of 30 tests made money, compared with 14. But a backtest fills a limit order as soon as price touches it. Without the 630 trades where price went less than one spread past the order, the retest made +0.021R per trade. Waiting for a second close beyond the line did not help much: the average trade still lost 0.003R, and only 10 of 30 tests made money.

The strategy code we ran

This is the exact code from the BacktestingMax strategy engine. Paste it into the strategy editor in the app to run it on any market.

class ChartPatterns(Strategy):
    """The common chart patterns, found from swing points, each traded with the same entry and exit.
    Swing points: a swing high is a candle whose high is higher than the highs of the 3 candles
    before it and the 3 candles after it (a tie doesn't count), the rule of the engine's pivot
    helper. A swing low is the mirror. A swing is known 3 candles after it, when those candles have
    closed. Highs and lows take turns: if two swing highs come without a swing low between them,
    only the higher one counts (two lows: only the lower one).
    'About the same price' and 'flat' mean that two swings differ by at most 15% of the pattern's
    height (tolerance = 0.15); 'rising' and 'falling' mean by more. The patterns, bearish form
    first (the bullish form is the mirror image):
      1 double top           the newest swing high and an earlier one at about the same price, at
                             most 60 candles apart, with every swing high between them lower than
                             both. Sell below the lowest swing low between them. Height: the higher
                             top to that low. Double bottom: the mirror.
      2 head and shoulders   5 swings in a row: left shoulder, low, head, low, right shoulder. The
                             head is the highest, the shoulders at about the same price and above
                             the neckline, the line through the two lows. Sell below the neckline.
                             Height: the head to the neckline. Inverse: the mirror.
      3 descending triangle  2 highs and 2 lows in a row: falling highs, flat lows. Sell below the
                             lower line. Ascending triangle: the mirror (buy above the upper line).
      4 symmetrical triangle falling highs and rising lows. Buy above the upper line or sell below
                             the lower line, whichever comes first.
      5 bear flag            4 swings in a row: high, low (the fall between them is the pole), a
                             bounce of at most half the pole, then a higher low. Sell below the
                             line through the two lows. Height: the pole. Bull flag: the mirror.
      6 rising wedge         2 highs and 2 lows in a row: rising highs and rising lows, the lows
                             rising faster, so the lines meet. Sell below the lower line. Falling
                             wedge: the mirror.
      7 cup and handle       the cup: two swing highs at about the same price (the rims), at most
                             60 candles apart, every swing high between them lower than both. It
                             must be rounded: more than half of the candles between the rims close
                             in the cup's lower half. The handle: the next swing low, at most half
                             the cup's depth below the right rim. Buy above the right rim. Height:
                             the right rim to the cup's lowest swing low. Inverted: the mirror.
    A triangle's or wedge's lines go through its two highs and its two lows. They must meet after
    its last swing, and its height is the gap between them at its first swing, where it is widest.
    A flag's 4 swings are not also counted as a triangle or wedge.
    A pattern is checked when its last swing is known. It stays live until the next swing point is
    known. In between, the first candle that closes beyond the breakout line is the breakout: trade
    at the next candle's open. A pattern is dropped if a candle closed beyond its breakout line
    before that (from the line's first swing on), if a candle closes beyond its other side first
    (the stop-loss level after the last swing, or a triangle's or wedge's other line), or if a
    triangle's or wedge's lines meet first.
    Exit: the stop-loss goes at the last swing point on the other side of the pattern (the second
    top, the right shoulder, the handle's low, the flag's high...). The take-profit is the measured
    move: the pattern's height added to the breakout line's price on the breakout candle. Size
    every trade to lose 1% of the account at the stop-loss. One trade at a time; patterns that break
    out during a trade are skipped.
    One shape counts as one pattern: on each new swing point the patterns are checked in the order
    head and shoulders, cup and handle, flag, triangles (ascending and descending, then
    symmetrical), wedges, double top and bottom, and a pattern is skipped if one found before it
    already trades the same way. (A shape can be a double top and a bull flag at once: they trade in
    opposite directions, so both stay.) Two breakouts the opposite way on one candle: no trade.
    Skipped candles: a candle that opens on a Friday at 17:00 New York time or later (a few minutes
    of index prices after the weekly close make a tiny extra candle in some years), and a candle
    whose high equals its low (a market holiday in the price data). The strategy ignores them: they
    are never swing points or breakout candles. On a market that trades at the weekend, remove the
    Friday skip.
    pattern = 0 trades all 7 together; 1..7 trades one of them (numbers above).
    Changes, one at a time (off by default):
      two_closes = 1   wait for a second candle to close beyond the breakout line, and trade at the
                       open after it. If the second candle closes back inside, no trade.
      retest     = 1   after the breakout close, wait for price to come back to the breakout line:
                       a limit order at the line's price on the breakout candle. It waits up to
                       retest_bars candles, and is cancelled if price reaches the take-profit first.
    """
    params = {"pattern": Param(type=int, default=0, min=0, max=7),
              "swing_bars": Param(type=int, default=3, min=1, max=20),
              "tolerance": Param(type=float, default=0.15, min=0.0, max=1.0),
              "two_closes": Param(type=int, default=0, min=0, max=1),
              "retest": Param(type=int, default=0, min=0, max=1),
              "retest_bars": Param(type=int, default=10, min=1, max=100),
              "max_width": Param(type=int, default=60, min=10, max=500)}
    lookback = 2
    # name: (tag of the bearish form, tag of the bullish form, pattern number)
    NAMES = {"hs": ("head_shoulders", "inverse_head_shoulders", 2),
             "cup": ("inverted_cup_handle", "cup_handle", 7),
             "flag": ("bear_flag", "bull_flag", 5),
             "triangle": ("descending_triangle", "ascending_triangle", 3),
             "symmetrical": ("symmetrical_triangle_down", "symmetrical_triangle_up", 4),
             "wedge": ("rising_wedge", "falling_wedge", 6),
             "double": ("double_top", "double_bottom", 1)}
    ORDER = ("hs", "cup", "flag", "triangle", "symmetrical", "wedge", "double")
    def init(self, ctx):
        self.ny = ctx.clock("America/New_York")
        self.k = -1                 # number of the newest candle the strategy uses
        self.hi = []                # highs, lows and closes of those candles
        self.lo = []
        self.cl = []
        self.swings = []            # [kind, price, k]: kind 1 = swing high, -1 = swing low
        self.setups = []            # live patterns waiting for their breakout
        self.group = 0
        self.waiting = None         # two_closes: a breakout waiting for its second close
        self.order = None           # retest: the resting limit order
        self.order_k = None
        self.order_side = None
        self.order_target = None
        self.max_width = ctx.params.max_width
    # ---- candles, swings and breakouts (no orders here) ----------------------------------------
    def step(self, high, low, close, tol=0.15, n=3):
        """Add one candle. Returns the breakouts on its close: [(side, stop, target, level, tag, line)]."""
        self.k += 1
        k = self.k
        self.hi.append(high)
        self.lo.append(low)
        self.cl.append(close)
        out = []
        done = set()
        for su in self.setups:  # breakouts first: a symmetrical triangle's two sides share a group
            if su["group"] in done or (su["apex"] is not None and k >= su["apex"]):
                continue
            level = self.line_at(su["line"], k)
            if (su["side"] == 1 and close > level) or (su["side"] == -1 and close < level):
                out.append((su["side"], su["stop"], level + su["side"] * su["height"], level, su["tag"],
                            su["line"]))
                done.add(su["group"])
        live = [su for su in self.setups if su["group"] not in done
                and not (su["apex"] is not None and k >= su["apex"])
                and not self.beyond_other(su, k, close)]
        self.setups = live
        c = k - n
        if c - n >= 0:
            win = range(c - n, k + 1)
            top = all(self.hi[c] > self.hi[j] for j in win if j != c)
            bottom = all(self.lo[c] < self.lo[j] for j in win if j != c)
            changed = False
            if top and bottom:  # an outside candle: keep the side that takes turns with the last swing
                last = self.swings[-1][0] if self.swings else -1
                changed = self.add_swing(-last, self.hi[c] if last == -1 else self.lo[c], c)
            elif top:
                changed = self.add_swing(1, self.hi[c], c)
            elif bottom:
                changed = self.add_swing(-1, self.lo[c], c)
            if changed:
                self.setups = self.find_patterns(k, tol)
        return out
    def add_swing(self, kind, price, k):
        s = self.swings
        if s and s[-1][0] == kind:
            if (kind == 1 and price > s[-1][1]) or (kind == -1 and price < s[-1][1]):
                s[-1] = [kind, price, k]
                return True
            return False
        s.append([kind, price, k])
        if len(s) > 40:
            del s[0]
        return True
    @staticmethod
    def line_at(line, k):
        k0, p0, slope = line
        return p0 + slope * (k - k0)
    def beyond_other(self, su, k, close):
        """True when a candle at k closed beyond the pattern's other side."""
        if su["other"] is not None:
            other = self.line_at(su["other"], k)
            return close > other if su["side"] == -1 else close < other
        if su["cancel_at_stop"]:
            return close > su["stop"] if su["side"] == -1 else close < su["stop"]
        return False
    # ---- patterns --------------------------------------------------------------------------
    def find_patterns(self, k, tol):
        """The patterns that end at the newest swing, in ORDER, at most one per direction."""
        found = []
        claimed = set()
        mirror = [[-t, -p, kk] for t, p, kk in self.swings]
        detect = {"hs": self.pat_hs, "cup": self.pat_cup, "flag": self.pat_flag,
                  "triangle": self.pat_triangle, "symmetrical": self.pat_symmetrical,
                  "wedge": self.pat_wedge, "double": self.pat_double}
        for name in self.ORDER:
            cands = []
            for mirrored in (False, True):
                su = detect[name](mirror if mirrored else self.swings, tol, -1 if mirrored else 1)
                if su is None:
                    continue
                if mirrored:
                    su = self.unmirror(su)
                su["tag"] = self.NAMES[name][1 if su["side"] == 1 else 0]
                su["number"] = self.NAMES[name][2]
                cands.append(su)
            if not cands:
                continue
            if name == "symmetrical":  # one pattern with two sides: both must be free and inside
                if claimed & {1, -1} or not all(self.still_inside(su, k) for su in cands):
                    continue
                self.group += 1
                for su in cands:
                    su["group"] = self.group
                    found.append(su)
                claimed |= {1, -1}
                continue
            for su in cands:
                if su["side"] in claimed or not self.still_inside(su, k):
                    continue
                self.group += 1
                su["group"] = self.group
                found.append(su)
                claimed.add(su["side"])
        return found
    @staticmethod
    def unmirror(su):
        out = dict(su)
        out["side"] = -su["side"]
        out["line"] = (su["line"][0], -su["line"][1], -su["line"][2])
        if su["other"] is not None:
            out["other"] = (su["other"][0], -su["other"][1], -su["other"][2])
        out["stop"] = -su["stop"]
        return out
    def still_inside(self, su, k):
        """No close beyond the breakout line from its first swing on, and none beyond the other side
        (a triangle's or wedge's other line: from its first swing on; otherwise the stop-loss level,
        after the last swing). The lines of a triangle or wedge must not have met."""
        for j in range(su["line"][0], k + 1):
            level = self.line_at(su["line"], j)
            if (su["side"] == 1 and self.cl[j] > level) or (su["side"] == -1 and self.cl[j] < level):
                return False
        start = su["other"][0] if su["other"] is not None else su["last_k"] + 1
        for j in range(start, k + 1):
            if self.beyond_other(su, j, self.cl[j]):
                return False
        return su["apex"] is None or k < su["apex"]
    @staticmethod
    def setup(side, line, stop, height, last_k, other=None, apex=None, cancel_at_stop=True):
        return {"side": side, "line": line, "other": other, "stop": stop, "height": height,
                "last_k": last_k, "apex": apex, "cancel_at_stop": cancel_at_stop}
    # Each pat_ method reads the swings for the BEARISH form (find_patterns mirrors the prices for
    # the bullish form) and returns a sell setup, or None.
    def earlier_match(self, s, end, tol):
        """For the swing high s[end]: the most recent earlier swing high at about the same price with
        every swing high between them lower than both, at most max_width candles before it.
        Returns (index of that high, lowest swing low between them, its k) or None."""
        _, h2, k2 = s[end]
        inner = None            # highest swing high between the two
        low, k_low = None, None  # lowest swing low between the two
        for i in range(end - 1, -1, -1):
            t, p, kk = s[i]
            if k2 - kk > self.max_width:
                return None
            if t == -1:
                if low is None or p < low:
                    low, k_low = p, kk
                continue
            if inner is not None and inner >= h2:
                return None
            if low is not None and (inner is None or p > inner):
                height = max(p, h2) - low
                if height > 0 and abs(p - h2) <= tol * height:
                    return i, low, k_low
            inner = p if inner is None else max(inner, p)
        return None
    def pat_double(self, s, tol, sign):
        """Double top: the newest swing high and an earlier one at about the same price, every swing
        high between them lower than both. Sell below the lowest swing low between them."""
        if len(s) < 3 or s[-1][0] != 1:
            return None
        m = self.earlier_match(s, len(s) - 1, tol)
        if m is None:
            return None
        i, low, k_low = m
        h1, (_, h2, k2) = s[i][1], s[-1]
        return self.setup(-1, (k_low, low, 0.0), h2, max(h1, h2) - low, k2)
    def pat_hs(self, s, tol, sign):
        if len(s) < 5 or s[-1][0] != 1:
            return None
        (_, h1, k1), (_, l1, k2), (_, h2, k3), (_, l2, k4), (_, h3, k5) = s[-5:]
        if not (h2 > h1 and h2 > h3):
            return None
        neck = (k2, l1, (l2 - l1) / (k4 - k2))
        height = h2 - self.line_at(neck, k3)
        if height <= 0 or abs(h1 - h3) > tol * height:
            return None
        if h1 <= self.line_at(neck, k1) or h3 <= self.line_at(neck, k5):
            return None
        return self.setup(-1, neck, h3, height, k5)
    def pat_cup(self, s, tol, sign):
        """Inverted cup and handle: the newest swing is a high (the handle's top), the one before it a
        low (the right rim). The left rim is an earlier swing low at about the same price, with every
        swing low between them higher than both (the mirror of a double top's two tops). The cup is
        rounded: more than half of the candles between the rims close in its half nearest its
        extreme. The handle goes back at most half the cup's depth. Sell below the right rim."""
        if len(s) < 4 or s[-1][0] != 1 or s[-2][0] != -1:
            return None
        flip = [[-t, -p, kk] for t, p, kk in s[:-1]]  # rims as highs: reuse the double-top search
        m = self.earlier_match(flip, len(flip) - 1, tol)
        if m is None:
            return None
        i, top, _ = m
        top = -top                       # the cup's extreme (its highest swing high here)
        ka, (_, c, kc), (_, d, kd) = s[i][2], s[-2], s[-1]
        depth = top - c
        if depth <= 0 or d - c > 0.5 * depth:
            return None  # the handle goes back more than half the cup's depth
        mid = (top + c) / 2
        inside = range(ka + 1, kc)
        near = sum(1 for j in inside if sign * self.cl[j] > mid)
        if not near * 2 > len(inside):
            return None  # not rounded: price did not spend most of the cup near its extreme
        return self.setup(-1, (kc, c, 0.0), d, depth, kd)
    def pat_flag(self, s, tol, sign=1):
        """Bear flag: high (the pole's start), low (its end), a bounce of at most half the pole,
        then a higher low."""
        if len(s) < 4 or s[-1][0] != -1:
            return None
        (_, h0, _), (_, l0, k1), (_, h1, _), (_, l1, k3) = s[-4:]
        pole = h0 - l0
        if pole <= 0 or h1 - l0 > 0.5 * pole or not l1 > l0:
            return None
        return self.setup(-1, (k1, l0, (l1 - l0) / (k3 - k1)), h1, pole, k3)
    def four(self, s, tol):
        """The last 4 swings as two lines: (upper, lower, height at the first swing, highs' move,
        lows' move, apex, last high, last low), moves 1 rising, -1 falling, 0 flat. None unless
        the lines meet after the last swing, or if the 4 swings are a flag."""
        if len(s) < 4:
            return None
        sw = s[-4:]
        mir = [[-t, -p, kk] for t, p, kk in sw]
        if any(self.pat_flag(x, tol) is not None for x in (sw, mir)):
            return None  # a flag is not counted as a triangle or wedge
        (ka, ha), (kb, hb) = [(kk, p) for t, p, kk in sw if t == 1]
        (kc, la), (kd, lb) = [(kk, p) for t, p, kk in sw if t == -1]
        up = (ka, ha, (hb - ha) / (kb - ka))
        low = (kc, la, (lb - la) / (kd - kc))
        k_first = sw[0][2]
        height = self.line_at(up, k_first) - self.line_at(low, k_first)
        if height <= 0 or not up[2] < low[2]:
            return None
        apex = k_first + height / (low[2] - up[2])
        if apex <= sw[-1][2]:
            return None
        lim = tol * height
        hm = 1 if hb - ha > lim else -1 if ha - hb > lim else 0
        lm = 1 if lb - la > lim else -1 if la - lb > lim else 0
        return up, low, height, hm, lm, apex, hb, lb
    def pat_triangle(self, s, tol, sign):
        """Descending triangle: falling highs, flat lows. Sell below the lower line."""
        f = self.four(s, tol)
        if f is None or not (f[3] == -1 and f[4] == 0):
            return None
        up, low, height, _, _, apex, hb, _ = f
        return self.setup(-1, low, hb, height, s[-1][2], other=up, apex=apex, cancel_at_stop=False)
    def pat_symmetrical(self, s, tol, sign):
        """Symmetrical triangle, its sell side: falling highs, rising lows. Sell below the lower line
        (find_patterns adds the buy side above the upper line from the mirror image)."""
        f = self.four(s, tol)
        if f is None or not (f[3] == -1 and f[4] == 1):
            return None
        up, low, height, _, _, apex, hb, _ = f
        return self.setup(-1, low, hb, height, s[-1][2], other=up, apex=apex, cancel_at_stop=False)
    def pat_wedge(self, s, tol, sign):
        """Rising wedge: rising highs and rising lows, the lines meeting ahead. Sell below the lower
        line."""
        f = self.four(s, tol)
        if f is None or not (f[3] == 1 and f[4] == 1):
            return None
        up, low, height, _, _, apex, hb, _ = f
        return self.setup(-1, low, hb, height, s[-1][2], other=up, apex=apex, cancel_at_stop=False)
    # ---- trading ---------------------------------------------------------------------------
    def on_bar(self, ctx):
        # A tiny candle after the Friday close, or a candle with no price movement: ignore it.
        if self.ny.weekday == 4 and self.ny.hour >= 17:
            return
        high, low, close = ctx.high[0], ctx.low[0], ctx.close[0]
        if not high > low:
            return
        p = ctx.params
        breakouts = self.step(high, low, close, p.tolerance, p.swing_bars)
        if p.pattern != 0:
            breakouts = [b for b in breakouts if self.number_of(b[4]) == p.pattern]
        if self.order is not None:  # retest: a limit order is waiting
            if self.order.state.value != "pending" or ctx.position is not None:
                self.order = None
            else:
                reached = high >= self.order_target if self.order_side == 1 else low <= self.order_target
                if reached or self.k - self.order_k >= p.retest_bars:
                    ctx.cancel_order(self.order.order_id)
                    self.order = None
                return
        if ctx.position is not None:
            self.waiting = None
            return
        waiting, self.waiting = self.waiting, None
        if waiting is not None:  # two_closes: the candle after the first close beyond the line
            side, stop, target, level, tag, line = waiting
            now = self.line_at(line, self.k)
            if ctx.gate("second_close_beyond_line", close > now if side == 1 else close < now):
                self.enter(ctx, side, stop, target, level, tag)
            return
        if not ctx.gate("pattern_breakout", len(breakouts) > 0):
            return
        if not ctx.gate("one_direction", all(b[0] == breakouts[0][0] for b in breakouts)):
            return
        if p.two_closes == 1:
            self.waiting = breakouts[0]
            return
        side, stop, target, level, tag, _ = breakouts[0]
        self.enter(ctx, side, stop, target, level, tag)
    def number_of(self, tag):
        for bear, bull, number in self.NAMES.values():
            if tag in (bear, bull):
                return number
        return None
    def enter(self, ctx, side, stop, target, level, tag):
        entry = level if ctx.params.retest == 1 else ctx.close[0]
        # The stop-loss must be behind the entry and the take-profit ahead of it.
        ok = stop < entry < target if side == 1 else target < entry < stop
        if not ctx.gate("room_to_target", ok):
            return
        size = ctx.size_by_risk_pct(0.01, stop=stop, entry=entry)
        if ctx.params.retest == 1:
            if side == 1:
                self.order = ctx.buy(size=size, limit=entry, sl=stop, tp=target, tag=tag)
            else:
                self.order = ctx.sell(size=size, limit=entry, sl=stop, tp=target, tag=tag)
            if self.order is not None:
                self.order_k, self.order_side, self.order_target = self.k, side, target
            return
        if side == 1:
            ctx.buy(size=size, sl=stop, tp=target, tag=tag)
        else:
            ctx.sell(size=size, sl=stop, tp=target, tag=tag)

Your turn

Test the chart pattern strategy yourself

Change the settings, the market or the stop-loss and see the result in seconds. Ask the BacktestingMax AI: Backtest double tops and double bottoms on EUR/USD, 4-hour chart, from 2019 to 2025: swing points with 3 candles on each side, enter with a limit order at the neckline after a close beyond it (wait up to 10 candles), stop-loss at the second top or bottom, take profit at the pattern's height from the neckline, risk 1% per trade.

Try it free in BacktestingMax

Free account. No credit card needed.

See every strategy we tested Browse strategies shared by traders

Backtest on historical data with trading costs, run by BacktestingMax. Past results do not guarantee future results, and nothing here is financial advice. Updated 26 October 2026.