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,393 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from queue import Queue
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import quantstats as qs
|
|
8
|
+
|
|
9
|
+
from bbstrader.btengine.data import DataHandler
|
|
10
|
+
from bbstrader.btengine.event import (
|
|
11
|
+
Events,
|
|
12
|
+
FillEvent,
|
|
13
|
+
MarketEvent,
|
|
14
|
+
OrderEvent,
|
|
15
|
+
SignalEvent,
|
|
16
|
+
)
|
|
17
|
+
from bbstrader.btengine.performance import (
|
|
18
|
+
create_drawdowns,
|
|
19
|
+
create_sharpe_ratio,
|
|
20
|
+
create_sortino_ratio,
|
|
21
|
+
plot_monthly_yearly_returns,
|
|
22
|
+
plot_performance,
|
|
23
|
+
plot_returns_and_dd,
|
|
24
|
+
show_qs_stats,
|
|
25
|
+
)
|
|
26
|
+
from bbstrader.metatrader.utils import TIMEFRAMES
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Portfolio",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Portfolio:
|
|
34
|
+
"""
|
|
35
|
+
This describes a `Portfolio()` object that keeps track of the positions
|
|
36
|
+
within a portfolio and generates orders of a fixed quantity of stock based on signals.
|
|
37
|
+
|
|
38
|
+
The portfolio order management system is possibly the most complex component of
|
|
39
|
+
an event driven backtester. Its role is to keep track of all current market positions
|
|
40
|
+
as well as the market value of the positions (known as the "holdings").
|
|
41
|
+
This is simply an estimate of the liquidation value of the position and is derived in part
|
|
42
|
+
from the data handling facility of the backtester.
|
|
43
|
+
|
|
44
|
+
In addition to the positions and holdings management the portfolio must also be aware of
|
|
45
|
+
risk factors and position sizing techniques in order to optimise orders that are sent
|
|
46
|
+
to a brokerage or other form of market access.
|
|
47
|
+
|
|
48
|
+
Unfortunately, Portfolio and `Order Management Systems (OMS)` can become rather complex!
|
|
49
|
+
So let's keep the `Portfolio` object relatively straightforward anf improve it foward.
|
|
50
|
+
|
|
51
|
+
Continuing in the vein of the Event class hierarchy a Portfolio object must be able
|
|
52
|
+
to handle `SignalEvent` objects, generate `OrderEvent` objects and interpret `FillEvent`
|
|
53
|
+
objects to update positions. Thus it is no surprise that the Portfolio objects are often
|
|
54
|
+
the largest component of event-driven systems, in terms of lines of code (LOC).
|
|
55
|
+
|
|
56
|
+
The initialisation of the Portfolio object requires access to the bars `DataHandler`,
|
|
57
|
+
the `Event Queue`, a `start datetime stamp` and an `initial capital`
|
|
58
|
+
value (defaulting to `100,000 USD`) and others parameter based on the `Strategy` requirement.
|
|
59
|
+
|
|
60
|
+
The `Portfolio` is designed to handle position sizing and current holdings,
|
|
61
|
+
but will carry out trading orders by simply them to the brokerage with a predetermined
|
|
62
|
+
fixed quantity size, if the portfolio has enough cash to place the order.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
The portfolio contains the `all_positions` and `current_positions` members.
|
|
66
|
+
The former stores a list of all previous positions recorded at the timestamp of a market data event.
|
|
67
|
+
A position is simply the quantity of the asset held. Negative positions mean the asset has been shorted.
|
|
68
|
+
|
|
69
|
+
The latter current_positions dictionary stores contains the current positions
|
|
70
|
+
for the last market bar update, for each symbol.
|
|
71
|
+
|
|
72
|
+
In addition to the positions data the portfolio stores `holdings`,
|
|
73
|
+
which describe the current market value of the positions held. "Current market value"
|
|
74
|
+
in this instance means the closing price obtained from the current market bar,
|
|
75
|
+
which is clearly an approximation, but is reasonable enough for the time being.
|
|
76
|
+
`all_holdings` stores the historical list of all symbol holdings, while current_holdings
|
|
77
|
+
stores the most up to date dictionary of all symbol holdings values.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
bars: DataHandler,
|
|
83
|
+
events: "Queue[Union[OrderEvent, FillEvent, SignalEvent]]",
|
|
84
|
+
start_date: datetime,
|
|
85
|
+
initial_capital: float = 100000.0,
|
|
86
|
+
**kwargs: Any,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Initialises the portfolio with bars and an event queue.
|
|
90
|
+
Also includes a starting datetime index and initial capital
|
|
91
|
+
(USD unless otherwise stated).
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
bars (DataHandler): The DataHandler object with current market data.
|
|
95
|
+
events (Queue): The Event Queue object.
|
|
96
|
+
start_date (datetime): The start date (bar) of the portfolio.
|
|
97
|
+
initial_capital (float): The starting capital in USD.
|
|
98
|
+
|
|
99
|
+
kwargs (dict): Additional arguments
|
|
100
|
+
- `leverage`: The leverage to apply to the portfolio.
|
|
101
|
+
- `time_frame`: The time frame of the bars.
|
|
102
|
+
- `session_duration`: The number of trading hours in a day.
|
|
103
|
+
- `benchmark`: The benchmark symbol to compare the portfolio.
|
|
104
|
+
- `output_dir`: The directory to save the backtest results.
|
|
105
|
+
- `strategy_name`: The name of the strategy (the name must not include 'Strategy' in it).
|
|
106
|
+
- `print_stats`: Whether to print the backtest statistics.
|
|
107
|
+
"""
|
|
108
|
+
self.bars = bars
|
|
109
|
+
self.events = events
|
|
110
|
+
self.symbol_list = self.bars.symbols
|
|
111
|
+
self.start_date = start_date
|
|
112
|
+
self.initial_capital = initial_capital
|
|
113
|
+
self._leverage = kwargs.get("leverage", 1)
|
|
114
|
+
|
|
115
|
+
self.trading_hours = kwargs.get("session_duration", 23)
|
|
116
|
+
self.benchmark = kwargs.get("benchmark", "SPY")
|
|
117
|
+
self.output_dir = kwargs.get("output_dir", None)
|
|
118
|
+
self.strategy_name = kwargs.get("strategy_name", "")
|
|
119
|
+
self.print_stats = kwargs.get("print_stats", True)
|
|
120
|
+
timeframe = kwargs.get("time_frame", "D1")
|
|
121
|
+
if timeframe not in TIMEFRAMES:
|
|
122
|
+
raise ValueError("Timeframe not supported")
|
|
123
|
+
if timeframe == "D1":
|
|
124
|
+
self.tf = 252
|
|
125
|
+
else:
|
|
126
|
+
if "m" in timeframe:
|
|
127
|
+
minutes = int(timeframe.replace("m", ""))
|
|
128
|
+
bars_per_day = self.trading_hours * (60 / minutes)
|
|
129
|
+
elif "h" in timeframe:
|
|
130
|
+
hours = int(timeframe.replace("h", ""))
|
|
131
|
+
bars_per_day = self.trading_hours / hours
|
|
132
|
+
else:
|
|
133
|
+
bars_per_day = 1 # Should not be reached given the check
|
|
134
|
+
self.tf = int(252 * bars_per_day)
|
|
135
|
+
|
|
136
|
+
self.all_positions: List[Dict[str, Any]] = self.construct_all_positions()
|
|
137
|
+
self.current_positions: Dict[str, Any] = dict(
|
|
138
|
+
(k, v) for k, v in [(s, 0) for s in self.symbol_list]
|
|
139
|
+
)
|
|
140
|
+
self.all_holdings: List[Dict[str, Any]] = self.construct_all_holdings()
|
|
141
|
+
self.current_holdings: Dict[str, Any] = self.construct_current_holdings()
|
|
142
|
+
self.equity_curve: Optional[pd.DataFrame] = None
|
|
143
|
+
|
|
144
|
+
def construct_all_positions(self) -> List[Dict[str, Any]]:
|
|
145
|
+
"""
|
|
146
|
+
Constructs the positions list using the start_date
|
|
147
|
+
to determine when the time index will begin.
|
|
148
|
+
"""
|
|
149
|
+
d = dict((k, v) for k, v in [(s, 0) for s in self.symbol_list])
|
|
150
|
+
d["Datetime"] = self.start_date
|
|
151
|
+
return [d]
|
|
152
|
+
|
|
153
|
+
def construct_all_holdings(self) -> List[Dict[str, Any]]:
|
|
154
|
+
"""
|
|
155
|
+
Constructs the holdings list using the start_date
|
|
156
|
+
to determine when the time index will begin.
|
|
157
|
+
"""
|
|
158
|
+
d = dict((k, v) for k, v in [(s, 0.0) for s in self.symbol_list])
|
|
159
|
+
d["Datetime"] = self.start_date
|
|
160
|
+
d["Cash"] = self.initial_capital
|
|
161
|
+
d["Commission"] = 0.0
|
|
162
|
+
d["Total"] = self.initial_capital
|
|
163
|
+
return [d]
|
|
164
|
+
|
|
165
|
+
def construct_current_holdings(self) -> Dict[str, float]:
|
|
166
|
+
"""
|
|
167
|
+
This constructs the dictionary which will hold the instantaneous
|
|
168
|
+
value of the portfolio across all symbols.
|
|
169
|
+
"""
|
|
170
|
+
d = dict((k, v) for k, v in [(s, 0.0) for s in self.symbol_list])
|
|
171
|
+
d["Cash"] = self.initial_capital
|
|
172
|
+
d["Commission"] = 0.0
|
|
173
|
+
d["Total"] = self.initial_capital
|
|
174
|
+
return d
|
|
175
|
+
|
|
176
|
+
def _get_price(self, symbol: str) -> float:
|
|
177
|
+
try:
|
|
178
|
+
price = self.bars.get_latest_bar_value(symbol, "adj_close")
|
|
179
|
+
return price
|
|
180
|
+
except (AttributeError, KeyError, ValueError):
|
|
181
|
+
try:
|
|
182
|
+
price = self.bars.get_latest_bar_value(symbol, "close")
|
|
183
|
+
return price
|
|
184
|
+
except (AttributeError, KeyError, ValueError):
|
|
185
|
+
return 0.0
|
|
186
|
+
|
|
187
|
+
def update_timeindex(self, event: MarketEvent) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Adds a new record to the positions matrix for the current
|
|
190
|
+
market data bar. This reflects the PREVIOUS bar, i.e. all
|
|
191
|
+
current market data at this stage is known (OHLCV).
|
|
192
|
+
Makes use of a MarketEvent from the events queue.
|
|
193
|
+
"""
|
|
194
|
+
latest_datetime = self.bars.get_latest_bar_datetime(self.symbol_list[0])
|
|
195
|
+
# Update positions
|
|
196
|
+
# ================
|
|
197
|
+
dp = dict((k, v) for k, v in [(s, 0) for s in self.symbol_list])
|
|
198
|
+
dp["Datetime"] = latest_datetime
|
|
199
|
+
for s in self.symbol_list:
|
|
200
|
+
dp[s] = self.current_positions[s]
|
|
201
|
+
# Append the current positions
|
|
202
|
+
self.all_positions.append(dp)
|
|
203
|
+
|
|
204
|
+
# Update holdings
|
|
205
|
+
# ===============
|
|
206
|
+
dh = dict((k, v) for k, v in [(s, 0) for s in self.symbol_list])
|
|
207
|
+
dh["Datetime"] = latest_datetime
|
|
208
|
+
dh["Cash"] = self.current_holdings["Cash"]
|
|
209
|
+
dh["Commission"] = self.current_holdings["Commission"]
|
|
210
|
+
dh["Total"] = self.current_holdings["Cash"]
|
|
211
|
+
for s in self.symbol_list:
|
|
212
|
+
# Approximation to the real value
|
|
213
|
+
price = self._get_price(s)
|
|
214
|
+
market_value = self.current_positions[s] * price
|
|
215
|
+
dh[s] = market_value
|
|
216
|
+
dh["Total"] += market_value
|
|
217
|
+
|
|
218
|
+
# Append the current holdings
|
|
219
|
+
self.all_holdings.append(dh)
|
|
220
|
+
|
|
221
|
+
def update_positions_from_fill(self, fill: FillEvent) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Takes a Fill object and updates the position matrix to
|
|
224
|
+
reflect the new position.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
fill (FillEvent): The Fill object to update the positions with.
|
|
228
|
+
"""
|
|
229
|
+
# Check whether the fill is a buy or sell
|
|
230
|
+
fill_dir = 0
|
|
231
|
+
if fill.direction == "BUY":
|
|
232
|
+
fill_dir = 1
|
|
233
|
+
if fill.direction == "SELL":
|
|
234
|
+
fill_dir = -1
|
|
235
|
+
|
|
236
|
+
# Update positions list with new quantities
|
|
237
|
+
self.current_positions[fill.symbol] += fill_dir * fill.quantity
|
|
238
|
+
|
|
239
|
+
def update_holdings_from_fill(self, fill: FillEvent) -> None:
|
|
240
|
+
"""
|
|
241
|
+
Takes a Fill object and updates the holdings matrix to
|
|
242
|
+
reflect the holdings value.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
fill (FillEvent): The Fill object to update the holdings with.
|
|
246
|
+
"""
|
|
247
|
+
# Check whether the fill is a buy or sell
|
|
248
|
+
fill_dir = 0
|
|
249
|
+
if fill.direction == "BUY":
|
|
250
|
+
fill_dir = 1
|
|
251
|
+
if fill.direction == "SELL":
|
|
252
|
+
fill_dir = -1
|
|
253
|
+
|
|
254
|
+
# Update holdings list with new quantities
|
|
255
|
+
price = self._get_price(fill.symbol)
|
|
256
|
+
cost = fill_dir * price * fill.quantity
|
|
257
|
+
self.current_holdings[fill.symbol] += cost
|
|
258
|
+
self.current_holdings["Commission"] += fill.commission
|
|
259
|
+
self.current_holdings["Cash"] -= cost + fill.commission
|
|
260
|
+
self.current_holdings["Total"] -= cost + fill.commission
|
|
261
|
+
|
|
262
|
+
def update_fill(self, event: FillEvent) -> None:
|
|
263
|
+
"""
|
|
264
|
+
Updates the portfolio current positions and holdings
|
|
265
|
+
from a FillEvent.
|
|
266
|
+
"""
|
|
267
|
+
if event.type == Events.FILL:
|
|
268
|
+
self.update_positions_from_fill(event)
|
|
269
|
+
self.update_holdings_from_fill(event)
|
|
270
|
+
|
|
271
|
+
def generate_order(self, signal: SignalEvent) -> Optional[OrderEvent]:
|
|
272
|
+
"""
|
|
273
|
+
Turns a SignalEvent into an OrderEvent.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
signal (SignalEvent): The tuple containing Signal information.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
OrderEvent: The OrderEvent to be executed.
|
|
280
|
+
"""
|
|
281
|
+
order = None
|
|
282
|
+
|
|
283
|
+
symbol = signal.symbol
|
|
284
|
+
direction = signal.signal_type
|
|
285
|
+
quantity = signal.quantity
|
|
286
|
+
strength = signal.strength
|
|
287
|
+
price = signal.price or self._get_price(symbol)
|
|
288
|
+
cur_quantity = self.current_positions[symbol]
|
|
289
|
+
mkt_quantity = round(float(quantity) * float(strength), 2)
|
|
290
|
+
new_quantity = mkt_quantity * self._leverage
|
|
291
|
+
|
|
292
|
+
if direction in ["LONG", "SHORT", "EXIT"]:
|
|
293
|
+
order_type = "MKT"
|
|
294
|
+
else:
|
|
295
|
+
order_type = direction
|
|
296
|
+
|
|
297
|
+
if direction == "LONG" and new_quantity > 0:
|
|
298
|
+
order = OrderEvent(
|
|
299
|
+
symbol, order_type, new_quantity, "BUY", price, direction
|
|
300
|
+
)
|
|
301
|
+
if direction == "SHORT" and new_quantity > 0:
|
|
302
|
+
order = OrderEvent(
|
|
303
|
+
symbol, order_type, new_quantity, "SELL", price, direction
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if direction == "EXIT" and cur_quantity > 0:
|
|
307
|
+
order = OrderEvent(
|
|
308
|
+
symbol, order_type, abs(cur_quantity), "SELL", price, direction
|
|
309
|
+
)
|
|
310
|
+
if direction == "EXIT" and cur_quantity < 0:
|
|
311
|
+
order = OrderEvent(
|
|
312
|
+
symbol, order_type, abs(cur_quantity), "BUY", price, direction
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return order
|
|
316
|
+
|
|
317
|
+
def update_signal(self, event: SignalEvent) -> None:
|
|
318
|
+
"""
|
|
319
|
+
Acts on a SignalEvent to generate new orders
|
|
320
|
+
based on the portfolio logic.
|
|
321
|
+
"""
|
|
322
|
+
if event.type == Events.SIGNAL:
|
|
323
|
+
order_event = self.generate_order(event)
|
|
324
|
+
self.events.put(order_event)
|
|
325
|
+
|
|
326
|
+
def create_equity_curve_dataframe(self) -> None:
|
|
327
|
+
"""
|
|
328
|
+
Creates a pandas DataFrame from the all_holdings
|
|
329
|
+
list of dictionaries.
|
|
330
|
+
"""
|
|
331
|
+
curve = pd.DataFrame(self.all_holdings)
|
|
332
|
+
curve["Datetime"] = pd.to_datetime(curve["Datetime"], utc=True)
|
|
333
|
+
curve.set_index("Datetime", inplace=True)
|
|
334
|
+
curve["Returns"] = curve["Total"].pct_change(fill_method=None)
|
|
335
|
+
curve["Equity Curve"] = (1.0 + curve["Returns"]).cumprod()
|
|
336
|
+
self.equity_curve = curve
|
|
337
|
+
|
|
338
|
+
def output_summary_stats(self) -> List[Any]:
|
|
339
|
+
"""
|
|
340
|
+
Creates a list of summary statistics for the portfolio.
|
|
341
|
+
"""
|
|
342
|
+
if self.equity_curve is None:
|
|
343
|
+
self.create_equity_curve_dataframe()
|
|
344
|
+
|
|
345
|
+
total_return = self.equity_curve["Equity Curve"].iloc[-1] # type: ignore
|
|
346
|
+
returns = self.equity_curve["Returns"] # type: ignore
|
|
347
|
+
pnl = self.equity_curve["Equity Curve"] # type: ignore
|
|
348
|
+
|
|
349
|
+
sharpe_ratio = create_sharpe_ratio(returns, periods=self.tf)
|
|
350
|
+
sortino_ratio = create_sortino_ratio(returns, periods=self.tf)
|
|
351
|
+
drawdown, _, _ = create_drawdowns(pnl)
|
|
352
|
+
drawdown = drawdown.fillna(0.0)
|
|
353
|
+
max_dd = qs.stats.max_drawdown(returns)
|
|
354
|
+
dd_details = qs.stats.drawdown_details(drawdown)
|
|
355
|
+
if dd_details.empty:
|
|
356
|
+
dd_duration = 0
|
|
357
|
+
else:
|
|
358
|
+
dd_duration = dd_details["days"].max()
|
|
359
|
+
self.equity_curve["Drawdown"] = drawdown
|
|
360
|
+
|
|
361
|
+
stats = [
|
|
362
|
+
("Total Return", f"{(total_return - 1.0) * 100.0:.2f}%"),
|
|
363
|
+
("Sharpe Ratio", f"{sharpe_ratio:.2f}"),
|
|
364
|
+
("Sortino Ratio", f"{sortino_ratio:.2f}"),
|
|
365
|
+
("Max Drawdown", f"{max_dd * 100.0:.2f}%"),
|
|
366
|
+
("Drawdown Duration", f"{dd_duration}"),
|
|
367
|
+
]
|
|
368
|
+
now = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
369
|
+
strategy_name = self.strategy_name.replace(" ", "_")
|
|
370
|
+
if self.output_dir:
|
|
371
|
+
results_dir = Path(self.output_dir) / strategy_name
|
|
372
|
+
else:
|
|
373
|
+
results_dir = Path(".backtests") / strategy_name
|
|
374
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
375
|
+
|
|
376
|
+
csv_file = f"{strategy_name}_{now}_equities.csv"
|
|
377
|
+
png_file = f"{strategy_name}_{now}_returns_heatmap.png"
|
|
378
|
+
html_file = f"{strategy_name}_{now}_report.html"
|
|
379
|
+
self.equity_curve.to_csv(results_dir / csv_file)
|
|
380
|
+
|
|
381
|
+
if self.print_stats:
|
|
382
|
+
plot_performance(self.equity_curve, self.strategy_name)
|
|
383
|
+
plot_returns_and_dd(self.equity_curve, self.benchmark, self.strategy_name)
|
|
384
|
+
qs.plots.monthly_heatmap(returns, savefig=f"{results_dir}/{png_file}")
|
|
385
|
+
plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
|
|
386
|
+
show_qs_stats(
|
|
387
|
+
returns,
|
|
388
|
+
self.benchmark,
|
|
389
|
+
self.strategy_name,
|
|
390
|
+
save_dir=f"{results_dir}/{html_file}",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
return stats
|