A specialist could spot one chart shape by eye, on a few charts at a time. I turned the eye into a scanner set up to watch 39 stocks at once.
A stock-market specialist I worked with looks for one chart shape, the flagpole: a sharp run up, a short pause, then a break higher. The specialist spots it by eye, and an eye has two limits. One person can watch a handful of charts, and the shape forms and breaks inside a single trading day. And an eye cannot be tested: nobody can ask it how it would have judged fifteen years of charts. My job was to turn that eye into rules a machine applies the same way every time, to test them on years the tuning never touched, and to run them while the market is open.
- 1Pole A rise between the minimum and maximum percent, inside its time limit, with the closes near a straight line fitted through it.
- 2Pause holds After the top, the price drifts no further than its limits, for at least the minimum time.
- 3Complete The price breaks far enough above the highest point of the pole and the pause, with enough of the day left, and the pattern’s average volume is above its recent normal. The ticker is marked on the dashboard.
The calls I made
- The specialist decides what to look for; the data decides the numbers. I sat with the specialist and turned each part of the shape into a setting: how far and how fast the run up, how straight it climbs, how far the pause may drift and for how long, how far the break must go, how much of the day must be left, how volume compares with its recent normal. Fourteen settings in all.11Run of 18 Jun 2025, 14 settings, trained on 2010–2022: 17 of the top set's 107 scored patterns came from 2023 on; the forward test found the same 17 The optimiser only chooses their values. It cannot invent a rule, so every rule stays one the specialist can read and argue with. The obvious alternative is a model trained on charts the specialist labels. It would find its own rules, and the specialist could not read them or argue with them.
- Hold back two sets of years, not one. Fourteen settings tuned on history will fit history, so years are held back. In June I tuned on 2010 to 2022 and kept 2023 on for one forward test. It found 17 patterns in the held-back years, and all 17 had already helped choose the settings, so the test was grading what the tuning had seen.11Run of 18 Jun 2025, 14 settings, trained on 2010–2022: 17 of the top set's 107 scored patterns came from 2023 on; the forward test found the same 17 The story of that leak, and of two fixes that failed quietly, is below. Since August the years split three ways: training years to tune on, validation years to choose between candidates, and test years no code optimises against. One set of held-back years cannot both choose between candidates and give the final check.22From 30 Jul 2025 the years split three ways; from 11 Aug a genetic search scores training and validation apart; from 13 Aug it favours close scores
- Prefer settings that behave the same on years they saw and years they didn’t. Until August the search chased one score. A set that shines in training and fades on new years is what overfitting looks like, so now the optimiser scores training and validation apart and prefers candidates whose two scores are close.22From 30 Jul 2025 the years split three ways; from 11 Aug a genetic search scores training and validation apart; from 13 Aug it favours close scores I accepted the price: once validation steers the choice, it is no longer unseen. And the test years are unseen only by code. The screen used to pick a set for live showed its test score too, so the last guard is the person choosing.
- Levels, not one alarm. Live, the scanner raises each stock through three levels as the shape builds: a pole, a pause that holds, a complete pattern. Higher levels are rechecked more often, and the dashboard draws the shape on the stock’s chart, so the specialist sees why a stock was flagged, not just that it was.
What happened
On 9 September 2025 the scanner was set to watch 39 stocks during market hours, on settings from a tuning run of 28 August, after the leak was closed.339 Sep 2025: 39 tickers set for live scanning, all on settings from an optimiser run of 28 Aug. Jan 2026: a 09:35 New York auto-start, never committed The specialist’s eye, which could follow a handful of charts, was now set to run on 39 at once, the same way every time.
Under the hood
The detector takes the day’s turning points and treats every earlier-to-later pair as a candidate pole. Its first cut, verbatim from the version that ran live:
price_pct_increases = 100 * (dst_prices - src_prices) / src_prices
time_valid = time_diffs < increase_max_time
min_increase_valid = price_pct_increases >= (min_percent_increase - FP_TOLERANCE)
if max_percent_increase == float("inf"):
max_increase_valid = np.ones(len(price_pct_increases), dtype=bool)
else:
max_increase_valid = price_pct_increases <= (
max_percent_increase + FP_TOLERANCE
)
valid_mask = time_valid & min_increase_valid & max_increase_valid
Each later filter narrows the list: the closes must stay near a straight line fitted through the pole, the pause must hold, the break must come with enough of the day left, and volume must beat the average of a set number of past days times a multiplier.
The held-out years were already scored. In June 2025 the optimiser trained on 2010 to 2022 and kept January 2023 to April 2025 for a forward test. The cut-off date travelled down to the function that runs the detector, whose docstring promised “Only flagpoles starting before this date will be included”. The only code that used it was a debug print. In the run of 18 June the best set was scored on 107 patterns, and 17 were dated after 1 January 2023. The forward test then found 17 patterns in the held-out years: the same 17 dates.11Run of 18 Jun 2025, 14 settings, trained on 2010–2022: 17 of the top set's 107 scored patterns came from 2023 on; the forward test found the same 17 It reloads the patterns the optimiser stored, by design. What made it worthless is that all 17 had already helped choose the settings.
The forward test reloads those 17, all already inside the score.
Two fixes then failed quietly. On 2 July the score got a real date filter. It expected a timestamp column, and no code in the repository wrote one:
if "Datetime" not in df.columns:
logger.warning("No Datetime column, cannot filter by date")
return df
On 13 July the filter was rewritten to read the date out of the pattern’s name, and it still kept every row: it looked for the name in the table’s index, but the loader read the file back with the name as an ordinary column. The filter first cut on 31 July, when the statistics files moved to a format that keeps the index.44Date filter added 2 Jul 2025 looked for a column no code wrote and kept every row; a 13 Jul rewrite kept every row too. It first cut on 31 Jul 2025 For about four weeks the code had a filter and the scores had a leak, and the only trace was a warning in a log.
Since 30 July each pattern is also stamped training, validation or test when it is saved.55Since 30 Jul 2025 a pattern saved with both boundary dates set carries its period: training, validation or test The stamp needs both boundary dates to reach the detector; without them every pattern from an optimiser trial is stamped training. What it changes is that a mistake is visible: a 2024 pattern marked training is caught by opening one file.
The search. Until 11 August it was Bayesian optimisation chasing one score. On 11 August it got two scores, training and validation, and became NSGA-II, a genetic algorithm that keeps the set of trade-offs where neither score can rise without the other falling. From 13 August the pick from that set gives closeness of the two scores 40% of the weight.22From 30 Jul 2025 the years split three ways; from 11 Aug a genetic search scores training and validation apart; from 13 Aug it favours close scores
Live. During market hours per-second price and volume arrive over one WebSocket for every stock in the live set.339 Sep 2025: 39 tickers set for live scanning, all on settings from an optimiser run of 28 Aug. Jan 2026: a 09:35 New York auto-start, never committed
- In market hours
Per-second bars for every ticker in the live set arrive over one WebSocket.
- Every minute
Each ticker’s seconds are rolled into a minute candle.
- Own process
The scanner runs the detector in a separate process, so a slow scan never stalls the stream.
- Three levels
Level 1 is a pole, level 2 a pause that holds, level 3 a complete pattern; higher levels are rechecked more often.
- On screen
The dashboard marks the ticker’s level and draws the pole, the pause and the break on its chart.
In January 2026 I made streaming start itself at 09:35 New York time, five minutes after the open. That code was never committed.339 Sep 2025: 39 tickers set for live scanning, all on settings from an optimiser run of 28 Aug. Jan 2026: a 09:35 New York auto-start, never committed
What transfers
- The expert decides which rules exist; the data decides their values. Rules a domain expert can read stay rules they can argue with.
- Make the split part of the data, not an argument. A date passed through several layers can be dropped at any of them. A period written on each record can be checked by opening it.
- A filter that cannot run should stop the run. Warn-and-continue turned a leak into a log line for four weeks.
- Check that your test can disagree. If the test reads what the optimiser stored, count the overlap before trusting the agreement.
Sources
5 sources behind the numbers above
- 1Run of 18 Jun 2025, 14 settings, trained on 2010–2022: 17 of the top set's 107 scored patterns came from 2023 on; the forward test found the same 17
- 2From 30 Jul 2025 the years split three ways; from 11 Aug a genetic search scores training and validation apart; from 13 Aug it favours close scores
- 39 Sep 2025: 39 tickers set for live scanning, all on settings from an optimiser run of 28 Aug. Jan 2026: a 09:35 New York auto-start, never committed
- 4Date filter added 2 Jul 2025 looked for a column no code wrote and kept every row; a 13 Jul rewrite kept every row too. It first cut on 31 Jul 2025
- 5Since 30 Jul 2025 a pattern saved with both boundary dates set carries its period: training, validation or test