Features
Patterns and event studies
Search price history for similar patterns and study what happened after events
When a price shape or a market event appeared before, what happened next? VBT searches price history for windows similar to a pattern, and turns any set of events into forward price paths you can summarize. Pattern recognition and event studies in Python take a few lines and run at compiled speed.
close = vbt.YFData.pull("BTC-USD", start="2015-01-01", end="2025-01-01").close
pattern = close.iloc[-30:]
ranges = close.vbt.find_pattern(
pattern=pattern.values,
rescale_mode="rebase",
min_similarity=0.75,
)
past = ranges.end_idx.values < len(close) - 60
similarity = np.where(past, ranges.similarity.values, -np.inf)
top10 = ranges.apply_mask(similarity >= np.sort(similarity)[-10])
top10.readable[["Start Index", "End Index", "Similarity"]].round(3) Start Index End Index Similarity
0 2015-07-20 00:00:00+00:00 2015-08-19 00:00:00+00:00 0.831
1 2016-07-07 00:00:00+00:00 2016-08-06 00:00:00+00:00 0.810
2 2017-08-18 00:00:00+00:00 2017-09-17 00:00:00+00:00 0.829
3 2017-11-08 00:00:00+00:00 2017-12-08 00:00:00+00:00 0.808
4 2019-03-04 00:00:00+00:00 2019-04-03 00:00:00+00:00 0.809
5 2022-05-18 00:00:00+00:00 2022-06-17 00:00:00+00:00 0.816
6 2022-08-28 00:00:00+00:00 2022-09-27 00:00:00+00:00 0.807
7 2022-11-29 00:00:00+00:00 2022-12-29 00:00:00+00:00 0.801
8 2023-04-21 00:00:00+00:00 2023-05-21 00:00:00+00:00 0.830
9 2024-08-11 00:00:00+00:00 2024-09-10 00:00:00+00:00 0.807after = top10.with_delta(30)
after.get_projections().iloc[-1].quantile([0.1, 0.5, 0.9]).round(3)0.1 0.989
0.5 1.071
0.9 1.413
Name: 2025-01-30 00:00:00+00:00, dtype: float64after.plot_projections(plot_past_period=90).show() Thirty days after the ten most similar windows, the median path was up 7.1%, and the 10th and 90th percentiles ranged from a 1.1% loss to a 41.3% gain. Ten past cases are a description of history, not a forecast, and the spread says as much as the median.
Pattern search
find_pattern slides a window over the series, fits the pattern to each window, and scores the fit
from 0 to 1. A pattern can be a list of points, such as [5, 1, 3, 1, 2, 1] for a descending
triangle, or a piece of real price history. Each window is stretched to the pattern's length and
scaled by rescale_mode: "minmax" compares shapes regardless of price range, and "rebase"
compares percentage moves. With max_window, the search tries every window length up to that size,
which finds the same shape at different speeds. Every extra length is another full search, so
row_select_prob and window_select_prob can sample a share of the rows and lengths instead. The
distance can be mean absolute or squared error, and max_error limits how far any single point may
deviate.
Matches come back as range records with a similarity score, which can be filtered, plotted, and
turned into signals. A record's end index is the bar after the match, and last_pd_mask marks the
bar where each match completes. The same score is also available as a rolling indicator, one value
per bar:
patsim = vbt.PATSIM.run(
close,
pattern=pattern.values,
window=30,
rescale_mode="rebase",
)
patsim.similarity.iloc[-5:].round(3)Date
2024-12-27 00:00:00+00:00 0.682
2024-12-28 00:00:00+00:00 0.682
2024-12-29 00:00:00+00:00 0.730
2024-12-30 00:00:00+00:00 0.796
2024-12-31 00:00:00+00:00 1.000
Freq: D, Name: (array, 30, rebase), dtype: float64The last value is 1 because the pattern was taken from those 30 bars.
Using matches as trading signals
By default, overlapping matches are resolved by keeping the most similar one, and a better match
found later can remove an earlier one. That looks ahead. For signals, search with
overlap_mode="allowall", or use the rolling similarity indicator. A match is complete at the
close of its last bar, so trade it on the next bar.
Forward paths after events
Projections work for any event, not only patterns. Turn a signal mask into ranges that start at each event, and VBT collects the price path after every one of them, rebased to start at 1. Here are all opening gaps of 1% or more in SPY since 2010:
data = vbt.YFData.pull("SPY", start="2010-01-01", end="2025-01-01", auto_adjust=False)
gap_down = data.open / data.close.shift(1) - 1 <= -0.01
events = gap_down.vbt.signals.delta_ranges(5, close=data.close)
paths = events.get_projections()
paths.shape (6, 176)paths.iloc[-1].quantile([0.1, 0.5, 0.9]).round(3)0.1 0.960
0.5 1.010
0.9 1.041
Name: 2025-01-05 00:00:00-05:00, dtype: float64print((paths.iloc[-1] > 1).mean().round(3))0.631Five days after a gap down, SPY was higher 63% of the time, with a median gain of 1.0%. The projections can be summarized as quantile bands or plotted as a fan of paths, and the same approach covers earnings dates, macro releases, or any other dated event.
Bring your own event calendar
If you already have a list of event dates, mark the corresponding bars and extract a path after each one. Here two sample dates pick out two-bar moves from a small price series:
event_close = pd.Series([100.0, 102.0, 103.0, 104.0, 200.0, 198.0, 196.0, 198.0], index=pd.date_range("2026-01-01", periods=8))
event_dates = pd.to_datetime(["2026-01-01", "2026-01-05"])
event_mask = pd.Series(event_close.index.isin(event_dates), index=event_close.index)
event_ranges = event_mask.vbt.signals.delta_ranges(2, close=event_close)
event_paths = event_ranges.get_projections(id_level="start_idx")
((event_paths.iloc[-1] - 1) * 100).rename("Return [%]").round(1)start_idx
2026-01-01 3.0
2026-01-05 -2.0
Name: Return [%], dtype: float64The first path goes from 100 to 103, and the second from 200 to 196. Each starts at its own event
close, so the different price levels become comparable percentage moves. Replace the sample dates
with your calendar, aligned to the price bars, to compare earnings announcements, scheduled
releases, or your own signal dates. The full event_paths DataFrame also lets you compare the route
to each result, not just the final return.
When an event suggests a trading rule, Signal backtesting adds position sizing, exits, and trading costs. Use Walk-forward and cross-validation to test the rule on periods that were not used to choose it.
Market structure events
Many events are simple masks. A price gap is a comparison between the open and the previous close.
Directional changes, which mark reversals of a minimum size, are confirmed only after the price has
moved far enough, so a provisional turning point can change as new bars arrive. vbt.PIVOTINFO
finds them from highs and lows with separate up and down thresholds, and reports both the last
confirmed pivot and the latest provisional one with the bar where each occurred. Account for the
confirmation delay before using pivots in a backtest.
Plotting matches and projections
ranges.plot() shows each match on the price with its similarity, and plot_projections() draws
the forward paths with optional quantile bands next to the current price. The Patterns
and Projections highlights below show both.
Tutorial
The members-only Patterns and projections tutorial covers fitting, distance measures, and pattern-based signals in depth.
✅ Patterns are distinctive formations created by price movements on a chart and are central to technical analysis. There are now new dedicated functions and classes for detecting patterns of any complexity in any type of time series data. The idea is simple: fit a pattern to align with the scale and period of your selected data window, then compute the element-wise distance between them to get a single similarity score. You can adjust the threshold for this score to decide above which value a data window should be marked as "matched." Thanks to Numba, this operation can be performed hundreds of thousands of times per second! 🔎
data = vbt.YFData.pull("BTC-USD")
data.hlc3.vbt.find_pattern(
pattern=[5, 1, 3, 1, 2, 1],
window=100,
max_window=700,
).loc["2017":"2019"].plot().show()Tutorial
Learn more in the Patterns and projections tutorial.
✅ There are cleaner ways to analyze events and their impact on price than conventional backtesting. Meet projections! 👋 Not only can they help you assess event performance visually and quantitatively, but they can also project events into the future to support trading. This is done by extracting the price range after each event, collecting all these price ranges into a multidimensional array, and then deriving confidence intervals and other useful statistics from that array. When combined with patterns, these tools are a quantitative analyst's dream! 🌟
data = vbt.YFData.pull("ETH-USD")
pattern_ranges = data.hlc3.vbt.find_pattern(
pattern=data.close.iloc[-7:],
rescale_mode="rebase"
)
delta_ranges = pattern_ranges.with_delta(7)
fig = data.iloc[-7:].plot(plot_volume=False)
delta_ranges.plot_projections(fig=fig)
fig.show()Tutorial
Learn more in the Patterns and projections tutorial.
Related pages
- Indicators and signals › Trading signalsGenerate, combine, clean, and analyze entry and exit signals before backtesting
- Research toolkit › Time-series operationsRun compiled rolling windows, reductions, and date logic on labeled pandas data
- Backtesting › Backtesting engineSimulate orders, signals, and callbacks across many assets and parameters at once
- Indicators and signals › Machine learning labelsCreate triple-barrier, trend, and pivot labels, and backtest model predictions
Copyright © 2021–2026 Oleg Polakow. All rights reserved.
Site content and documentation are provided for using and evaluating VectorBT PRO and for educational purposes. Any other use, including building or supporting competing products or services, requires prior written consent.