bbstrader 0.1.9__py3-none-any.whl → 0.1.91__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.
- bbstrader/__ini__.py +4 -2
- bbstrader/btengine/__init__.py +5 -5
- bbstrader/btengine/backtest.py +51 -10
- bbstrader/btengine/data.py +147 -55
- bbstrader/btengine/event.py +4 -1
- bbstrader/btengine/execution.py +125 -23
- bbstrader/btengine/performance.py +4 -7
- bbstrader/btengine/portfolio.py +34 -13
- bbstrader/btengine/strategy.py +466 -6
- bbstrader/config.py +111 -0
- bbstrader/metatrader/__init__.py +4 -4
- bbstrader/metatrader/account.py +348 -53
- bbstrader/metatrader/rates.py +232 -27
- bbstrader/metatrader/risk.py +34 -23
- bbstrader/metatrader/trade.py +321 -165
- bbstrader/metatrader/utils.py +2 -53
- bbstrader/models/factors.py +0 -0
- bbstrader/models/ml.py +0 -0
- bbstrader/models/optimization.py +0 -0
- bbstrader/trading/__init__.py +1 -1
- bbstrader/trading/execution.py +268 -164
- bbstrader/trading/scripts.py +57 -0
- bbstrader/trading/strategies.py +41 -65
- bbstrader/tseries.py +274 -39
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/METADATA +11 -3
- bbstrader-0.1.91.dist-info/RECORD +31 -0
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/WHEEL +1 -1
- bbstrader-0.1.9.dist-info/RECORD +0 -26
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/top_level.txt +0 -0
bbstrader/btengine/execution.py
CHANGED
|
@@ -2,15 +2,16 @@ from datetime import datetime
|
|
|
2
2
|
from queue import Queue
|
|
3
3
|
from abc import ABCMeta, abstractmethod
|
|
4
4
|
from bbstrader.btengine.event import FillEvent, OrderEvent
|
|
5
|
+
from bbstrader.btengine.data import DataHandler
|
|
5
6
|
from bbstrader.metatrader.account import Account
|
|
7
|
+
from bbstrader.config import config_logger
|
|
6
8
|
|
|
7
9
|
__all__ = [
|
|
8
10
|
"ExecutionHandler",
|
|
9
|
-
"
|
|
11
|
+
"SimExecutionHandler",
|
|
10
12
|
"MT5ExecutionHandler"
|
|
11
13
|
]
|
|
12
14
|
|
|
13
|
-
|
|
14
15
|
class ExecutionHandler(metaclass=ABCMeta):
|
|
15
16
|
"""
|
|
16
17
|
The ExecutionHandler abstract class handles the interaction
|
|
@@ -42,7 +43,7 @@ class ExecutionHandler(metaclass=ABCMeta):
|
|
|
42
43
|
)
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
class
|
|
46
|
+
class SimExecutionHandler(ExecutionHandler):
|
|
46
47
|
"""
|
|
47
48
|
The simulated execution handler simply converts all order
|
|
48
49
|
objects into their equivalent fill objects automatically
|
|
@@ -53,7 +54,7 @@ class SimulatedExecutionHandler(ExecutionHandler):
|
|
|
53
54
|
handler.
|
|
54
55
|
"""
|
|
55
56
|
|
|
56
|
-
def __init__(self, events: Queue, **kwargs):
|
|
57
|
+
def __init__(self, events: Queue, data: DataHandler, **kwargs):
|
|
57
58
|
"""
|
|
58
59
|
Initialises the handler, setting the event queues
|
|
59
60
|
up internally.
|
|
@@ -62,6 +63,8 @@ class SimulatedExecutionHandler(ExecutionHandler):
|
|
|
62
63
|
events (Queue): The Queue of Event objects.
|
|
63
64
|
"""
|
|
64
65
|
self.events = events
|
|
66
|
+
self.bardata = data
|
|
67
|
+
self.logger = kwargs.get("logger", config_logger("execution.log"))
|
|
65
68
|
|
|
66
69
|
def execute_order(self, event: OrderEvent):
|
|
67
70
|
"""
|
|
@@ -72,51 +75,145 @@ class SimulatedExecutionHandler(ExecutionHandler):
|
|
|
72
75
|
event (OrderEvent): Contains an Event object with order information.
|
|
73
76
|
"""
|
|
74
77
|
if event.type == 'ORDER':
|
|
78
|
+
dtime = self.bardata.get_latest_bar_datetime(event.symbol)
|
|
75
79
|
fill_event = FillEvent(
|
|
76
|
-
|
|
80
|
+
dtime, event.symbol,
|
|
77
81
|
'ARCA', event.quantity, event.direction, None
|
|
78
82
|
)
|
|
79
83
|
self.events.put(fill_event)
|
|
84
|
+
self.logger.info(
|
|
85
|
+
f"{event.direction} ORDER FILLED: SYMBOL={event.symbol}, "
|
|
86
|
+
f"QUANTITY={event.quantity}, PRICE @{event.price} EXCHANGE={fill_event.exchange}",
|
|
87
|
+
custom_time=fill_event.timeindex
|
|
88
|
+
)
|
|
80
89
|
|
|
81
90
|
|
|
82
91
|
class MT5ExecutionHandler(ExecutionHandler):
|
|
83
|
-
|
|
92
|
+
"""
|
|
93
|
+
The main role of `MT5ExecutionHandler` class is to estimate the execution fees
|
|
94
|
+
for different asset classes on the MT5 terminal.
|
|
95
|
+
|
|
96
|
+
Generally we have four types of fees when we execute trades using the MT5 terminal
|
|
97
|
+
(commissions, swap, spread and other fees). But most of these fees depend on the specifications
|
|
98
|
+
of each instrument and the duration of the transaction for the swap for example.
|
|
99
|
+
|
|
100
|
+
Calculating the exact fees for each instrument would be a bit complex because our Backtest engine
|
|
101
|
+
and the Portfolio class do not take into account the duration of each trade to apply the appropriate
|
|
102
|
+
rate for the swap for example. So we have to use only the model of calculating the commissions
|
|
103
|
+
for each asset class and each instrument.
|
|
104
|
+
|
|
105
|
+
The second thing that must be taken into account on MT5 is the type of account offered by the broker.
|
|
106
|
+
Brokers have different account categories each with its specifications for each asset class and each instrument.
|
|
107
|
+
Again considering all these conditions would make our class very complex. So we took the `Raw Spread`
|
|
108
|
+
account fee calculation model from [Just Market](https://one.justmarkets.link/a/tufvj0xugm/registration/trader)
|
|
109
|
+
for indicies, forex, commodities and crypto. We used the [Admiral Market](https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets)
|
|
110
|
+
account fee calculation model from `Trade.MT5` account type for stocks and ETFs.
|
|
111
|
+
|
|
112
|
+
NOTE:
|
|
113
|
+
This class only works with `bbstrader.metatrader.data.MT5DataHandler` class.
|
|
114
|
+
"""
|
|
115
|
+
def __init__(self, events: Queue, data: DataHandler, **kwargs):
|
|
84
116
|
"""
|
|
117
|
+
Initialises the handler, setting the event queues up internally.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
events (Queue): The Queue of Event objects.
|
|
85
121
|
"""
|
|
86
122
|
self.events = events
|
|
87
|
-
self.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# TODO: Implement the calculation of fees based on the broker's fees
|
|
91
|
-
# https://www.metatrader5.com/en/terminal/help/trading/market_watch
|
|
92
|
-
# Calculate fees based on the broker's fees , swap and commission
|
|
93
|
-
return 0.0
|
|
123
|
+
self.bardata = data
|
|
124
|
+
self.logger = kwargs.get("logger", config_logger("execution.log"))
|
|
125
|
+
self.__account = Account()
|
|
94
126
|
|
|
95
127
|
def _calculate_lot(self, symbol, quantity, price):
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
FUT = self.account.get_symbol_type(symbol) == 'FUT'
|
|
99
|
-
CRYPTO = self.account.get_symbol_type(symbol) == 'CRYPTO'
|
|
100
|
-
symbol_info = self.account.get_symbol_info(symbol)
|
|
128
|
+
symbol_type = self.__account.get_symbol_type(symbol)
|
|
129
|
+
symbol_info = self.__account.get_symbol_info(symbol)
|
|
101
130
|
contract_size = symbol_info.trade_contract_size
|
|
102
131
|
|
|
103
132
|
lot = (quantity*price) / (contract_size * price)
|
|
104
133
|
if contract_size == 1:
|
|
105
134
|
lot = quantity
|
|
106
|
-
if
|
|
135
|
+
if symbol_type in [
|
|
136
|
+
'COMD', 'FUT', 'CRYPTO'] and contract_size > 1:
|
|
107
137
|
lot = quantity / contract_size
|
|
108
|
-
if FX:
|
|
138
|
+
if symbol_type == 'FX':
|
|
109
139
|
lot = (quantity*price / contract_size)
|
|
110
140
|
return self._check_lot(symbol, lot)
|
|
111
141
|
|
|
112
142
|
def _check_lot(self, symbol, lot):
|
|
113
|
-
symbol_info =
|
|
143
|
+
symbol_info = self.__account.get_symbol_info(symbol)
|
|
114
144
|
if lot < symbol_info.volume_min:
|
|
115
145
|
return symbol_info.volume_min
|
|
116
146
|
elif lot > symbol_info.volume_max:
|
|
117
147
|
return symbol_info.volume_max
|
|
118
148
|
return round(lot, 2)
|
|
119
149
|
|
|
150
|
+
def _estimate_total_fees(self, symbol, lot, qty, price):
|
|
151
|
+
symbol_type = self.__account.get_symbol_type(symbol)
|
|
152
|
+
if symbol_type in ['STK', 'ETF']:
|
|
153
|
+
return self._estimate_stock_commission(symbol, qty, price)
|
|
154
|
+
elif symbol_type == 'FX':
|
|
155
|
+
return self._estimate_forex_commission(lot)
|
|
156
|
+
elif symbol_type == 'COMD':
|
|
157
|
+
return self._estimate_commodity_commission(lot)
|
|
158
|
+
elif symbol_type == 'IDX':
|
|
159
|
+
return self._estimate_index_commission(lot)
|
|
160
|
+
elif symbol_type == 'FUT':
|
|
161
|
+
return self._estimate_futures_commission()
|
|
162
|
+
elif symbol_type == 'CRYPTO':
|
|
163
|
+
return self._estimate_crypto_commission()
|
|
164
|
+
else:
|
|
165
|
+
return 0.0
|
|
166
|
+
|
|
167
|
+
def _estimate_stock_commission(self, symbol, qty, price):
|
|
168
|
+
# https://admiralmarkets.com/start-trading/contract-specifications?regulator=jsc
|
|
169
|
+
min_com = 1.0
|
|
170
|
+
min_aud = 8.0
|
|
171
|
+
min_dkk = 30.0
|
|
172
|
+
min_nok = min_sek = 10.0
|
|
173
|
+
us_com = 0.02 # per chare
|
|
174
|
+
ger_fr_uk_cm = 0.001 # percent
|
|
175
|
+
eu_asia_cm = 0.0015 # percent
|
|
176
|
+
if (
|
|
177
|
+
symbol in self.__account.get_stocks_from_country('USA')
|
|
178
|
+
or self.__account.get_symbol_type(symbol) == 'ETF'
|
|
179
|
+
and self.__account.get_currency_rates(symbol)['mc'] == 'USD'
|
|
180
|
+
):
|
|
181
|
+
return max(min_com, qty * us_com)
|
|
182
|
+
elif (
|
|
183
|
+
symbol in self.__account.get_stocks_from_country('GBR')
|
|
184
|
+
or symbol in self.__account.get_stocks_from_country('FRA')
|
|
185
|
+
or symbol in self.__account.get_stocks_from_country('DEU')
|
|
186
|
+
or self.__account.get_symbol_type(symbol) == 'ETF'
|
|
187
|
+
and self.__account.get_currency_rates(symbol)['mc'] in ['GBP', 'EUR']
|
|
188
|
+
):
|
|
189
|
+
return max(min_com, qty*price*ger_fr_uk_cm)
|
|
190
|
+
else:
|
|
191
|
+
if self.__account.get_currency_rates(symbol)['mc'] == 'AUD':
|
|
192
|
+
return max(min_aud, qty*price*eu_asia_cm)
|
|
193
|
+
elif self.__account.get_currency_rates(symbol)['mc'] == 'DKK':
|
|
194
|
+
return max(min_dkk, qty*price*eu_asia_cm)
|
|
195
|
+
elif self.__account.get_currency_rates(symbol)['mc'] == 'NOK':
|
|
196
|
+
return max(min_nok, qty*price*eu_asia_cm)
|
|
197
|
+
elif self.__account.get_currency_rates(symbol)['mc'] == 'SEK':
|
|
198
|
+
return max(min_sek, qty*price*eu_asia_cm)
|
|
199
|
+
else:
|
|
200
|
+
return max(min_com, qty*price*eu_asia_cm)
|
|
201
|
+
|
|
202
|
+
def _estimate_forex_commission(self, lot):
|
|
203
|
+
return 3.0 * lot
|
|
204
|
+
|
|
205
|
+
def _estimate_commodity_commission(self, lot):
|
|
206
|
+
return 3.0 * lot
|
|
207
|
+
|
|
208
|
+
def _estimate_index_commission(self, lot):
|
|
209
|
+
return 0.25 * lot
|
|
210
|
+
|
|
211
|
+
def _estimate_futures_commission(self):
|
|
212
|
+
return 0.0
|
|
213
|
+
|
|
214
|
+
def _estimate_crypto_commission(self):
|
|
215
|
+
return 0.0
|
|
216
|
+
|
|
120
217
|
def execute_order(self, event: OrderEvent):
|
|
121
218
|
"""
|
|
122
219
|
Executes an Order event by converting it into a Fill event.
|
|
@@ -130,13 +227,18 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
130
227
|
quantity = event.quantity
|
|
131
228
|
price = event.price
|
|
132
229
|
lot = self._calculate_lot(symbol, quantity, price)
|
|
133
|
-
fees = self._estimate_total_fees(symbol, lot)
|
|
230
|
+
fees = self._estimate_total_fees(symbol, lot, quantity, price)
|
|
231
|
+
dtime = self.bardata.get_latest_bar_datetime(symbol)
|
|
134
232
|
fill_event = FillEvent(
|
|
135
|
-
timeindex=
|
|
233
|
+
timeindex=dtime, symbol=symbol,
|
|
136
234
|
exchange='MT5', quantity=quantity, direction=direction,
|
|
137
235
|
fill_cost=None, commission=fees
|
|
138
236
|
)
|
|
139
237
|
self.events.put(fill_event)
|
|
238
|
+
self.logger.info(
|
|
239
|
+
f"{direction} ORDER FILLED: SYMBOL={symbol}, QUANTITY={quantity}, "
|
|
240
|
+
f"PRICE @{price} EXCHANGE={fill_event.exchange}", custom_time=fill_event.timeindex
|
|
241
|
+
)
|
|
140
242
|
|
|
141
243
|
|
|
142
244
|
class IBExecutionHandler(ExecutionHandler):
|
|
@@ -2,15 +2,11 @@ import numpy as np
|
|
|
2
2
|
import pandas as pd
|
|
3
3
|
import seaborn as sns
|
|
4
4
|
import yfinance as yf
|
|
5
|
-
|
|
6
5
|
from scipy.stats import mstats
|
|
7
6
|
import matplotlib.pyplot as plt
|
|
8
7
|
from matplotlib.ticker import MaxNLocator
|
|
9
8
|
import quantstats as qs
|
|
10
9
|
|
|
11
|
-
import warnings
|
|
12
|
-
warnings.filterwarnings("ignore")
|
|
13
|
-
warnings.simplefilter(action='ignore', category=FutureWarning)
|
|
14
10
|
sns.set_theme()
|
|
15
11
|
|
|
16
12
|
__all__ = [
|
|
@@ -53,6 +49,7 @@ def create_sortino_ratio(returns, periods=252) -> float:
|
|
|
53
49
|
"""
|
|
54
50
|
return qs.stats.sortino(returns, periods=periods)
|
|
55
51
|
|
|
52
|
+
|
|
56
53
|
def create_drawdowns(pnl):
|
|
57
54
|
"""
|
|
58
55
|
Calculate the largest peak-to-trough drawdown of the PnL curve
|
|
@@ -138,7 +135,7 @@ def plot_performance(df, title):
|
|
|
138
135
|
plt.show()
|
|
139
136
|
|
|
140
137
|
|
|
141
|
-
def plot_returns_and_dd(df, benchmark: str, title):
|
|
138
|
+
def plot_returns_and_dd(df: pd.DataFrame, benchmark: str, title):
|
|
142
139
|
"""
|
|
143
140
|
Plot the returns and drawdowns of the strategy
|
|
144
141
|
compared to a benchmark.
|
|
@@ -216,7 +213,7 @@ def plot_returns_and_dd(df, benchmark: str, title):
|
|
|
216
213
|
plt.show()
|
|
217
214
|
|
|
218
215
|
|
|
219
|
-
def plot_monthly_yearly_returns(df, title):
|
|
216
|
+
def plot_monthly_yearly_returns(df:pd.DataFrame, title):
|
|
220
217
|
"""
|
|
221
218
|
Plot the monthly and yearly returns of the strategy.
|
|
222
219
|
|
|
@@ -253,7 +250,7 @@ def plot_monthly_yearly_returns(df, title):
|
|
|
253
250
|
|
|
254
251
|
# Calculate and prepare yearly returns DataFrame
|
|
255
252
|
yearly_returns_df = equity_df['Total'].resample(
|
|
256
|
-
'
|
|
253
|
+
'A').last().pct_change().to_frame(name='Yearly Returns') * 100
|
|
257
254
|
|
|
258
255
|
# Set the aesthetics for the plots
|
|
259
256
|
sns.set_theme(style="darkgrid")
|
bbstrader/btengine/portfolio.py
CHANGED
|
@@ -3,17 +3,30 @@ from queue import Queue
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from bbstrader.btengine.event import (
|
|
6
|
-
OrderEvent,
|
|
6
|
+
OrderEvent,
|
|
7
|
+
FillEvent,
|
|
8
|
+
MarketEvent,
|
|
9
|
+
SignalEvent
|
|
7
10
|
)
|
|
8
11
|
from bbstrader.btengine.data import DataHandler
|
|
9
12
|
from bbstrader.btengine.performance import (
|
|
10
|
-
create_drawdowns,
|
|
11
|
-
|
|
13
|
+
create_drawdowns,
|
|
14
|
+
create_sharpe_ratio,
|
|
15
|
+
create_sortino_ratio,
|
|
16
|
+
plot_performance,
|
|
17
|
+
show_qs_stats,
|
|
18
|
+
plot_returns_and_dd,
|
|
12
19
|
plot_monthly_yearly_returns
|
|
13
20
|
)
|
|
21
|
+
from bbstrader.config import BBSTRADER_DIR
|
|
14
22
|
import quantstats as qs
|
|
15
23
|
|
|
16
24
|
|
|
25
|
+
__all__ = [
|
|
26
|
+
'Portfolio',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
17
30
|
class Portfolio(object):
|
|
18
31
|
"""
|
|
19
32
|
This describes a `Portfolio()` object that keeps track of the positions
|
|
@@ -97,7 +110,9 @@ class Portfolio(object):
|
|
|
97
110
|
self.timeframe = kwargs.get("time_frame", "D1")
|
|
98
111
|
self.trading_hours = kwargs.get("session_duration", 6.5)
|
|
99
112
|
self.benchmark = kwargs.get('benchmark', 'SPY')
|
|
113
|
+
self.output_dir = kwargs.get('output_dir', None)
|
|
100
114
|
self.strategy_name = kwargs.get('strategy_name', '')
|
|
115
|
+
self.print_stats = kwargs.get('print_stats', True)
|
|
101
116
|
if self.timeframe not in self._tf_mapping():
|
|
102
117
|
raise ValueError(
|
|
103
118
|
f"Timeframe not supported,"
|
|
@@ -112,6 +127,7 @@ class Portfolio(object):
|
|
|
112
127
|
[(s, 0) for s in self.symbol_list])
|
|
113
128
|
self.all_holdings = self.construct_all_holdings()
|
|
114
129
|
self.current_holdings = self.construct_current_holdings()
|
|
130
|
+
self.equity_curve = None
|
|
115
131
|
|
|
116
132
|
def _tf_mapping(self):
|
|
117
133
|
"""
|
|
@@ -188,7 +204,7 @@ class Portfolio(object):
|
|
|
188
204
|
for s in self.symbol_list:
|
|
189
205
|
# Approximation to the real value
|
|
190
206
|
market_value = self.current_positions[s] * \
|
|
191
|
-
self.bars.get_latest_bar_value(s, "
|
|
207
|
+
self.bars.get_latest_bar_value(s, "adj_close")
|
|
192
208
|
dh[s] = market_value
|
|
193
209
|
dh['Total'] += market_value
|
|
194
210
|
|
|
@@ -230,7 +246,7 @@ class Portfolio(object):
|
|
|
230
246
|
|
|
231
247
|
# Update holdings list with new quantities
|
|
232
248
|
price = self.bars.get_latest_bar_value(
|
|
233
|
-
fill.symbol, "
|
|
249
|
+
fill.symbol, "adj_close"
|
|
234
250
|
)
|
|
235
251
|
cost = fill_dir * price * fill.quantity
|
|
236
252
|
self.current_holdings[fill.symbol] += cost
|
|
@@ -295,6 +311,7 @@ class Portfolio(object):
|
|
|
295
311
|
list of dictionaries.
|
|
296
312
|
"""
|
|
297
313
|
curve = pd.DataFrame(self.all_holdings)
|
|
314
|
+
curve['Datetime'] = pd.to_datetime(curve['Datetime'], utc=True)
|
|
298
315
|
curve.set_index('Datetime', inplace=True)
|
|
299
316
|
curve['Returns'] = curve['Total'].pct_change(fill_method=None)
|
|
300
317
|
curve['Equity Curve'] = (1.0+curve['Returns']).cumprod()
|
|
@@ -325,20 +342,24 @@ class Portfolio(object):
|
|
|
325
342
|
]
|
|
326
343
|
now = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
327
344
|
strategy_name = self.strategy_name.replace(' ', '_')
|
|
328
|
-
|
|
345
|
+
if self.output_dir:
|
|
346
|
+
results_dir = Path(self.output_dir) / strategy_name
|
|
347
|
+
else:
|
|
348
|
+
results_dir = Path('.backtests') / strategy_name
|
|
329
349
|
results_dir.mkdir(parents=True, exist_ok=True)
|
|
330
350
|
|
|
331
|
-
csv_file = f"{strategy_name}_{now}
|
|
351
|
+
csv_file = f"{strategy_name}_{now}_equities.csv"
|
|
332
352
|
png_file = f'{strategy_name}_{now}_returns_heatmap.png'
|
|
333
353
|
html_file = f"{strategy_name}_{now}_report.html"
|
|
334
354
|
self.equity_curve.to_csv(results_dir / csv_file)
|
|
335
355
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
356
|
+
if self.print_stats:
|
|
357
|
+
plot_performance(self.equity_curve, self.strategy_name)
|
|
358
|
+
plot_returns_and_dd(self.equity_curve, self.benchmark, self.strategy_name)
|
|
359
|
+
qs.plots.monthly_heatmap(returns, savefig=f"{results_dir}/{png_file}")
|
|
360
|
+
plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
|
|
361
|
+
show_qs_stats(returns, self.benchmark, self.strategy_name,
|
|
362
|
+
save_dir=f"{results_dir}/{html_file}")
|
|
342
363
|
|
|
343
364
|
return stats
|
|
344
365
|
|