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.

@@ -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
- "SimulatedExecutionHandler",
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 SimulatedExecutionHandler(ExecutionHandler):
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
- datetime.now(), event.symbol,
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
- def __init__(self, events: Queue, **kwargs):
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.account = Account()
88
-
89
- def _estimate_total_fees(self, symbol, lot):
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
- FX = self.account.get_symbol_type(symbol) == 'FX'
97
- COMD = self.account.get_symbol_type(symbol) == 'COMD'
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 COMD or FUT or CRYPTO and contract_size > 1:
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 = self.account.get_symbol_info(symbol)
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=datetime.now(), symbol=symbol,
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
- 'YE').last().pct_change().to_frame(name='Yearly Returns') * 100
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")
@@ -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, FillEvent, MarketEvent, SignalEvent
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, create_sharpe_ratio, create_sortino_ratio,
11
- plot_performance, show_qs_stats,plot_returns_and_dd,
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, "Adj Close")
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, "Adj Close"
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
- results_dir = Path('.backtests') / strategy_name
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}_equity.csv"
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
- plot_performance(self.equity_curve, self.strategy_name)
337
- plot_returns_and_dd(self.equity_curve, self.benchmark, self.strategy_name)
338
- qs.plots.monthly_heatmap(returns, savefig=f"{results_dir}/{png_file}")
339
- plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
340
- show_qs_stats(returns, self.benchmark, self.strategy_name,
341
- save_dir=f"{results_dir}/{html_file}")
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