bbstrader 0.1.6__py3-none-any.whl → 0.1.8__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.

@@ -360,10 +360,12 @@ class YFHistoricDataHandler(BaseCSVDataHandler):
360
360
 
361
361
 
362
362
  # TODO # Get data from EODHD
363
+ # https://eodhd.com/
363
364
  class EODHDHistoricDataHandler(BaseCSVDataHandler):
364
365
  ...
365
366
 
366
- # TODO # Get data from FinancialModelingPrep ()
367
+ # TODO # Get data from FMP using Financialtoolkit API
368
+ # https://github.com/bbalouki/FinanceToolkit
367
369
  class FMPHistoricDataHandler(BaseCSVDataHandler):
368
370
  ...
369
371
 
@@ -54,9 +54,10 @@ class SignalEvent(Event):
54
54
  strategy_id: int,
55
55
  symbol: str,
56
56
  datetime: datetime,
57
- signal_type: str,
57
+ signal_type: Literal['LONG', 'SHORT', 'EXIT'],
58
58
  quantity: int | float = 100,
59
- strength: int | float = 1.0
59
+ strength: int | float = 1.0,
60
+ price: int | float = None
60
61
  ):
61
62
  """
62
63
  Initialises the SignalEvent.
@@ -67,10 +68,11 @@ class SignalEvent(Event):
67
68
 
68
69
  symbol (str): The ticker symbol, e.g. 'GOOG'.
69
70
  datetime (datetime): The timestamp at which the signal was generated.
70
- signal_type (str): 'LONG' or 'SHORT'.
71
+ signal_type (str): 'LONG' or 'SHORT' or 'EXIT'.
71
72
  quantity (int | float): An optional integer (or float) representing the order size.
72
73
  strength (int | float): An adjustment factor "suggestion" used to scale
73
74
  quantity at the portfolio level. Useful for pairs strategies.
75
+ price (int | float): An optional price to be used when the signal is generated.
74
76
  """
75
77
  self.type = 'SIGNAL'
76
78
  self.strategy_id = strategy_id
@@ -79,6 +81,7 @@ class SignalEvent(Event):
79
81
  self.signal_type = signal_type
80
82
  self.quantity = quantity
81
83
  self.strength = strength
84
+ self.price = price
82
85
 
83
86
 
84
87
  class OrderEvent(Event):
@@ -96,9 +99,10 @@ class OrderEvent(Event):
96
99
 
97
100
  def __init__(self,
98
101
  symbol: str,
99
- order_type: str,
102
+ order_type: Literal['MKT', 'LMT'],
100
103
  quantity: int | float,
101
- direction: str
104
+ direction: Literal['BUY', 'SELL'],
105
+ price: int | float = None
102
106
  ):
103
107
  """
104
108
  Initialises the order type, setting whether it is
@@ -110,20 +114,22 @@ class OrderEvent(Event):
110
114
  order_type (str): 'MKT' or 'LMT' for Market or Limit.
111
115
  quantity (int | float): Non-negative number for quantity.
112
116
  direction (str): 'BUY' or 'SELL' for long or short.
117
+ price (int | float): The price at which to order.
113
118
  """
114
119
  self.type = 'ORDER'
115
120
  self.symbol = symbol
116
121
  self.order_type = order_type
117
122
  self.quantity = quantity
118
123
  self.direction = direction
124
+ self.price = price
119
125
 
120
126
  def print_order(self):
121
127
  """
122
128
  Outputs the values within the Order.
123
129
  """
124
130
  print(
125
- "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" %
126
- (self.symbol, self.order_type, self.quantity, self.direction)
131
+ "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s, Price=%s" %
132
+ (self.symbol, self.order_type, self.quantity, self.direction, self.price)
127
133
  )
128
134
 
129
135
 
@@ -151,7 +157,7 @@ class FillEvent(Event):
151
157
  symbol: str,
152
158
  exchange: str,
153
159
  quantity: int | float,
