Analysis¶
Simulation ranges¶
- Each simulation begins and ends at specific points, typically corresponding to the first and last rows of your data. You can define a different simulation range beforehand, and now, you can also adjust this range during the simulation. This flexibility allows you to halt the simulation when further processing is unnecessary. Additionally, the date range is stored in the portfolio object, ensuring all metrics and subplots are aware of it. Processing only the relevant dates enhances execution speed and introduces a new dimension for analysis—isolated time windows
Example 1: Simulate a quick liquidation scenario
>>> @njit
... def post_segment_func_nb(c):
... value = vbt.pf_nb.get_group_value_nb(c, c.group)
... if value <= 0:
... vbt.pf_nb.stop_group_sim_nb(c, c.group) # (1)!
>>> pf = vbt.PF.from_random_signals(
... "BTC-USD",
... n=10,
... seed=42,
... sim_start="auto", # (2)!
... post_segment_func_nb=post_segment_func_nb,
... leverage=10,
... )
>>> pf.plot_value() # (3)!
- Halt the simulation of the current group if its value becomes negative
- Initiate the simulation at the first signal
- Ensure all metrics and subplots consider only data up to the liquidation point, even if the portfolio retains all original data (2014 → today)
Example 2: Analyze a date range of an already simulated portfolio
>>> pf = vbt.PF.from_random_signals("BTC-USD", n=10, seed=42)
>>> pf.get_sharpe_ratio(sim_start="2023", sim_end="2024") # (1)!
1.7846214408154346
>>> pf.get_sharpe_ratio(sim_start="2023", sim_end="2024", rec_sim_range=True) # (2)!
1.8377982089422782
>>> pf.returns_stats(settings=dict(sim_start="2023", sim_end="2024")) # (3)!
Start Index 2023-01-01 00:00:00+00:00
End Index 2023-12-31 00:00:00+00:00
Total Duration 365 days 00:00:00
Total Return [%] 84.715081
Benchmark Return [%] 155.417419
Annualized Return [%] 84.715081
Annualized Volatility [%] 38.49976
Max Drawdown [%] 20.057773
Max Drawdown Duration 102 days 00:00:00
Sharpe Ratio 1.784621
Calmar Ratio 4.223554
Omega Ratio 1.378076
Sortino Ratio 3.059933
Skew -0.39136
Kurtosis 13.607937
Tail Ratio 1.323376
Common Sense Ratio 1.823713
Value at Risk -0.028314
Alpha -0.103145
Beta 0.770428
dtype: object
- Consider only the returns within the specified date range to calculate the Sharpe ratio. Note that returns may still be influenced by data from outside the date range (e.g., open positions)
- Recursively apply the date range to all metrics that the Sharpe ratio depends on, such as equity, cash, and orders, treating data outside the date range as non-existent
- Ensure that the date range is applied consistently across all statistics
Expanding trade metrics¶
- Regular metrics such as MAE and MFE represent only the final point of each trade, but what if we would like to see their development during each trade? You can now analyze expanding trade metrics as DataFrames!
Visualize the expanding MFE using projections
>>> data = vbt.YFData.pull("BTC-USD")
>>> pf = vbt.PF.from_random_signals(data, n=50, tp_stop=0.5)
>>> pf.trades.plot_expanding_mfe_returns().show()
Trade signals¶
- New method for plotting trades that categorizes entry and exit trades into long entries, long exits, short entries, and short exits. Supports different styles for positions.
Plot trade signals of a Bollinger Bands strategy
>>> data = vbt.YFData.pull("BTC-USD")
>>> bb = data.run("bbands")
>>> long_entries = data.hlc3.vbt.crossed_above(bb.upper) & (bb.bandwidth < 0.1)
>>> long_exits = data.hlc3.vbt.crossed_below(bb.upper) & (bb.bandwidth > 0.5)
>>> short_entries = data.hlc3.vbt.crossed_below(bb.lower) & (bb.bandwidth < 0.1)
>>> short_exits = data.hlc3.vbt.crossed_above(bb.lower) & (bb.bandwidth > 0.5)
>>> pf = vbt.PF.from_signals(
... data,
... long_entries=long_entries,
... long_exits=long_exits,
... short_entries=short_entries,
... short_exits=short_exits
... )
>>> pf.plot_trade_signals().show()
Edge ratio¶
- Edge ratio is a unique way to quantify entry profitability. Unlike most performance metrics, the edge ratio takes both open profits and losses into account. This can help you determine optimal trade exits.
Compare the edge ratio of an EMA crossover to a random strategy
>>> data = vbt.YFData.pull("BTC-USD")
>>> fast_ema = data.run("ema", 10, hide_params=True)
>>> slow_ema = data.run("ema", 20, hide_params=True)
>>> entries = fast_ema.real_crossed_above(slow_ema)
>>> exits = fast_ema.real_crossed_below(slow_ema)
>>> pf = vbt.PF.from_signals(data, entries, exits, direction="both")
>>> rand_pf = vbt.PF.from_random_signals(data, n=pf.orders.count() // 2) # (1)!
>>> fig = pf.trades.plot_running_edge_ratio(
... trace_kwargs=dict(line_color="limegreen", name="Edge Ratio (S)")
... )
>>> fig = rand_pf.trades.plot_running_edge_ratio(
... trace_kwargs=dict(line_color="mediumslateblue", name="Edge Ratio (R)"),
... fig=fig
... )
>>> fig.show()
- Random strategy should have a similar number of orders to be comparable
Trade history¶
- Trade history is a human-readable DataFrame that lists orders and extends them with valuable information on entry trades, exit trades, and positions.
Get the trade history of a random portfolio with one signal
>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], missing_index="drop")
>>> pf = vbt.PF.from_random_signals(
... data,
... n=1,
... run_kwargs=dict(hide_params=True),
... tp_stop=0.5,
... sl_stop=0.1
... )
>>> pf.trade_history
Order Id Column Signal Index Creation Index \
0 0 BTC-USD 2016-02-20 00:00:00+00:00 2016-02-20 00:00:00+00:00
1 1 BTC-USD 2016-02-20 00:00:00+00:00 2016-06-12 00:00:00+00:00
2 0 ETH-USD 2019-05-25 00:00:00+00:00 2019-05-25 00:00:00+00:00
3 1 ETH-USD 2019-05-25 00:00:00+00:00 2019-07-15 00:00:00+00:00
Fill Index Side Type Stop Type Size Price \
0 2016-02-20 00:00:00+00:00 Buy Market None 0.228747 437.164001
1 2016-06-12 00:00:00+00:00 Sell Market TP 0.228747 655.746002
2 2019-05-25 00:00:00+00:00 Buy Market None 0.397204 251.759872
3 2019-07-15 00:00:00+00:00 Sell Market SL 0.397204 226.583885
Fees PnL Return Direction Status Entry Trade Id Exit Trade Id \
0 0.0 50.0 0.5 Long Closed 0 -1
1 0.0 50.0 0.5 Long Closed -1 0
2 0.0 -10.0 -0.1 Long Closed 0 -1
3 0.0 -10.0 -0.1 Long Closed -1 0
Position Id
0 0
1 0
2 0
3 0
Patterns¶
- Patterns are the distinctive formations created by the movements of security prices on a chart and are the foundation of technical analysis. There are new designated functions and classes for detecting patterns of any complexity in any time series data. The concept is simple: fit a pattern to match the scale and period of the selected data window, and compute their element-wise distance to each other to derive a single similarity score. You can then tweak the threshold for this score above which a data window should be marked as "matched". Thanks to Numba, this operation can be done hundreds of thousands of times per second!
Tutorial
Learn more in the Patterns and projections tutorial.
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()
Projections¶
- There are more clean ways of analyzing events and their impact on the price than conventional backtesting. Meet projections! Not only they can allow you to assess the performance of events visually and quantitatively, but they also can enable you to project events into the future for assistance in trading. That is possible by extracting the price range after each event, putting all the price ranges into a multidimensional array, and deriving the confidence intervals and other meaningful statistics from that array. Combined with patterns, these are a quantitative analyst's dream tools!
Tutorial
Learn more in the Patterns and projections tutorial.
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()
MAE and MFE¶
- Maximum Adverse Excursion (MAE) helps you to identify what the maximum loss was during a trade. This is also known as maximum drawdown of the position. Maximum Favorable Excursion (MFE), on the other hand, shows you what the highest profit was during a trade. Analyzing MAE and MFE statistics can help you optimize your exit strategies.
Analyze the MAE of a random portfolio without SL
>>> data = vbt.YFData.pull("BTC-USD")
>>> pf = vbt.PF.from_random_signals(data, n=50)
>>> pf.trades.plot_mae_returns().show()
Analyze the MAE of a random portfolio with SL
>>> pf = vbt.PF.from_random_signals(data, n=50, sl_stop=0.1)
>>> pf.trades.plot_mae_returns().show()
OHLC-native classes¶
- Previously, OHLC was used for simulation but only the close price was used for analysis purposes. With this update, most classes will allow you to keep track of the entire OHLC data for a more accurate quantitative and qualitative analysis.
Plot trades of a random portfolio
>>> data = vbt.YFData.pull("BTC-USD", start="2020-01", end="2020-03")
>>> pf = vbt.PF.from_random_signals(
... open=data.open,
... high=data.high,
... low=data.low,
... close=data.close,
... n=10
... )
>>> pf.trades.plot().show()
Benchmark¶
- Benchmark can be easily set for the entire portfolio.
Compare Microsoft to S&P 500
>>> data = vbt.YFData.pull(["SPY", "MSFT"], start="2010", missing_columns="drop")
>>> pf = vbt.PF.from_holding(
... close=data.data["MSFT"]["Close"],
... bm_close=data.data["SPY"]["Close"]
... )
>>> pf.plot_cumulative_returns().show()