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.
- bbstrader/__init__.py +27 -0
- bbstrader/__main__.py +92 -0
- bbstrader/api/__init__.py +96 -0
- bbstrader/api/handlers.py +245 -0
- bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
- bbstrader/api/metatrader_client.pyi +624 -0
- bbstrader/assets/bbs_.png +0 -0
- bbstrader/assets/bbstrader.ico +0 -0
- bbstrader/assets/bbstrader.png +0 -0
- bbstrader/assets/qs_metrics_1.png +0 -0
- bbstrader/btengine/__init__.py +54 -0
- bbstrader/btengine/backtest.py +358 -0
- bbstrader/btengine/data.py +737 -0
- bbstrader/btengine/event.py +229 -0
- bbstrader/btengine/execution.py +287 -0
- bbstrader/btengine/performance.py +408 -0
- bbstrader/btengine/portfolio.py +393 -0
- bbstrader/btengine/strategy.py +588 -0
- bbstrader/compat.py +28 -0
- bbstrader/config.py +100 -0
- bbstrader/core/__init__.py +27 -0
- bbstrader/core/data.py +628 -0
- bbstrader/core/strategy.py +466 -0
- bbstrader/metatrader/__init__.py +48 -0
- bbstrader/metatrader/_copier.py +720 -0
- bbstrader/metatrader/account.py +865 -0
- bbstrader/metatrader/broker.py +418 -0
- bbstrader/metatrader/copier.py +1487 -0
- bbstrader/metatrader/rates.py +495 -0
- bbstrader/metatrader/risk.py +667 -0
- bbstrader/metatrader/trade.py +1692 -0
- bbstrader/metatrader/utils.py +402 -0
- bbstrader/models/__init__.py +39 -0
- bbstrader/models/nlp.py +932 -0
- bbstrader/models/optimization.py +182 -0
- bbstrader/scripts.py +665 -0
- bbstrader/trading/__init__.py +33 -0
- bbstrader/trading/execution.py +1159 -0
- bbstrader/trading/strategy.py +362 -0
- bbstrader/trading/utils.py +69 -0
- bbstrader-2.0.3.dist-info/METADATA +396 -0
- bbstrader-2.0.3.dist-info/RECORD +45 -0
- bbstrader-2.0.3.dist-info/WHEEL +5 -0
- bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
- 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)
|