§ 1Where This Came From
Every trading book has the same gallery: the hammer, the engulfing bar, the morning star, the three white soldiers. Someone named them by staring at charts, and they have been repeated ever since. Nobody ever proved they were the best shapes a few candles can make. They were just the ones people noticed.
neurotrader's video Data Mining Candlestick Patterns With a Genetic Algorithm flips the question. Instead of asking whether the hammer works, write down a grammar that can describe any candlestick pattern, and let a genetic algorithm breed the ones that make money. I liked the idea enough to rebuild it from scratch and push it as far as I could.
The data is Binance BTCUSDT hourly candles from January 2022 to June 2026, 39,408 candles in total. The patterns were bred on 2022 to 2025 (35,064 candles), and the first half of 2026 (4,344 candles) was locked away until the very end.
This post comes in two halves. Part one is the idea, and it is a genuinely good one. Part two is what happened when I tried to trade it.
Part one · The idea
§ 2A Pattern Is a Handful of Comparisons
Each candle carries four numbers: open, high, low and close. Write them as O, H,
L and C, with the number in brackets saying how many candles back you look.
C[0] is the current close, L[2] is the low two candles ago.
A rule compares two of those numbers. L[2] > C[0] reads: the current close
has fallen below the low of two candles ago. A pattern is a few rules joined with
AND, and it fires on any candle where all of them hold at once.
Each rule compares series X lagged a candles with series Y lagged b candles. The pattern fires (s = 1) only when all K rules are true.
Two settings shape the search: how many rules a pattern has, and how many candles it can look back. Some comparisons are thrown out because they can never be false, like a candle's high against its own low. I also dropped open against the previous close: in a market that trades around the clock the two are almost always the same price, so the comparison is a coin flip on a single tick.
What is left is still a large space. Three candles give 98 usable rules, which already allows 152,096 three-rule patterns. Four candles and four rules push it past 57 million. That is far too many to test one by one, and it is exactly the kind of space a genetic algorithm is built for.
§ 3Let Evolution Do the Search
A genetic algorithm keeps a population of candidate patterns and improves it the way breeders improve livestock: the fittest get to reproduce, their offspring mix their traits, and an occasional mutation keeps the gene pool from going stale.
Selection uses a roulette wheel: every pattern gets a slice proportional to its fitness, so strong patterns breed often and a pattern with zero fitness never breeds at all. Crossover cuts both parents at the same random point between rules and swaps the tails, so good rules spread through the population.
Mutation changes one detail of one rule, a series or a lag, in about 5% of the children, and replaces about 2% with brand-new random patterns. Keep those rates low: crank them up and the algorithm decays into a random search.
§ 4Telling the Algorithm What Good Means
Fitness is measured by trading the pattern. On every candle where it fires you take the next hour's return, everywhere else you take nothing. That produces a return stream, and the fitness function turns it into one number.
The choice of fitness changes which patterns win. Total return favours patterns that fire all the time. Profit factor favours rare patterns with a big win to loss ratio and is easy to overfit. The video's favourite, and mine, is the Martin ratio: total return divided by the ulcer index, which punishes drawdowns for being both deep and long.
D is the percentage drawdown from the running peak of the equity curve. A pattern must also fire on at least 2.5% of candles, otherwise its fitness is zero.
One pattern is fragile, so the run is repeated to find five. After the first, each new candidate's fitness is scaled down by how closely its returns track the patterns already found. An identical copy scores zero, which forces the algorithm to find something new.
§ 5Why This Beats a Textbook Indicator
It searches the logic, not just the parameters. An RSI or a MACD is a fixed formula. You can change 14 to 21, but you cannot change what it measures. The genetic algorithm searches over the structure of the signal itself, across millions of candidates.
It starts with no folklore. The classic patterns were named by eye, long before anyone traded hourly crypto. The algorithm has no favourites. If a hammer is useful here, it will find something like it. If it isn't, it won't care.
You choose the objective. Instead of eyeballing a lagging oscillator, you state what you care about, here drawdown-adjusted return, and the search optimises exactly that.
Every answer is readable. A neural network gives you weights. This gives you four inequalities you could check by hand on a chart.
Diversification is built in, and it travels. The decorrelation step hands you a set of patterns that fire at different times, and nothing in the method is specific to Bitcoin, to hourly bars, or to going long.
The pitch in one line
Stop arguing about which patterns matter. Describe every pattern you could draw, and let the data breed the winners.
§ 6The Results Look Incredible
I ran it the way the video does: three candles, three rules per pattern, the Martin ratio, a one-hour hold, and five long plus five short patterns, bred on 2022 to 2025 with a population of 200 over 80 generations. Every one of the ten patterns made money in sample, between +69% and +404%.
| Side | Pattern | Trades | In-sample return |
|---|---|---|---|
| Long | H[0] > L[2] & L[2] > L[1] & C[2] > C[1] | 6,151 | +404% |
| Long | H[2] > C[0] & L[0] > C[2] & L[2] > L[1] | 1,066 | +97% |
| Long | O[2] > C[0] & H[2] > O[0] & L[0] > O[1] | 1,189 | +83% |
| Long | H[0] > C[2] & L[1] > L[2] & L[2] > C[0] | 1,134 | +69% |
| Long | O[0] > C[0] & H[0] > H[1] & L[0] > C[2] | 1,808 | +116% |
| Short | O[0] > O[1] & O[1] > L[0] & C[0] > L[1] | 5,076 | +242% |
| Short | O[2] > L[0] & L[1] > L[0] & C[0] > C[1] | 4,009 | +137% |
| Short | L[1] > L[2] & L[2] > L[0] & C[0] > L[2] | 2,094 | +105% |
| Short | O[0] > O[1] & C[0] > H[1] & C[1] > L[0] | 3,719 | +146% |
| Short | O[2] > L[0] & L[1] > L[2] & C[0] > O[2] | 3,574 | +284% |
Combine them, going long when a long pattern fires and short when a short one does, and the four-year result is +51,832% with an annualised Sharpe of 3.70, over 7,464 trades. Buying and holding Bitcoin over the same four years made +88%.
If you stopped reading here you would be checking how much leverage your exchange allows. Every instinct I have says that a curve this smooth is lying to me, so the rest of this post is about finding out how.
Part two · The bill
§ 7Every Trade Costs Money
Everything above assumes trading is free. It isn't. I charge 5 bps of fee plus 1 bp of slippage on each side, so a round trip costs 12 bps. That is a cheap assumption: Binance's standard spot taker fee is higher. The video mentions that Binance had removed fees on its BTC pairs at the time, but that is not the standard schedule, and slippage never goes away.
The break-even condition. A pattern has to earn more than both legs of the trade cost, on average, every time it trades.
Now look at what each pattern actually earns per trade before costs.
The patterns are right more often than they are wrong, but the moves they catch are tiny: a few basis points on an hourly candle. The edge is real in the sense that it shows up in the data. It is just smaller than the toll for collecting it, and the strategy pays that toll thousands of times.
| Side | Pattern | Edge / trade | After costs |
|---|---|---|---|
| Long | H[0] > L[2] & L[2] > L[1] & C[2] > C[1] | 2.6 bps | −99.7% |
| Long | H[2] > C[0] & L[0] > C[2] & L[2] > L[1] | 6.4 bps | −45.1% |
| Long | O[2] > C[0] & H[2] > O[0] & L[0] > O[1] | 5.1 bps | −56.1% |
| Long | H[0] > C[2] & L[1] > L[2] & L[2] > C[0] | 4.6 bps | −56.6% |
| Long | O[0] > C[0] & H[0] > H[1] & L[0] > C[2] | 4.3 bps | −75.3% |
| Short | O[0] > O[1] & O[1] > L[0] & C[0] > L[1] | 2.4 bps | −99.2% |
| Short | O[2] > L[0] & L[1] > L[0] & C[0] > C[1] | 2.2 bps | −98.1% |
| Short | L[1] > L[2] & L[2] > L[0] & C[0] > L[2] | 3.4 bps | −83.4% |
| Short | O[0] > O[1] & C[0] > H[1] & C[1] > L[0] | 2.4 bps | −97.2% |
| Short | O[2] > L[0] & L[1] > L[2] & C[0] > O[2] | 3.8 bps | −94.7% |
§ 8Six Months It Never Saw
Costs alone are fatal, but the second question matters just as much: do the patterns hold up on data they were never fitted to? January to June 2026 was a rough stretch for Bitcoin, which fell 33% over those six months.
The video's patterns kept their shape for a while: before costs they made +27.3% out of sample. But their edge shrank to 2.5 bps per trade, and across 983 trades the costs took the result to −86.3%.
§ 9Fixing What Can Be Fixed
I did not want to stop at "fees kill it", so I rebuilt the search to attack each weakness directly:
Hold longer. Patterns now keep the position for 1, 6, 24 or 48 hours after firing, so the cost is paid once per bigger move. Charge costs inside the fitness, so the algorithm breeds against the real bill. Score timing, not trend: returns are demeaned, so a long pattern earns nothing for simply riding a bull market. Demand consistency: fitness is multiplied by the share of profitable quarters, and a pattern has to work on BTC and ETH at the same time. Cap exposure: no single pattern may be in the market more than half the time.
The pattern shape was then chosen by cross-validation inside the training years (fit 2022, test 2023; fit 2022 to 2023, test 2024; fit 2022 to 2024, test 2025) across 80 shapes per side, and the final model averages five independent runs.
The search picked 3 candles with 4 rules and a 24-hour hold for longs, and 3 candles with 2 rules and a 24-hour hold for shorts. Out of sample it lost −11.7% after costs (Sharpe −0.92), and −12.1% with a retrain on 1 April. That is far better than the video's −86.3%, and better than buy and hold. It is still a loss.
§ 10Could Chance Do This?
A genetic algorithm will find patterns in noise. That is what it is for. The honest test is to give it noise and see if it does as well. I ran two permutation tests, where candles are shuffled so their shapes survive but their order, and any real pattern, is destroyed.
In sample, the whole algorithm was re-run on 100 shuffled copies of 2022 to 2025. It found patterns as good as the real ones 71% of the time for longs (p = 0.71) and 15% of the time for shorts (p = 0.15). Out of sample, the frozen model traded 1,000 shuffled versions of 2026.
The model selection step fails its checks too. The probability of backtest overfitting is 0.35 for longs and 0.30 for shorts, and the deflated Sharpe ratio, which asks whether the winner beats the best of 80 lucky guesses, is zero on both sides.
§ 11What It Actually Learned
Strip away the noise and the algorithm keeps rediscovering one idea: reversal. The
rule L[2] > C[0] appears in 13 of the 25 long patterns:
buy after the close drops below a recent low. The rule C[1] > H[2] appears in
20 of the 25 short patterns: sell after the previous close breaks
above an earlier high.
Short-term mean reversion in Bitcoin is not a fantasy. Over an hour it is just small, and it lives in the same few basis points the exchange charges you to trade it. Holding for a day makes each trade big enough to pay for itself in sample: the robust long patterns averaged 76 bps per trade on 2022 to 2025. On 2026 they averaged −65 bps.
The one leg that made money out of sample deserves suspicion too. The short side returned +15.6% while BTC fell 33%, but it was short 78% of the time. On ETH the same model made +39.8% while ETH fell 47%. Shuffled candles with the same downtrend did as well (p = 0.80). That is a bet on direction, not a pattern.
| Jan to Jun 2026, after costs | Return | Sharpe | Max drawdown | Trades |
|---|---|---|---|---|
| Video method, long + short | −86.3% | −10.53 | −86% | 983 |
| Video method, long only | −71.1% | −8.78 | −71% | 1,049 |
| Video method, short only | −61.7% | −6.40 | −63% | 987 |
| Robust version, long + short | −11.7% | −0.92 | −24% | 91 |
| Robust version, long only | −23.7% | −1.43 | −32% | 30 |
| Robust version, short only | +15.6% | 0.75 | −23% | 53 |
| Robust, retrained on 1 April | −12.1% | −1.01 | −24% | 89 |
| BTC buy & hold | −33.3% | −1.75 | −40% | 1 |
§ 12What I Am Not Sure About
Whether daily candles change the picture. Costs are fixed per trade while moves grow with the timeframe, and every improvement here came from holding longer. A daily version has far less data to learn from, but it is the obvious next test.
Whether maker-only execution saves it. Resting limit orders cut the fee side of the 12 bps hurdle, but you only get filled when the market comes to you, which is often exactly when the trade is going wrong.
Whether six months is enough. 2026 H1 was one regime, a steady decline. Mean reversion may look better in a choppy sideways market, and a single half-year cannot tell me.
Whether richer rules would help. These patterns only ask whether one price is above another. Adding size (how far below the low, not just below it) or volume might find a bigger edge, or might just give the algorithm more rope to overfit with.
The idea holds up as a research tool: it finds structure, it explains itself, and it is far more honest about what it found than a hand-picked indicator. As a strategy, the patterns are real enough to find and too small to keep.
§ AAppendix: The Core of the Search (Python)
The full pipeline (data download, the search, cross-validation, permutation tests and the report) runs to a few hundred lines. These are the four functions everything else is built on.
# The core of the search, simplified from the full code. rule_matrix[i, t] is True when
# rule i (e.g. L[2] > C[0]) holds on candle t.
import numpy as np
def pattern_signal(rule_matrix, pattern):
sig = rule_matrix[pattern[0]].copy()
for i in pattern[1:]:
sig &= rule_matrix[i] # every rule must hold
return sig
def martin_fitness(signal, fwd_ret, min_trades, cost=0.0):
pos = signal.astype(float)
ret = pos * fwd_ret - cost * np.abs(np.diff(pos, prepend=0.0))
if signal.sum() < min_trades or ret.sum() <= 0:
return 0.0 # roulette needs fitness >= 0
equity = np.cumsum(ret) # log equity
peak = np.maximum.accumulate(np.maximum(equity, 0.0))
drawdown = 1.0 - np.exp(equity - peak)
ulcer = np.sqrt(np.mean(drawdown ** 2))
return ret.sum() / max(ulcer, 1e-12)
def roulette(fitness, rng):
cum = np.cumsum(fitness) # chance of being picked ~ fitness
if cum[-1] <= 0:
return int(rng.integers(len(fitness)))
return int(np.searchsorted(cum, rng.random() * cum[-1]))
def crossover(a, b, rng):
cut = int(rng.integers(1, len(a))) # random split between rules
return a[:cut] + b[cut:], b[:cut] + a[cut:]
# cost=0.0 reproduces the video. cost=0.0006 (6 bps per side) is the setting that
# turns every pattern in this post from a winner into a loser.
Credit for the method goes to neurotrader; the rebuild, the costs, the out-of-sample test and the permutation tests are mine.