bbstrader 0.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bbstrader might be problematic. Click here for more details.

@@ -0,0 +1,347 @@
1
+ import warnings
2
+
3
+ import matplotlib.pyplot as plt
4
+ import numpy as np
5
+ import pandas as pd
6
+ import quantstats as qs
7
+ import seaborn as sns
8
+ import yfinance as yf
9
+
10
+ warnings.filterwarnings("ignore")
11
+
12
+ sns.set_theme()
13
+
14
+ __all__ = [
15
+ "create_drawdowns",
16
+ "plot_performance",
17
+ "create_sharpe_ratio",
18
+ "create_sortino_ratio",
19
+ "plot_returns_and_dd",
20
+ "plot_monthly_yearly_returns",
21
+ "show_qs_stats",
22
+ ]
23
+
24
+
25
+ def create_sharpe_ratio(returns, periods=252) -> float:
26
+ """
27
+ Create the Sharpe ratio for the strategy, based on a
28
+ benchmark of zero (i.e. no risk-free rate information).
29
+
30
+ Args:
31
+ returns : A pandas Series representing period percentage returns.
32
+ periods (int): Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc.
33
+
34
+ Returns:
35
+ S (float): Sharpe ratio
36
+ """
37
+ return qs.stats.sharpe(returns, periods=periods)
38
+
39
+
40
+ # Define a function to calculate the Sortino Ratio
41
+
42
+
43
+ def create_sortino_ratio(returns, periods=252) -> float:
44
+ """
45
+ Create the Sortino ratio for the strategy, based on a
46
+ benchmark of zero (i.e. no risk-free rate information).
47
+
48
+ Args:
49
+ returns : A pandas Series representing period percentage returns.
50
+ periods (int): Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc.
51
+
52
+ Returns:
53
+ S (float): Sortino ratio
54
+ """
55
+ return qs.stats.sortino(returns, periods=periods)
56
+
57
+
58
+ def create_drawdowns(pnl):
59
+ """
60
+ Calculate the largest peak-to-trough drawdown of the PnL curve
61
+ as well as the duration of the drawdown. Requires that the
62
+ pnl_returns is a pandas Series.
63
+
64
+ Args:
65
+ pnl : A pandas Series representing period percentage returns.
66
+
67
+ Returns:
68
+ (tuple): drawdown, duration - high-water mark, duration.
69
+ """
70
+ # Calculate the cumulative returns curve
71
+ # and set up the High Water Mark
72
+ hwm = pd.Series(index=pnl.index)
73
+ hwm.iloc[0] = 0
74
+
75
+ # Create the drawdown and duration series
76
+ idx = pnl.index
77
+ drawdown = pd.Series(index=idx)
78
+ duration = pd.Series(index=idx)
79
+
80
+ # Loop over the index range
81
+ for t in range(1, len(idx)):
82
+ hwm.iloc[t] = max(hwm.iloc[t - 1], pnl.iloc[t])
83
+ drawdown.iloc[t] = hwm.iloc[t] - pnl.iloc[t]
84
+ duration.iloc[t] = 0 if drawdown.iloc[t] == 0 else duration.iloc[t - 1] + 1
85
+
86
+ return drawdown, drawdown.max(), duration.max()
87
+
88
+
89
+ def plot_performance(df, title):
90
+ """
91
+ Plot the performance of the strategy:
92
+ - (Portfolio value, %)
93
+ - (Period returns, %)
94
+ - (Drawdowns, %)
95
+
96
+ Args:
97
+ df (pd.DataFrame):
98
+ The DataFrame containing the strategy returns and drawdowns.
99
+ title (str): The title of the plot.
100
+
101
+ Note:
102
+ The DataFrame should contain the following columns
103
+ - Datetime: The timestamp of the data
104
+ - Equity Curve: The portfolio value
105
+ - Returns: The period returns
106
+ - Drawdown: The drawdowns
107
+ - Total : The total returns
108
+ """
109
+ data = df.copy()
110
+ data = data.sort_values(by="Datetime")
111
+ # Plot three charts: Equity curve,
112
+ # period returns, drawdowns
113
+ fig = plt.figure(figsize=(14, 8))
114
+ fig.suptitle(f"{title} Strategy Performance", fontsize=16)
115
+
116
+ # Set the outer colour to white
117
+ sns.set_theme()
118
+
119
+ # Plot the equity curve
120
+ ax1 = fig.add_subplot(311, ylabel="Portfolio value, %")
121
+ data["Equity Curve"].plot(ax=ax1, color="blue", lw=2.0)
122
+ ax1.set_xlabel("")
123
+ plt.grid(True)
124
+
125
+ # Plot the returns
126
+ ax2 = fig.add_subplot(312, ylabel="Period returns, %")
127
+ data["Returns"].plot(ax=ax2, color="black", lw=2.0)
128
+ ax2.set_xlabel("")
129
+ plt.grid(True)
130
+
131
+ # Plot Drawdown
132
+ ax3 = fig.add_subplot(313, ylabel="Drawdowns, %")
133
+ data["Drawdown"].plot(ax=ax3, color="red", lw=2.0)
134
+ ax3.set_xlabel("")
135
+ plt.grid(True)
136
+
137
+ # Plot the figure
138
+ plt.tight_layout()
139
+ plt.show()
140
+
141
+
142
+ def plot_returns_and_dd(df: pd.DataFrame, benchmark: str, title):
143
+ """
144
+ Plot the returns and drawdowns of the strategy
145
+ compared to a benchmark.
146
+
147
+ Args:
148
+ df (pd.DataFrame):
149
+ The DataFrame containing the strategy returns and drawdowns.
150
+ benchmark (str):
151
+ The ticker symbol of the benchmark to compare the strategy to.
152
+ title (str): The title of the plot.
153
+
154
+ Note:
155
+ The DataFrame should contain the following columns:
156
+ - Datetime : The timestamp of the data
157
+ - Equity Curve : The portfolio value
158
+ - Returns : The period returns
159
+ - Drawdown : The drawdowns
160
+ - Total : The total returns
161
+ """
162
+ # Ensure data is sorted by Datetime
163
+ data = df.copy()
164
+ data.reset_index(inplace=True)
165
+ data = data.sort_values(by="Datetime")
166
+ data.sort_values(by="Datetime", inplace=True)
167
+
168
+ # Get the first and last Datetime values
169
+ first_date = data["Datetime"].iloc[0]
170
+ last_date = data["Datetime"].iloc[-1]
171
+
172
+ # Download benchmark data from Yahoo Finance
173
+ # To avoid errors, we use the try-except block
174
+ # in case the benchmark is not available
175
+ try:
176
+ bm = yf.download(benchmark, start=first_date, end=last_date)
177
+ bm["log_return"] = np.log(bm["Close"] / bm["Close"].shift(1))
178
+ # Use exponential to get cumulative returns
179
+ bm_returns = np.exp(np.cumsum(bm["log_return"].fillna(0)))
180
+
181
+ # Normalize bm series to start at 1.0
182
+ bm_returns_normalized = bm_returns / bm_returns.iloc[0]
183
+ except Exception:
184
+ bm = None
185
+
186
+ # Create figure and plot space
187
+ fig, (ax1, ax2) = plt.subplots(
188
+ 2, 1, figsize=(14, 8), gridspec_kw={"height_ratios": [3, 1]}
189
+ )
190
+
191
+ # Plot the Equity Curve for the strategy
192
+ ax1.plot(
193
+ data["Datetime"], data["Equity Curve"], label="Backtest", color="green", lw=2.5
194
+ )
195
+ # Check benchmarck an Plot the Returns for the benchmark
196
+ if bm is not None:
197
+ ax1.plot(
198
+ bm.index, bm_returns_normalized, label="benchmark", color="gray", lw=2.5
199
+ )
200
+ ax1.set_title(f"{title} Strategy vs. Benchmark ({benchmark})")
201
+ else:
202
+ ax1.set_title(f"{title} Strategy Returns")
203
+ ax1.set_xlabel("Date")
204
+ ax1.set_ylabel("Cumulative Returns")
205
+ ax1.grid(True)
206
+ ax1.legend(loc="upper left")
207
+
208
+ # Plot the Drawdown
209
+ ax2.fill_between(
210
+ data["Datetime"], data["Drawdown"], 0, color="red", step="pre", alpha=0.5
211
+ )
212
+ ax2.plot(
213
+ data["Datetime"], data["Drawdown"], color="red", alpha=0.6, lw=2.5
214
+ ) # Overlay the line
215
+ ax2.set_title("Drawdown (%)")
216
+ ax2.set_xlabel("Date")
217
+ ax2.set_ylabel("Drawdown")
218
+ ax2.grid(True)
219
+
220
+ # Display the plot
221
+ plt.tight_layout()
222
+ plt.show()
223
+
224
+
225
+ def plot_monthly_yearly_returns(df: pd.DataFrame, title):
226
+ """
227
+ Plot the monthly and yearly returns of the strategy.
228
+
229
+ Args:
230
+ df (pd.DataFrame):
231
+ The DataFrame containing the strategy returns and drawdowns.
232
+ title (str): The title of the plot.
233
+
234
+ Note:
235
+ The DataFrame should contain the following columns:
236
+ - Datetime : The timestamp of the data
237
+ - Equity Curve : The portfolio value
238
+ - Returns : The period returns
239
+ - Drawdown : The drawdowns
240
+ - Total : The total returns
241
+ """
242
+ equity_df = df.copy()
243
+ equity_df.reset_index(inplace=True)
244
+ equity_df["Datetime"] = pd.to_datetime(equity_df["Datetime"])
245
+ equity_df.set_index("Datetime", inplace=True)
246
+
247
+ # Calculate daily returns
248
+ equity_df["Daily Returns"] = equity_df["Total"].pct_change()
249
+
250
+ # Group by year and month to get monthly returns
251
+ monthly_returns = (
252
+ equity_df["Daily Returns"]
253
+ .groupby([equity_df.index.year, equity_df.index.month])
254
+ .apply(lambda x: (1 + x).prod() - 1)
255
+ )
256
+
257
+ # Prepare monthly returns DataFrame
258
+ monthly_returns_df = monthly_returns.unstack(level=-1) * 100
259
+ monthly_returns_df.columns = monthly_returns_df.columns.map(
260
+ lambda x: pd.to_datetime(x, format="%m").strftime("%b")
261
+ )
262
+
263
+ # Calculate and prepare yearly returns DataFrame
264
+ yearly_returns_df = (
265
+ equity_df["Total"]
266
+ .resample("A")
267
+ .last()
268
+ .pct_change()
269
+ .to_frame(name="Yearly Returns")
270
+ * 100
271
+ )
272
+
273
+ # Set the aesthetics for the plots
274
+ sns.set_theme(style="darkgrid")
275
+
276
+ # Initialize the matplotlib figure,
277
+ # adjust the height_ratios to give more space to the yearly returns
278
+ f, (ax1, ax2) = plt.subplots(
279
+ 2, 1, figsize=(12, 8), gridspec_kw={"height_ratios": [2, 1]}
280
+ )
281
+ f.suptitle(f"{title} Strategy Monthly and Yearly Returns")
282
+ # Find the min and max values in the data to set the color scale range.
283
+ vmin = monthly_returns_df.min().min()
284
+ vmax = monthly_returns_df.max().max()
285
+ # Define the color palette for the heatmap
286
+ cmap = sns.diverging_palette(10, 133, sep=3, n=256, center="light")
287
+
288
+ # Create the heatmap with the larger legend
289
+ sns.heatmap(
290
+ monthly_returns_df,
291
+ annot=True,
292
+ fmt=".1f",
293
+ linewidths=0.5,
294
+ ax=ax1,
295
+ cbar_kws={"shrink": 0.8},
296
+ cmap=cmap,
297
+ center=0,
298
+ vmin=vmin,
299
+ vmax=vmax,
300
+ )
301
+
302
+ # Rotate the year labels on the y-axis to vertical
303
+ ax1.set_yticklabels(ax1.get_yticklabels(), rotation=0)
304
+ ax1.set_ylabel("")
305
+ ax1.set_xlabel("")
306
+
307
+ # Create the bar plot
308
+ yearly_returns_df.plot(kind="bar", ax=ax2, legend=None, color="skyblue")
309
+
310
+ # Set plot titles and labels
311
+ ax1.set_title("Monthly Returns (%)")
312
+ ax2.set_title("Yearly Returns (%)")
313
+
314
+ # Rotate the x labels for the yearly returns bar plot
315
+ ax2.set_xticklabels(yearly_returns_df.index.strftime("%Y"), rotation=45)
316
+ ax2.set_xlabel("")
317
+
318
+ # Adjust layout spacing
319
+ plt.tight_layout()
320
+
321
+ # Show the plot
322
+ plt.show()
323
+
324
+
325
+ def show_qs_stats(returns, benchmark, strategy_name, save_dir=None):
326
+ """
327
+ Generate the full quantstats report for the strategy.
328
+
329
+ Args:
330
+ returns (pd.Serie):
331
+ The DataFrame containing the strategy returns and drawdowns.
332
+ benchmark (str):
333
+ The ticker symbol of the benchmark to compare the strategy to.
334
+ strategy_name (str): The name of the strategy.
335
+ """
336
+ # Load the returns data
337
+ returns = returns.copy()
338
+
339
+ # Drop duplicate index entries
340
+ returns = returns[~returns.index.duplicated(keep="first")]
341
+
342
+ # Extend pandas functionality with quantstats
343
+ qs.extend_pandas()
344
+
345
+ # Generate the full report with a benchmark
346
+ qs.reports.full(returns, mode="full", benchmark=benchmark)
347
+ qs.reports.html(returns, benchmark=benchmark, output=save_dir, title=strategy_name)