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