bbstrader 0.2.0__py3-none-any.whl → 0.2.1__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 +7 -7
- bbstrader/btengine/__init__.py +7 -7
- bbstrader/btengine/backtest.py +30 -26
- bbstrader/btengine/data.py +92 -81
- bbstrader/btengine/event.py +2 -1
- bbstrader/btengine/execution.py +18 -16
- bbstrader/btengine/performance.py +11 -7
- bbstrader/btengine/portfolio.py +35 -36
- bbstrader/btengine/strategy.py +113 -92
- bbstrader/config.py +12 -10
- bbstrader/core/data.py +4 -5
- bbstrader/core/utils.py +57 -0
- bbstrader/ibkr/utils.py +0 -0
- bbstrader/metatrader/__init__.py +5 -5
- bbstrader/metatrader/account.py +117 -121
- bbstrader/metatrader/rates.py +81 -78
- bbstrader/metatrader/risk.py +23 -37
- bbstrader/metatrader/trade.py +154 -138
- bbstrader/metatrader/utils.py +3 -3
- bbstrader/models/__init__.py +5 -5
- bbstrader/models/factors.py +17 -12
- bbstrader/models/ml.py +371 -305
- bbstrader/models/optimization.py +14 -12
- bbstrader/models/portfolio.py +44 -35
- bbstrader/models/risk.py +15 -9
- bbstrader/trading/__init__.py +2 -2
- bbstrader/trading/execution.py +245 -179
- bbstrader/trading/scripts.py +8 -4
- bbstrader/trading/strategies.py +78 -65
- bbstrader/tseries.py +124 -98
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/LICENSE +1 -1
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/METADATA +2 -1
- bbstrader-0.2.1.dist-info/RECORD +37 -0
- bbstrader-0.2.0.dist-info/RECORD +0 -36
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/WHEEL +0 -0
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/top_level.txt +0 -0
bbstrader/btengine/execution.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
from queue import Queue
|
|
3
1
|
from abc import ABCMeta, abstractmethod
|
|
4
|
-
from
|
|
2
|
+
from queue import Queue
|
|
3
|
+
|
|
5
4
|
from bbstrader.btengine.data import DataHandler
|
|
5
|
+
from bbstrader.btengine.event import FillEvent, OrderEvent
|
|
6
6
|
from bbstrader.metatrader.account import Account
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
@@ -11,6 +11,7 @@ __all__ = [
|
|
|
11
11
|
"MT5ExecutionHandler"
|
|
12
12
|
]
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
class ExecutionHandler(metaclass=ABCMeta):
|
|
15
16
|
"""
|
|
16
17
|
The ExecutionHandler abstract class handles the interaction
|
|
@@ -84,7 +85,7 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
84
85
|
self.events.put(fill_event)
|
|
85
86
|
self.logger.info(
|
|
86
87
|
f"{event.direction} ORDER FILLED: SYMBOL={event.symbol}, "
|
|
87
|
-
f"QUANTITY={event.quantity}, PRICE @{event.price} EXCHANGE={fill_event.exchange}",
|
|
88
|
+
f"QUANTITY={event.quantity}, PRICE @{event.price} EXCHANGE={fill_event.exchange}",
|
|
88
89
|
custom_time=fill_event.timeindex
|
|
89
90
|
)
|
|
90
91
|
|
|
@@ -93,16 +94,16 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
93
94
|
"""
|
|
94
95
|
The main role of `MT5ExecutionHandler` class is to estimate the execution fees
|
|
95
96
|
for different asset classes on the MT5 terminal.
|
|
96
|
-
|
|
97
|
+
|
|
97
98
|
Generally we have four types of fees when we execute trades using the MT5 terminal
|
|
98
99
|
(commissions, swap, spread and other fees). But most of these fees depend on the specifications
|
|
99
100
|
of each instrument and the duration of the transaction for the swap for example.
|
|
100
|
-
|
|
101
|
+
|
|
101
102
|
Calculating the exact fees for each instrument would be a bit complex because our Backtest engine
|
|
102
103
|
and the Portfolio class do not take into account the duration of each trade to apply the appropriate
|
|
103
104
|
rate for the swap for example. So we have to use only the model of calculating the commissions
|
|
104
105
|
for each asset class and each instrument.
|
|
105
|
-
|
|
106
|
+
|
|
106
107
|
The second thing that must be taken into account on MT5 is the type of account offered by the broker.
|
|
107
108
|
Brokers have different account categories each with its specifications for each asset class and each instrument.
|
|
108
109
|
Again considering all these conditions would make our class very complex. So we took the `Raw Spread`
|
|
@@ -113,6 +114,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
113
114
|
NOTE:
|
|
114
115
|
This class only works with `bbstrader.metatrader.data.MT5DataHandler` class.
|
|
115
116
|
"""
|
|
117
|
+
|
|
116
118
|
def __init__(self, events: Queue, data: DataHandler, **kwargs):
|
|
117
119
|
"""
|
|
118
120
|
Initialises the handler, setting the event queues up internally.
|
|
@@ -134,8 +136,8 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
134
136
|
if contract_size == 1:
|
|
135
137
|
lot = quantity
|
|
136
138
|
if symbol_type in [
|
|
137
|
-
|
|
138
|
-
lot = quantity
|
|
139
|
+
'COMD', 'FUT', 'CRYPTO'] and contract_size > 1:
|
|
140
|
+
lot = quantity / contract_size
|
|
139
141
|
if symbol_type == 'FX':
|
|
140
142
|
lot = (quantity*price / contract_size)
|
|
141
143
|
return self._check_lot(symbol, lot)
|
|
@@ -147,7 +149,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
147
149
|
elif lot > symbol_info.volume_max:
|
|
148
150
|
return symbol_info.volume_max
|
|
149
151
|
return round(lot, 2)
|
|
150
|
-
|
|
152
|
+
|
|
151
153
|
def _estimate_total_fees(self, symbol, lot, qty, price):
|
|
152
154
|
symbol_type = self.__account.get_symbol_type(symbol)
|
|
153
155
|
if symbol_type in ['STK', 'ETF']:
|
|
@@ -164,16 +166,16 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
164
166
|
return self._estimate_crypto_commission()
|
|
165
167
|
else:
|
|
166
168
|
return 0.0
|
|
167
|
-
|
|
169
|
+
|
|
168
170
|
def _estimate_stock_commission(self, symbol, qty, price):
|
|
169
171
|
# https://admiralmarkets.com/start-trading/contract-specifications?regulator=jsc
|
|
170
172
|
min_com = 1.0
|
|
171
173
|
min_aud = 8.0
|
|
172
174
|
min_dkk = 30.0
|
|
173
175
|
min_nok = min_sek = 10.0
|
|
174
|
-
us_com = 0.02
|
|
175
|
-
ger_fr_uk_cm = 0.001
|
|
176
|
-
eu_asia_cm = 0.0015
|
|
176
|
+
us_com = 0.02 # per chare
|
|
177
|
+
ger_fr_uk_cm = 0.001 # percent
|
|
178
|
+
eu_asia_cm = 0.0015 # percent
|
|
177
179
|
if (
|
|
178
180
|
symbol in self.__account.get_stocks_from_country('USA')
|
|
179
181
|
or self.__account.get_symbol_type(symbol) == 'ETF'
|
|
@@ -214,7 +216,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
214
216
|
|
|
215
217
|
def _estimate_crypto_commission(self):
|
|
216
218
|
return 0.0
|
|
217
|
-
|
|
219
|
+
|
|
218
220
|
def execute_order(self, event: OrderEvent):
|
|
219
221
|
"""
|
|
220
222
|
Executes an Order event by converting it into a Fill event.
|
|
@@ -243,4 +245,4 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
243
245
|
|
|
244
246
|
|
|
245
247
|
class IBExecutionHandler(ExecutionHandler):
|
|
246
|
-
...
|
|
248
|
+
...
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
1
4
|
import numpy as np
|
|
2
5
|
import pandas as pd
|
|
6
|
+
import quantstats as qs
|
|
3
7
|
import seaborn as sns
|
|
4
8
|
import yfinance as yf
|
|
5
|
-
|
|
6
|
-
import matplotlib.pyplot as plt
|
|
7
|
-
from matplotlib.ticker import MaxNLocator
|
|
8
|
-
import quantstats as qs
|
|
9
|
-
import warnings
|
|
9
|
+
|
|
10
10
|
warnings.filterwarnings("ignore")
|
|
11
11
|
|
|
12
12
|
sns.set_theme()
|
|
@@ -37,6 +37,8 @@ def create_sharpe_ratio(returns, periods=252) -> float:
|
|
|
37
37
|
return qs.stats.sharpe(returns, periods=periods)
|
|
38
38
|
|
|
39
39
|
# Define a function to calculate the Sortino Ratio
|
|
40
|
+
|
|
41
|
+
|
|
40
42
|
def create_sortino_ratio(returns, periods=252) -> float:
|
|
41
43
|
"""
|
|
42
44
|
Create the Sortino ratio for the strategy, based on a
|
|
@@ -215,7 +217,7 @@ def plot_returns_and_dd(df: pd.DataFrame, benchmark: str, title):
|
|
|
215
217
|
plt.show()
|
|
216
218
|
|
|
217
219
|
|
|
218
|
-
def plot_monthly_yearly_returns(df:pd.DataFrame, title):
|
|
220
|
+
def plot_monthly_yearly_returns(df: pd.DataFrame, title):
|
|
219
221
|
"""
|
|
220
222
|
Plot the monthly and yearly returns of the strategy.
|
|
221
223
|
|
|
@@ -295,6 +297,7 @@ def plot_monthly_yearly_returns(df:pd.DataFrame, title):
|
|
|
295
297
|
# Show the plot
|
|
296
298
|
plt.show()
|
|
297
299
|
|
|
300
|
+
|
|
298
301
|
def show_qs_stats(returns, benchmark, strategy_name, save_dir=None):
|
|
299
302
|
"""
|
|
300
303
|
Generate the full quantstats report for the strategy.
|
|
@@ -317,4 +320,5 @@ def show_qs_stats(returns, benchmark, strategy_name, save_dir=None):
|
|
|
317
320
|
|
|
318
321
|
# Generate the full report with a benchmark
|
|
319
322
|
qs.reports.full(returns, mode='full', benchmark=benchmark)
|
|
320
|
-
qs.reports.html(returns, benchmark=benchmark,
|
|
323
|
+
qs.reports.html(returns, benchmark=benchmark,
|
|
324
|
+
output=save_dir, title=strategy_name)
|
bbstrader/btengine/portfolio.py
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
|
-
import pandas as pd
|
|
2
|
-
from queue import Queue
|
|
3
|
-
from pathlib import Path
|
|
4
1
|
from datetime import datetime
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from queue import Queue
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import quantstats as qs
|
|
7
|
+
|
|
11
8
|
from bbstrader.btengine.data import DataHandler
|
|
9
|
+
from bbstrader.btengine.event import FillEvent, MarketEvent, OrderEvent, SignalEvent
|
|
12
10
|
from bbstrader.btengine.performance import (
|
|
13
|
-
create_drawdowns,
|
|
14
|
-
create_sharpe_ratio,
|
|
15
|
-
create_sortino_ratio,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
plot_returns_and_dd,
|
|
19
|
-
|
|
11
|
+
create_drawdowns,
|
|
12
|
+
create_sharpe_ratio,
|
|
13
|
+
create_sortino_ratio,
|
|
14
|
+
plot_monthly_yearly_returns,
|
|
15
|
+
plot_performance,
|
|
16
|
+
plot_returns_and_dd,
|
|
17
|
+
show_qs_stats,
|
|
20
18
|
)
|
|
21
|
-
from bbstrader.config import BBSTRADER_DIR
|
|
22
|
-
import quantstats as qs
|
|
23
|
-
|
|
24
19
|
|
|
25
20
|
__all__ = [
|
|
26
21
|
'Portfolio',
|
|
@@ -175,7 +170,7 @@ class Portfolio(object):
|
|
|
175
170
|
d['Commission'] = 0.0
|
|
176
171
|
d['Total'] = self.initial_capital
|
|
177
172
|
return d
|
|
178
|
-
|
|
173
|
+
|
|
179
174
|
def _get_price(self, symbol: str) -> float:
|
|
180
175
|
try:
|
|
181
176
|
price = self.bars.get_latest_bar_value(
|
|
@@ -190,7 +185,7 @@ class Portfolio(object):
|
|
|
190
185
|
return price
|
|
191
186
|
except AttributeError:
|
|
192
187
|
raise AttributeError(
|
|
193
|
-
|
|
188
|
+
"Bars object must have 'adj_close' or 'close' prices"
|
|
194
189
|
)
|
|
195
190
|
|
|
196
191
|
def update_timeindex(self, event: MarketEvent):
|
|
@@ -285,7 +280,7 @@ class Portfolio(object):
|
|
|
285
280
|
|
|
286
281
|
Args:
|
|
287
282
|
signal (SignalEvent): The tuple containing Signal information.
|
|
288
|
-
|
|
283
|
+
|
|
289
284
|
Returns:
|
|
290
285
|
OrderEvent: The OrderEvent to be executed.
|
|
291
286
|
"""
|
|
@@ -305,19 +300,22 @@ class Portfolio(object):
|
|
|
305
300
|
else:
|
|
306
301
|
order_type = direction
|
|
307
302
|
|
|
308
|
-
if
|
|
309
|
-
order = OrderEvent(symbol, order_type,
|
|
303
|
+
if direction == 'LONG' and new_quantity > 0:
|
|
304
|
+
order = OrderEvent(symbol, order_type,
|
|
305
|
+
new_quantity, 'BUY', price, direction)
|
|
310
306
|
if direction == 'SHORT' and new_quantity > 0:
|
|
311
|
-
order = OrderEvent(symbol, order_type,
|
|
307
|
+
order = OrderEvent(symbol, order_type,
|
|
308
|
+
new_quantity, 'SELL', price, direction)
|
|
312
309
|
|
|
313
310
|
if direction == 'EXIT' and cur_quantity > 0:
|
|
314
|
-
order = OrderEvent(symbol, order_type, abs(
|
|
311
|
+
order = OrderEvent(symbol, order_type, abs(
|
|
312
|
+
cur_quantity), 'SELL', price, direction)
|
|
315
313
|
if direction == 'EXIT' and cur_quantity < 0:
|
|
316
|
-
order = OrderEvent(symbol, order_type, abs(
|
|
314
|
+
order = OrderEvent(symbol, order_type, abs(
|
|
315
|
+
cur_quantity), 'BUY', price, direction)
|
|
317
316
|
|
|
318
317
|
return order
|
|
319
318
|
|
|
320
|
-
|
|
321
319
|
def update_signal(self, event: SignalEvent):
|
|
322
320
|
"""
|
|
323
321
|
Acts on a SignalEvent to generate new orders
|
|
@@ -369,19 +367,20 @@ class Portfolio(object):
|
|
|
369
367
|
else:
|
|
370
368
|
results_dir = Path('.backtests') / strategy_name
|
|
371
369
|
results_dir.mkdir(parents=True, exist_ok=True)
|
|
372
|
-
|
|
373
|
-
csv_file =
|
|
370
|
+
|
|
371
|
+
csv_file = f"{strategy_name}_{now}_equities.csv"
|
|
374
372
|
png_file = f'{strategy_name}_{now}_returns_heatmap.png'
|
|
375
373
|
html_file = f"{strategy_name}_{now}_report.html"
|
|
376
374
|
self.equity_curve.to_csv(results_dir / csv_file)
|
|
377
|
-
|
|
375
|
+
|
|
378
376
|
if self.print_stats:
|
|
379
377
|
plot_performance(self.equity_curve, self.strategy_name)
|
|
380
|
-
plot_returns_and_dd(self.equity_curve,
|
|
381
|
-
|
|
378
|
+
plot_returns_and_dd(self.equity_curve,
|
|
379
|
+
self.benchmark, self.strategy_name)
|
|
380
|
+
qs.plots.monthly_heatmap(
|
|
381
|
+
returns, savefig=f"{results_dir}/{png_file}")
|
|
382
382
|
plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
|
|
383
|
-
show_qs_stats(returns, self.benchmark, self.strategy_name,
|
|
384
|
-
|
|
383
|
+
show_qs_stats(returns, self.benchmark, self.strategy_name,
|
|
384
|
+
save_dir=f"{results_dir}/{html_file}")
|
|
385
385
|
|
|
386
386
|
return stats
|
|
387
|
-
|