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.

Find the 10 closest matches to the last 30 days and project the next 30
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.807
after = 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: float64
after.plot_projections(plot_past_period=90).show()  
Bitcoin price for the last 90 days of 2024 with the paths that followed the 10 most similar historical windows projected 30 days forward Figure data (JSON)

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.

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:

Compute pattern similarity as an indicator
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: float64

The 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:

Study what SPY did in the five days after a gap down
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: float64
print((paths.iloc[-1] > 1).mean().round(3))
0.631

Five 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:

Compare price moves after your own event dates
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: float64

The 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

✅ 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! 🔎

Find and plot a descending triangle pattern
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()
Descending triangle pattern matches in BTC-USD from 2017 through 2019 Figure data (JSON)

Tutorial

Learn more in the Patterns and projections tutorial.

Projections

✅ 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! 🌟

Find occurrences of the price moving similarly to the last week and project them
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()
ETH-USD last-week pattern matches with projected confidence bands Figure data (JSON)

Tutorial

Learn more in the Patterns and projections tutorial.

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.