154
- direction: Literal['LONG', 'SHORT', 'EXIT'],
160
+ direction: Literal['BUY', 'SELL'],
155
161
  fill_cost: int | float | None,
156
162
  commission: float | None = None
157
163
  ):
@@ -170,7 +176,7 @@ class FillEvent(Event):
170
176
  exchange (str): The exchange where the order was filled.
171
177
  quantity (int | float): The filled quantity.
172
178
  direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
173
- fill_cost (int | float): The holdings value in dollars.
179
+ fill_cost (int | float): Price of the shares when filled.
174
180
  commission (float | None): An optional commission sent from IB.
175
181
  """
176
182
  self.type = 'FILL'
@@ -1,11 +1,13 @@
1
- import datetime
1
+ 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.metatrader.account import Account
5
6
 
6
7
  __all__ = [
7
8
  "ExecutionHandler",
8
- "SimulatedExecutionHandler"
9
+ "SimulatedExecutionHandler",
10
+ "MT5ExecutionHandler"
9
11
  ]
10
12
 
11
13
 
@@ -71,13 +73,71 @@ class SimulatedExecutionHandler(ExecutionHandler):
71
73
  """
72
74
  if event.type == 'ORDER':
73
75
  fill_event = FillEvent(
74
- datetime.datetime.now(), event.symbol,
76
+ datetime.now(), event.symbol,
75
77
  'ARCA', event.quantity, event.direction, None
76
78
  )
77
79
  self.events.put(fill_event)
78
80
 
79
- # TODO # Use in live execution
80
-
81
81
 
82
82
  class MT5ExecutionHandler(ExecutionHandler):
83
- ...
83
+ def __init__(self, events: Queue, **kwargs):
84
+ """
85
+ """
86
+ 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
94
+
95
+ 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)
101
+ contract_size = symbol_info.trade_contract_size
102
+
103
+ lot = (quantity*price) / (contract_size * price)
104
+ if contract_size == 1:
105
+ lot = quantity
106
+ if COMD or FUT or CRYPTO and contract_size > 1:
107
+ lot = quantity / contract_size
108
+ if FX:
109
+ lot = (quantity*price / contract_size)
110
+ return self._check_lot(symbol, lot)
111
+
112
+ def _check_lot(self, symbol, lot):
113
+ symbol_info = self.account.get_symbol_info(symbol)
114
+ if lot < symbol_info.volume_min:
115
+ return symbol_info.volume_min
116
+ elif lot > symbol_info.volume_max:
117
+ return symbol_info.volume_max
118
+ return round(lot, 2)
119
+
120
+ def execute_order(self, event: OrderEvent):
121
+ """
122
+ Executes an Order event by converting it into a Fill event.
123
+
124
+ Args:
125
+ event (OrderEvent): Contains an Event object with order information.
126
+ """
127
+ if event.type == 'ORDER':
128
+ symbol = event.symbol
129
+ direction = event.direction
130
+ quantity = event.quantity
131
+ price = event.price
132
+ lot = self._calculate_lot(symbol, quantity, price)
133
+ fees = self._estimate_total_fees(symbol, lot)
134
+ fill_event = FillEvent(
135
+ timeindex=datetime.now(), symbol=symbol,
136
+ exchange='MT5', quantity=quantity, direction=direction,
137
+ fill_cost=None, commission=fees
138
+ )
139
+ self.events.put(fill_event)
140
+
141
+
142
+ class IBExecutionHandler(ExecutionHandler):
143
+ ...
@@ -6,9 +6,11 @@ import yfinance as yf
6
6
  from scipy.stats import mstats
7
7
  import matplotlib.pyplot as plt
8
8
  from matplotlib.ticker import MaxNLocator
9
+ import quantstats as qs
9
10
 
10
11
  import warnings
11
12
  warnings.filterwarnings("ignore")
13
+ warnings.simplefilter(action='ignore', category=FutureWarning)
12
14
  sns.set_theme()
13
15
 
14
16
  __all__ = [
@@ -17,11 +19,12 @@ __all__ = [
17
19
  "create_sharpe_ratio",
18
20
  "create_sortino_ratio",
19
21
  "plot_returns_and_dd",
20
- "plot_monthly_yearly_returns"
22
+ "plot_monthly_yearly_returns",
23
+ "show_qs_stats"
21
24
  ]
22
25
 
23
26
 
24
- def create_sharpe_ratio(returns, periods=252):
27
+ def create_sharpe_ratio(returns, periods=252) -> float:
25
28
  """
26
29
  Create the Sharpe ratio for the strategy, based on a
27
30
  benchmark of zero (i.e. no risk-free rate information).
@@ -33,15 +36,10 @@ def create_sharpe_ratio(returns, periods=252):
33
36
  Returns:
34
37
  S (float): Sharpe ratio
35
38
  """
36
- if np.std(returns) != 0:
37
- return np.sqrt(periods) * (np.mean(returns)) / np.std(returns)
38
- else:
39
- return 0.0
39
+ return qs.stats.sharpe(returns, periods=periods)
40
40
 
41
41
  # Define a function to calculate the Sortino Ratio
42
-
43
-
44
- def create_sortino_ratio(returns, periods=252):
42
+ def create_sortino_ratio(returns, periods=252) -> float:
45
43
  """
46
44
  Create the Sortino ratio for the strategy, based on a
47
45
  benchmark of zero (i.e. no risk-free rate information).
@@ -53,17 +51,7 @@ def create_sortino_ratio(returns, periods=252):
53
51
  Returns:
54
52
  S (float): Sortino ratio
55
53
  """
56
- # Calculate the annualized return
57
- annualized_return = np.power(1 + np.mean(returns), periods) - 1
58
- # Calculate the downside deviation
59
- downside_returns = returns.copy()
60
- downside_returns[returns > 0] = 0
61
- annualized_downside_std = np.std(downside_returns) * np.sqrt(periods)
62
- if annualized_downside_std != 0:
63
- return annualized_return / annualized_downside_std
64
- else:
65
- return 0.0
66
-
54
+ return qs.stats.sortino(returns, periods=periods)
67
55
 
68
56
  def create_drawdowns(pnl):
69
57
  """
@@ -307,3 +295,27 @@ def plot_monthly_yearly_returns(df, title):
307
295
 
308
296
  # Show the plot
309
297
  plt.show()
298
+
299
+ def show_qs_stats(returns, benchmark, strategy_name, save_dir=None):
300
+ """
301
+ Generate the full quantstats report for the strategy.
302
+
303
+ Args:
304
+ returns (pd.Serie):
305
+ The DataFrame containing the strategy returns and drawdowns.
306
+ benchmark (str):
307
+ The ticker symbol of the benchmark to compare the strategy to.
308
+ strategy_name (str): The name of the strategy.
309
+ """
310
+ # Load the returns data
311
+ returns = returns.copy()
312
+
313
+ # Drop duplicate index entries
314
+ returns = returns[~returns.index.duplicated(keep='first')]
315
+
316
+ # Extend pandas functionality with quantstats
317
+ qs.extend_pandas()
318
+
319
+ # Generate the full report with a benchmark
320
+ qs.reports.full(returns, mode='full', benchmark=benchmark)
321
+ qs.reports.html(returns, benchmark=benchmark, output=save_dir, title=strategy_name)
@@ -1,15 +1,17 @@
1
1
  import pandas as pd
2
2
  from queue import Queue
3
+ from pathlib import Path
3
4
  from datetime import datetime
4
5
  from bbstrader.btengine.event import (
5
6
  OrderEvent, FillEvent, MarketEvent, SignalEvent
6
7
  )
7
8
  from bbstrader.btengine.data import DataHandler
8
9
  from bbstrader.btengine.performance import (
9
- create_drawdowns, plot_performance,
10
- create_sharpe_ratio, create_sortino_ratio,
11
- plot_returns_and_dd, plot_monthly_yearly_returns
10
+ create_drawdowns, create_sharpe_ratio, create_sortino_ratio,
11
+ plot_performance, show_qs_stats,plot_returns_and_dd,
12
+ plot_monthly_yearly_returns
12
13
  )
14
+ import quantstats as qs
13
15
 
14
16
 
15
17
  class Portfolio(object):
@@ -20,7 +22,7 @@ class Portfolio(object):
20
22
  sizing tools (such as the `Kelly Criterion`).
21
23
 
22
24
  The portfolio order management system is possibly the most complex component of
23
- an eventdriven backtester. Its role is to keep track of all current market positions
25
+ an event driven backtester. Its role is to keep track of all current market positions
24
26
  as well as the market value of the positions (known as the "holdings").
25
27
  This is simply an estimate of the liquidation value of the position and is derived in part
26
28
  from the data handling facility of the backtester.
@@ -95,7 +97,7 @@ class Portfolio(object):
95
97
  self.timeframe = kwargs.get("time_frame", "D1")
96
98
  self.trading_hours = kwargs.get("session_duration", 6.5)
97
99
  self.benchmark = kwargs.get('benchmark', 'SPY')
98
- self.strategy_name = kwargs.get('strategy_name', 'Strategy')
100
+ self.strategy_name = kwargs.get('strategy_name', '')
99
101
  if self.timeframe not in self._tf_mapping():
100
102
  raise ValueError(
101
103
  f"Timeframe not supported,"
@@ -227,10 +229,10 @@ class Portfolio(object):
227
229
  fill_dir = -1
228
230
 
229
231
  # Update holdings list with new quantities
230
- fill_cost = self.bars.get_latest_bar_value(
232
+ price = self.bars.get_latest_bar_value(
231
233
  fill.symbol, "Adj Close"
232
234
  )
233
- cost = fill_dir * fill_cost * fill.quantity
235
+ cost = fill_dir * price * fill.quantity
234
236
  self.current_holdings[fill.symbol] += cost
235
237
  self.current_holdings['Commission'] += fill.commission
236
238
  self.current_holdings['Cash'] -= (cost + fill.commission)
@@ -260,20 +262,21 @@ class Portfolio(object):
260
262
  direction = signal.signal_type
261
263
  quantity = signal.quantity
262
264
  strength = signal.strength
265
+ price = signal.price
263
266
  cur_quantity = self.current_positions[symbol]
264
267
 
265
268
  order_type = 'MKT'
266
269
  mkt_quantity = round(quantity * strength)
267
270
 
268
271
  if direction == 'LONG' and cur_quantity == 0:
269
- order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
272
+ order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY', price)
270
273
  if direction == 'SHORT' and cur_quantity == 0:
271
- order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')
274
+ order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL', price)
272
275
 
273
276
  if direction == 'EXIT' and cur_quantity > 0:
274
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
277
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price)
275
278
  if direction == 'EXIT' and cur_quantity < 0:
276
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
279
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price)
277
280
 
278
281
  return order
279
282
 
@@ -307,7 +310,10 @@ class Portfolio(object):
307
310
 
308
311
  sharpe_ratio = create_sharpe_ratio(returns, periods=self.tf)
309
312
  sortino_ratio = create_sortino_ratio(returns, periods=self.tf)
310
- drawdown, max_dd, dd_duration = create_drawdowns(pnl)
313
+ drawdown, _, _ = create_drawdowns(pnl)
314
+ max_dd = qs.stats.max_drawdown(returns)
315
+ dd_details = qs.stats.drawdown_details(drawdown)
316
+ dd_duration = dd_details['days'].max()
311
317
  self.equity_curve['Drawdown'] = drawdown
312
318
 
313
319
  stats = [
@@ -317,10 +323,22 @@ class Portfolio(object):
317
323
  ("Max Drawdown", f"{max_dd * 100.0:.2f}%"),
318
324
  ("Drawdown Duration", f"{dd_duration}")
319
325
  ]
320
- self.equity_curve.to_csv('equity.csv')
326
+ now = datetime.now().strftime('%Y%m%d%H%M%S')
327
+ strategy_name = self.strategy_name.replace(' ', '_')
328
+ results_dir = Path('Backtest_Results') / strategy_name
329
+ results_dir.mkdir(parents=True, exist_ok=True)
330
+
331
+ csv_file = f"{strategy_name}_{now}_equity.csv"
332
+ png_file = f'{strategy_name}_{now}_returns_heatmap.png'
333
+ html_file = f"{strategy_name}_{now}_report.html"
334
+ self.equity_curve.to_csv(results_dir / csv_file)
335
+
321
336
  plot_performance(self.equity_curve, self.strategy_name)
322
- plot_returns_and_dd(self.equity_curve,
323
- self.benchmark, 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}")
324
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}")
325
342
 
326
343
  return stats
344
+
@@ -1,7 +1,6 @@
1
1
  from abc import ABCMeta, abstractmethod
2
2
  from typing import Dict, Union
3
3
 
4
-
5
4
  class Strategy(metaclass=ABCMeta):
6
5
  """
7
6
  A `Strategy()` object encapsulates all calculation on market data
@@ -30,4 +29,4 @@ class Strategy(metaclass=ABCMeta):
30
29
  """
31
30
  raise NotImplementedError(
32
31
  "Should implement calculate_signals()"
33
- )
32
+ )
@@ -6,10 +6,9 @@ from datetime import datetime
6
6
  import MetaTrader5 as mt5
7
7
  from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
8
8
  from bbstrader.metatrader.utils import (
9
- raise_mt5_error, AccountInfo, TerminalInfo,
9
+ raise_mt5_error, AccountInfo, TerminalInfo, InvalidBroker,
10
10
  SymbolInfo, TickInfo, TradeRequest, OrderCheckResult,
11
- OrderSentResult, TradePosition, TradeOrder, TradeDeal,
12
- )
11
+ OrderSentResult, TradePosition, TradeOrder, TradeDeal,)
13
12
  from typing import Tuple, Union, List, Dict, Any, Optional, Literal
14
13
 
15
14
 
@@ -18,6 +17,13 @@ __BROKERS__ = {
18
17
  'JGM': "Just Global Markets Ltd.",
19
18
  'FTMO': "FTMO S.R.O."
20
19
  }
20
+
21
+ BROKERS_TIMEZONES = {
22
+ 'AMG': 'Europe/Helsinki',
23
+ 'JGM': 'Europe/Helsinki',
24
+ 'FTMO': 'Europe/Helsinki'
25
+ }
26
+
21
27
  _ADMIRAL_MARKETS_URL_ = "https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets"
22
28
  _ADMIRAL_MARKETS_PRODUCTS_ = ["Stocks", "ETFs",
23
29
  "Indices", "Commodities", "Futures", "Forex"]
@@ -93,13 +99,14 @@ class Account(object):
93
99
  supported = __BROKERS__.copy()
94
100
  broker = self.get_terminal_info().company
95
101
  if broker not in supported.values():
96
- raise ValueError(
102
+ msg = (
97
103
  f"{broker} is not currently supported broker for the Account() class\n"
98
104
  f"Currently Supported brokers are: {', '.join(supported.values())}\n"
99
105
  f"For {supported['AMG']}, See [{amg_url}]\n"
100
106
  f"For {supported['JGM']}, See [{jgm_url}]\n"
101
107
  f"For {supported['FTMO']}, See [{ftmo_url}]\n"
102
108
  )
109
+ raise InvalidBroker(message=msg)
103
110
 
104
111
  def get_account_info(
105
112
  self,
@@ -782,7 +789,7 @@ class Account(object):
782
789
  else:
783
790
  positions = mt5.positions_get()
784
791
 
785
- if positions is None:
792
+ if positions is None or len(positions) == 0:
786
793
  return None
787
794
  if to_df:
788
795
  df = pd.DataFrame(list(positions), columns=positions[0]._asdict())
@@ -885,7 +892,7 @@ class Account(object):
885
892
  else:
886
893
  position_deals = mt5.history_deals_get(date_from, date_to)
887
894
 
888
- if position_deals is None:
895
+ if position_deals is None or len(position_deals) == 0:
889
896
  return None
890
897
 
891
898
  df = pd.DataFrame(list(position_deals),
@@ -961,7 +968,7 @@ class Account(object):
961
968
  else:
962
969
  orders = mt5.orders_get()
963
970
 
964
- if orders is None:
971
+ if orders is None or len(orders) == 0:
965
972
  return None
966
973
 
967
974
  if to_df:
@@ -1064,7 +1071,7 @@ class Account(object):
1064
1071
  else:
1065
1072
  history_orders = mt5.history_orders_get(date_from, date_to)
1066
1073
 
1067
- if history_orders is None:
1074
+ if history_orders is None or len(history_orders) == 0:
1068
1075
  return None
1069
1076
 
1070
1077
  df = pd.DataFrame(list(history_orders),
@@ -3,8 +3,7 @@ import MetaTrader5 as Mt5
3
3
  from datetime import datetime
4
4
  from typing import Union, Optional
5
5
  from bbstrader.metatrader.utils import (
6
- raise_mt5_error, TimeFrame, TIMEFRAMES
7
- )
6
+ raise_mt5_error, TimeFrame, TIMEFRAMES)
8
7
  from bbstrader.metatrader.account import INIT_MSG
9
8
  from pandas.tseries.offsets import CustomBusinessDay
10
9
  from pandas.tseries.holiday import USFederalHolidayCalendar
@@ -22,8 +21,13 @@ class Rates(object):
22
21
  and count of bars or by providing a specific date range.
23
22
 
24
23
  Notes:
25
- The `get_open, get_high, get_low, get_close, get_adj_close, get_returns,
26
- get_volume` properties return data in Broker's timezone.
24
+ 1. Befor using this class, ensure that the `Max bars in chart` in you terminal
25
+ is set to a value that is greater than the number of bars you want to retrieve
26
+ or just set it to Unlimited.
27
+ In your MT5 terminal, go to `Tools` -> `Options` -> `Charts` -> `Max bars in chart`.
28
+
29
+ 2. The `get_open, get_high, get_low, get_close, get_adj_close, get_returns,
30
+ get_volume` properties returns data in Broker's timezone.
27
31
 
28
32
  Example:
29
33
  >>> rates = Rates("EURUSD", "1h")
@@ -221,7 +225,7 @@ class Rates(object):
221
225
 
222
226
  @property
223
227
  def get_volume(self):
224
- return self.data['Volume']
228
+ return self.__data['Volume']
225
229
 
226
230
  def get_historical_data(
227
231
  self,
@@ -247,7 +251,7 @@ class Rates(object):
247
251
  ValueError: If the starting date is greater than the ending date.
248
252
 
249
253
  Notes:
250
- The Datetime for this method is in UTC timezone.
254
+ The Datetime for this method is in Local timezone.
251
255
  """
252
256
  df = self._fetch_data(date_from, date_to)
253
257
  if save_csv and df is not None:
@@ -7,8 +7,7 @@ import MetaTrader5 as Mt5
7
7
  from bbstrader.metatrader.account import Account
8
8
  from bbstrader.metatrader.rates import Rates
9
9
  from bbstrader.metatrader.utils import (
10
- TIMEFRAMES, raise_mt5_error, TimeFrame
11
- )
10
+ TIMEFRAMES, raise_mt5_error, TimeFrame)
12
11
  from typing import List, Dict, Optional, Literal, Union, Any
13
12
 
14
13