bbstrader 0.1.94__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.

Files changed (38) hide show
  1. bbstrader/__ini__.py +9 -9
  2. bbstrader/btengine/__init__.py +7 -7
  3. bbstrader/btengine/backtest.py +30 -26
  4. bbstrader/btengine/data.py +100 -79
  5. bbstrader/btengine/event.py +2 -1
  6. bbstrader/btengine/execution.py +18 -16
  7. bbstrader/btengine/performance.py +11 -7
  8. bbstrader/btengine/portfolio.py +35 -36
  9. bbstrader/btengine/strategy.py +119 -94
  10. bbstrader/config.py +14 -8
  11. bbstrader/core/__init__.py +0 -0
  12. bbstrader/core/data.py +22 -0
  13. bbstrader/core/utils.py +57 -0
  14. bbstrader/ibkr/__init__.py +0 -0
  15. bbstrader/ibkr/utils.py +0 -0
  16. bbstrader/metatrader/__init__.py +5 -5
  17. bbstrader/metatrader/account.py +117 -121
  18. bbstrader/metatrader/rates.py +83 -80
  19. bbstrader/metatrader/risk.py +23 -37
  20. bbstrader/metatrader/trade.py +169 -140
  21. bbstrader/metatrader/utils.py +3 -3
  22. bbstrader/models/__init__.py +5 -5
  23. bbstrader/models/factors.py +280 -0
  24. bbstrader/models/ml.py +1092 -0
  25. bbstrader/models/optimization.py +31 -28
  26. bbstrader/models/{portfolios.py → portfolio.py} +64 -46
  27. bbstrader/models/risk.py +15 -9
  28. bbstrader/trading/__init__.py +2 -2
  29. bbstrader/trading/execution.py +252 -164
  30. bbstrader/trading/scripts.py +8 -4
  31. bbstrader/trading/strategies.py +79 -66
  32. bbstrader/tseries.py +482 -107
  33. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/LICENSE +1 -1
  34. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/METADATA +6 -1
  35. bbstrader-0.2.1.dist-info/RECORD +37 -0
  36. bbstrader-0.1.94.dist-info/RECORD +0 -32
  37. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/WHEEL +0 -0
  38. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/top_level.txt +0 -0
@@ -1,26 +1,23 @@
1
+ import string
1
2
  from abc import ABCMeta, abstractmethod
2
- import pytz
3
- import pandas as pd
4
- import numpy as np
5
- from queue import Queue
6
3
  from datetime import datetime
7
- from bbstrader.config import config_logger
8
- from bbstrader.btengine.event import SignalEvent
9
- from bbstrader.btengine.event import FillEvent
4
+ from queue import Queue
5
+ from typing import Dict, List, Literal, Union
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import pytz
10
+
10
11
  from bbstrader.btengine.data import DataHandler
11
- from bbstrader.metatrader.account import Account
12
+ from bbstrader.btengine.event import FillEvent, SignalEvent
13
+ from bbstrader.metatrader.account import Account, AdmiralMarktsGroup
12
14
  from bbstrader.metatrader.rates import Rates
13
- from typing import (
14
- Dict,
15
- Union,
16
- Any,
17
- List,
18
- Literal
19
- )
20
15
  from bbstrader.models.optimization import optimized_weights
16
+ from bbstrader.core.utils import TradeSignal
21
17
 
22
18
  __all__ = ['Strategy', 'MT5Strategy']
23
19
 
20
+
24
21
  class Strategy(metaclass=ABCMeta):
25
22
  """
26
23
  A `Strategy()` object encapsulates all calculation on market data
@@ -44,13 +41,15 @@ class Strategy(metaclass=ABCMeta):
44
41
  """
45
42
 
46
43
  @abstractmethod
47
- def calculate_signals(self, *args, **kwargs) -> Any:
44
+ def calculate_signals(self, *args, **kwargs) -> List[TradeSignal]:
48
45
  raise NotImplementedError(
49
46
  "Should implement calculate_signals()"
50
47
  )
48
+
51
49
  def check_pending_orders(self, *args, **kwargs): ...
52
50
  def get_update_from_portfolio(self, *args, **kwargs): ...
53
51
  def update_trades_from_fill(self, *args, **kwargs): ...
52
+ def perform_period_end_checks(self, *args, **kwargs): ...
54
53
 
55
54
 
56
55
  class MT5Strategy(Strategy):
@@ -61,8 +60,8 @@ class MT5Strategy(Strategy):
61
60
  for live trading and `MT5BacktestEngine` objects for backtesting.
62
61
  """
63
62
 
64
- def __init__(self, events: Queue=None, symbol_list: List[str]=None,
65
- bars: DataHandler=None, mode: str=None, **kwargs):
63
+ def __init__(self, events: Queue = None, symbol_list: List[str] = None,
64
+ bars: DataHandler = None, mode: str = None, **kwargs):
66
65
  """
67
66
  Initialize the `MT5Strategy` object.
68
67
 
@@ -82,15 +81,18 @@ class MT5Strategy(Strategy):
82
81
  self.mode = mode
83
82
  self._porfolio_value = None
84
83
  self.risk_budget = self._check_risk_budget(**kwargs)
85
- self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
84
+ self.max_trades = kwargs.get(
85
+ "max_trades", {s: 1 for s in self.symbols})
86
86
  self.tf = kwargs.get("time_frame", 'D1')
87
87
  self.logger = kwargs.get("logger")
88
- self._initialize_portfolio()
88
+ if self.mode == 'backtest':
89
+ self._initialize_portfolio()
90
+ self.kwargs = kwargs
89
91
 
90
92
  @property
91
93
  def cash(self) -> float:
92
94
  return self._porfolio_value
93
-
95
+
94
96
  @cash.setter
95
97
  def cash(self, value):
96
98
  self._porfolio_value = value
@@ -98,28 +100,30 @@ class MT5Strategy(Strategy):
98
100
  @property
99
101
  def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
100
102
  return self._orders
101
-
103
+
102
104
  @property
103
105
  def trades(self) -> Dict[str, Dict[str, int]]:
104
106
  return self._trades
105
107
 
106
108
  @property
107
- def positions(self) -> Dict[str, Dict[str, int|float]]:
109
+ def positions(self) -> Dict[str, Dict[str, int | float]]:
108
110
  return self._positions
109
-
111
+
110
112
  @property
111
113
  def holdings(self) -> Dict[str, float]:
112
114
  return self._holdings
113
-
115
+
114
116
  def _check_risk_budget(self, **kwargs):
115
117
  weights = kwargs.get('risk_weights')
116
118
  if weights is not None and isinstance(weights, dict):
117
119
  for asset in self.symbols:
118
120
  if asset not in weights:
119
- raise ValueError(f"Risk budget for asset {asset} is missing.")
121
+ raise ValueError(
122
+ f"Risk budget for asset {asset} is missing.")
120
123
  total_risk = sum(weights.values())
121
124
  if not np.isclose(total_risk, 1.0):
122
- raise ValueError(f'Risk budget weights must sum to 1. got {total_risk}')
125
+ raise ValueError(
126
+ f'Risk budget weights must sum to 1. got {total_risk}')
123
127
  return weights
124
128
  elif isinstance(weights, str):
125
129
  return weights
@@ -140,7 +144,7 @@ class MT5Strategy(Strategy):
140
144
  for order in orders:
141
145
  self._orders[symbol][order] = []
142
146
  self._holdings = {s: 0.0 for s in self.symbols}
143
-
147
+
144
148
  def get_update_from_portfolio(self, positions, holdings):
145
149
  """
146
150
  Update the positions and holdings for the strategy from the portfolio.
@@ -163,7 +167,7 @@ class MT5Strategy(Strategy):
163
167
  self._positions[symbol]['SHORT'] = 0
164
168
  if symbol in holdings:
165
169
  self._holdings[symbol] = holdings[symbol]
166
-
170
+
167
171
  def update_trades_from_fill(self, event: FillEvent):
168
172
  """
169
173
  This method updates the trades for the strategy based on the fill event.
@@ -177,52 +181,47 @@ class MT5Strategy(Strategy):
177
181
  elif event.order == 'EXIT' and event.direction == 'SELL':
178
182
  self._trades[event.symbol]['LONG'] = 0
179
183
 
180
- def calculate_signals(self, *args, **kwargs
181
- ) -> Dict[str, Union[str, dict, None]] | None:
184
+ def calculate_signals(self, *args, **kwargs) -> List[TradeSignal]:
182
185
  """
183
186
  Provides the mechanisms to calculate signals for the strategy.
184
- This methods should return a dictionary of symbols and their respective signals.
185
- The returned signals should be either string or dictionary objects.
187
+ This methods should return a list of signals for the strategy.
186
188
 
187
- If a string is used, it should be:
188
- - ``LONG`` , ``BMKT``, ``BLMT``, ``BSTP``, ``BSTPLMT`` for a long signal (market, limit, stop, stop-limit).
189
- - ``SHORT``, ``SMKT``, ``SLMT``, ``SSTP``, ``SSTPLMT`` for a short signal (market, limit, stop, stop-limit).
190
- - ``EXIT``, ``EXIT_LONG``, ``EXIT_LONG_STOP``, ``EXIT_LONG_LIMIT``, ``EXIT_LONG_STOP_LIMIT`` for an exit signal (long).
191
- - ``EXIT_SHORT``, ``EXIT_SHORT_STOP``, ``EXIT_SHORT_LIMIT``, ``EXIT_SHORT_STOP_LIMIT`` for an exit signal (short).
192
- - ``EXIT_ALL_ORDERS`` for cancelling all orders.
193
- - ``EXIT_ALL_POSITIONS`` for exiting all positions.
194
- - ``EXIT_PROFITABLES`` for exiting all profitable positions.
195
- - ``EXIT_LOSINGS`` for exiting all losing positions.
196
-
197
- The signals could also be ``EXIT_STOP``, ``EXIT_LIMIT``, ``EXIT_STOP_LIMIT`` for exiting a position.
189
+ Each signal must be a ``TradeSignal`` object with the following attributes:
190
+ - ``action``: The order to execute on the symbol (LONG, SHORT, EXIT, etc.), see `bbstrader.core.utils.TradeAction`.
191
+ - ``price``: The price at which to execute the action, used for pending orders.
192
+ - ``stoplimit``: The stop-limit price for STOP-LIMIT orders, used for pending stop limit orders.
193
+ - ``id``: The unique identifier for the strategy or order.
194
+ """
195
+ pass
198
196
 
199
- If a dictionary is used, it should be:
200
- for each symbol, a dictionary with the following keys
201
- - ``action``: The action to take for the symbol (LONG, SHORT, EXIT, etc.)
202
- - ``price``: The price at which to execute the action.
203
- - ``stoplimit``: The stop-limit price for STOP-LIMIT orders.
197
+ def perform_period_end_checks(self, *args, **kwargs):
198
+ """
199
+ Some strategies may require additional checks at the end of the period,
200
+ such as closing all positions or orders or tracking the performance of the strategy etc.
204
201
 
205
- The dictionary can be use for pending orders (limit, stop, stop-limit) where the price is required.
202
+ This method is called at the end of the period to perform such checks.
206
203
  """
207
204
  pass
208
205
 
209
- def apply_risk_management(self, optimer, freq=252) -> Dict[str, float] | None:
206
+ def apply_risk_management(self, optimer, symbols=None, freq=252) -> Dict[str, float] | None:
210
207
  """
211
208
  Apply risk management rules to the strategy.
212
209
  """
213
210
  if optimer is None:
214
211
  return None
212
+ symbols = symbols or self.symbols
215
213
  prices = self.get_asset_values(
216
- symbol_list=self.symbols, bars=self.data, mode=self.mode,
214
+ symbol_list=symbols, bars=self.data, mode=self.mode,
217
215
  window=freq, value_type='close', array=False, tf=self.tf
218
216
  )
219
217
  prices = pd.DataFrame(prices)
220
218
  prices = prices.dropna(axis=0, how='any')
221
219
  try:
222
- weights = optimized_weights(prices=prices, freq=freq, method=optimer)
220
+ weights = optimized_weights(
221
+ prices=prices, freq=freq, method=optimer)
223
222
  return {symbol: weight for symbol, weight in weights.items()}
224
223
  except Exception:
225
- return {symbol: 0.0 for symbol in self.symbols}
224
+ return {symbol: 0.0 for symbol in symbols}
226
225
 
227
226
  def get_quantity(self, symbol, weight, price=None, volume=None, maxqty=None) -> int:
228
227
  """
@@ -237,17 +236,17 @@ class MT5Strategy(Strategy):
237
236
  qty : The quantity to buy or sell for the symbol.
238
237
  """
239
238
  if (self._porfolio_value is None or weight == 0 or
240
- self._porfolio_value == 0 or np.isnan(self._porfolio_value)):
239
+ self._porfolio_value == 0 or np.isnan(self._porfolio_value)):
241
240
  return 0
242
241
  if volume is None:
243
242
  volume = round(self._porfolio_value * weight)
244
243
  if price is None:
245
244
  price = self.data.get_latest_bar_value(symbol, 'close')
246
245
  if (price is None or not isinstance(price, (int, float, np.number))
247
- or volume is None or not isinstance(volume, (int, float, np.number))
246
+ or volume is None or not isinstance(volume, (int, float, np.number))
248
247
  or np.isnan(float(price))
249
248
  or np.isnan(float(volume))
250
- ):
249
+ ):
251
250
  if weight != 0:
252
251
  return 1
253
252
  return 0
@@ -256,7 +255,7 @@ class MT5Strategy(Strategy):
256
255
  if maxqty is not None:
257
256
  qty = min(qty, maxqty)
258
257
  return max(round(qty, 2), 0)
259
-
258
+
260
259
  def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
261
260
  """
262
261
  Get the quantities to buy or sell for the symbols in the strategy.
@@ -271,7 +270,7 @@ class MT5Strategy(Strategy):
271
270
  return quantities
272
271
  elif isinstance(quantities, int):
273
272
  return {symbol: quantities for symbol in self.symbols}
274
-
273
+
275
274
  def _send_order(self, id, symbol: str, signal: str, strength: float, price: float,
276
275
  quantity: int, dtime: datetime | pd.Timestamp):
277
276
 
@@ -290,8 +289,8 @@ class MT5Strategy(Strategy):
290
289
  self.logger.info(
291
290
  f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
292
291
 
293
- def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
294
- strength: float=1.0, dtime: datetime | pd.Timestamp=None):
292
+ def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
293
+ strength: float = 1.0, dtime: datetime | pd.Timestamp = None):
295
294
  """
296
295
  Open a long position
297
296
 
@@ -305,7 +304,8 @@ class MT5Strategy(Strategy):
305
304
 
306
305
  See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
307
306
  """
308
- self._send_order(id, symbol, 'SHORT', strength, price, quantity, dtime)
307
+ self._send_order(id, symbol, 'SHORT', strength,
308
+ price, quantity, dtime)
309
309
 
310
310
  def close_positions(self, id, symbol, price, quantity, strength=1.0, dtime=None):
311
311
  """
@@ -371,8 +371,8 @@ class MT5Strategy(Strategy):
371
371
  quantity=quantity, strength=strength, price=price)
372
372
  self._orders[symbol]['SLMT'].append(order)
373
373
 
374
- def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
375
- quantity: int, strength: float=1.0, dtime: datetime | pd.Timestamp = None):
374
+ def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
375
+ quantity: int, strength: float = 1.0, dtime: datetime | pd.Timestamp = None):
376
376
  """
377
377
  Open a pending order to buy at a stop-limit price
378
378
 
@@ -412,52 +412,57 @@ class MT5Strategy(Strategy):
412
412
  """
413
413
  for symbol in self.symbols:
414
414
  dtime = self.data.get_latest_bar_datetime(symbol)
415
- logmsg = lambda order, type: self.logger.info(
415
+
416
+ def logmsg(order, type): return self.logger.info(
416
417
  f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
417
418
  f"PRICE @ {order.price}", custom_time=dtime)
418
419
  for order in self._orders[symbol]['BLMT'].copy():
419
420
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
420
421
  self.buy_mkt(order.strategy_id, symbol,
421
- order.price, order.quantity, dtime)
422
+ order.price, order.quantity, dtime)
422
423
  try:
423
424
  self._orders[symbol]['BLMT'].remove(order)
424
425
  assert order not in self._orders[symbol]['BLMT']
425
426
  logmsg(order, 'BUY LIMIT')
426
427
  except AssertionError:
427
- self._orders[symbol]['BLMT'] = [o for o in self._orders[symbol]['BLMT'] if o != order]
428
+ self._orders[symbol]['BLMT'] = [
429
+ o for o in self._orders[symbol]['BLMT'] if o != order]
428
430
  logmsg(order, 'BUY LIMIT')
429
431
  for order in self._orders[symbol]['SLMT'].copy():
430
432
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
431
433
  self.sell_mkt(order.strategy_id, symbol,
432
- order.price, order.quantity, dtime)
434
+ order.price, order.quantity, dtime)
433
435
  try:
434
436
  self._orders[symbol]['SLMT'].remove(order)
435
437
  assert order not in self._orders[symbol]['SLMT']
436
438
  logmsg(order, 'SELL LIMIT')
437
439
  except AssertionError:
438
- self._orders[symbol]['SLMT'] = [o for o in self._orders[symbol]['SLMT'] if o != order]
440
+ self._orders[symbol]['SLMT'] = [
441
+ o for o in self._orders[symbol]['SLMT'] if o != order]
439
442
  logmsg(order, 'SELL LIMIT')
440
443
  for order in self._orders[symbol]['BSTP'].copy():
441
444
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
442
445
  self.buy_mkt(order.strategy_id, symbol,
443
- order.price, order.quantity, dtime)
446
+ order.price, order.quantity, dtime)
444
447
  try:
445
448
  self._orders[symbol]['BSTP'].remove(order)
446
449
  assert order not in self._orders[symbol]['BSTP']
447
450
  logmsg(order, 'BUY STOP')
448
451
  except AssertionError:
449
- self._orders[symbol]['BSTP'] = [o for o in self._orders[symbol]['BSTP'] if o != order]
452
+ self._orders[symbol]['BSTP'] = [
453
+ o for o in self._orders[symbol]['BSTP'] if o != order]
450
454
  logmsg(order, 'BUY STOP')
451
455
  for order in self._orders[symbol]['SSTP'].copy():
452
456
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
453
457
  self.sell_mkt(order.strategy_id, symbol,
454
- order.price, order.quantity, dtime)
458
+ order.price, order.quantity, dtime)
455
459
  try:
456
460
  self._orders[symbol]['SSTP'].remove(order)
457
461
  assert order not in self._orders[symbol]['SSTP']
458
462
  logmsg(order, 'SELL STOP')
459
463
  except AssertionError:
460
- self._orders[symbol]['SSTP'] = [o for o in self._orders[symbol]['SSTP'] if o != order]
464
+ self._orders[symbol]['SSTP'] = [
465
+ o for o in self._orders[symbol]['SSTP'] if o != order]
461
466
  logmsg(order, 'SELL STOP')
462
467
  for order in self._orders[symbol]['BSTPLMT'].copy():
463
468
  if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
@@ -468,7 +473,8 @@ class MT5Strategy(Strategy):
468
473
  assert order not in self._orders[symbol]['BSTPLMT']
469
474
  logmsg(order, 'BUY STOP LIMIT')
470
475
  except AssertionError:
471
- self._orders[symbol]['BSTPLMT'] = [o for o in self._orders[symbol]['BSTPLMT'] if o != order]
476
+ self._orders[symbol]['BSTPLMT'] = [
477
+ o for o in self._orders[symbol]['BSTPLMT'] if o != order]
472
478
  logmsg(order, 'BUY STOP LIMIT')
473
479
  for order in self._orders[symbol]['SSTPLMT'].copy():
474
480
  if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
@@ -479,21 +485,23 @@ class MT5Strategy(Strategy):
479
485
  assert order not in self._orders[symbol]['SSTPLMT']
480
486
  logmsg(order, 'SELL STOP LIMIT')
481
487
  except AssertionError:
482
- self._orders[symbol]['SSTPLMT'] = [o for o in self._orders[symbol]['SSTPLMT'] if o != order]
488
+ self._orders[symbol]['SSTPLMT'] = [
489
+ o for o in self._orders[symbol]['SSTPLMT'] if o != order]
483
490
  logmsg(order, 'SELL STOP LIMIT')
484
491
 
485
- def calculate_pct_change(self, current_price, lh_price):
492
+ @staticmethod
493
+ def calculate_pct_change(current_price, lh_price):
486
494
  return ((current_price - lh_price) / lh_price) * 100
487
-
495
+
488
496
  def get_asset_values(self,
489
- symbol_list: List[str],
490
- window: int,
491
- value_type: str = 'returns',
492
- array: bool = True,
493
- bars: DataHandler = None,
494
- mode: Literal['backtest', 'live'] = 'backtest',
495
- tf: str = 'D1'
496
- ) -> Dict[str, np.ndarray | pd.Series] | None:
497
+ symbol_list: List[str],
498
+ window: int,
499
+ value_type: str = 'returns',
500
+ array: bool = True,
501
+ bars: DataHandler = None,
502
+ mode: Literal['backtest', 'live'] = 'backtest',
503
+ tf: str = 'D1'
504
+ ) -> Dict[str, np.ndarray | pd.Series] | None:
497
505
  """
498
506
  Get the historical OHLCV value or returns or custum value
499
507
  based on the DataHandker of the assets in the symbol list.
@@ -509,7 +517,7 @@ class MT5Strategy(Strategy):
509
517
 
510
518
  Returns:
511
519
  asset_values : Historical values of the assets in the symbol list.
512
-
520
+
513
521
  Note:
514
522
  In Live mode, the `bbstrader.metatrader.rates.Rates` class is used to get the historical data
515
523
  so the value_type must be 'returns', 'open', 'high', 'low', 'close', 'adjclose', 'volume'.
@@ -527,10 +535,11 @@ class MT5Strategy(Strategy):
527
535
  asset_values[asset] = values[~np.isnan(values)]
528
536
  else:
529
537
  values = bars.get_latest_bars(asset, N=window)
530
- asset_values[asset] = getattr(values, value_type)
538
+ asset_values[asset] = getattr(values, value_type)
531
539
  elif mode == 'live':
532
540
  for asset in symbol_list:
533
- rates = Rates(symbol=asset, time_frame=tf, count=window + 1)
541
+ rates = Rates(asset, timeframe=tf,
542
+ count=window + 1, **self.kwargs)
534
543
  if array:
535
544
  values = getattr(rates, value_type).values
536
545
  asset_values[asset] = values[~np.isnan(values)]
@@ -542,7 +551,8 @@ class MT5Strategy(Strategy):
542
551
  else:
543
552
  return None
544
553
 
545
- def is_signal_time(self, period_count, signal_inverval) -> bool:
554
+ @staticmethod
555
+ def is_signal_time(period_count, signal_inverval) -> bool:
546
556
  """
547
557
  Check if we can generate a signal based on the current period count.
548
558
  We use the signal interval as a form of periodicity or rebalancing period.
@@ -575,11 +585,11 @@ class MT5Strategy(Strategy):
575
585
  Returns:
576
586
  bool : True if there are open positions, False otherwise
577
587
  """
578
- account = account or Account()
588
+ account = account or Account(**self.kwargs)
579
589
  positions = account.get_positions(symbol=symbol)
580
590
  if positions is not None:
581
591
  open_positions = [
582
- pos for pos in positions if pos.type == position
592
+ pos.ticket for pos in positions if pos.type == position
583
593
  and pos.magic == strategy_id
584
594
  ]
585
595
  if one_true:
@@ -600,7 +610,7 @@ class MT5Strategy(Strategy):
600
610
  Returns:
601
611
  prices : numpy array of buy or sell prices for open positions if any or an empty array.
602
612
  """
603
- account = account or Account()
613
+ account = account or Account(**self.kwargs)
604
614
  positions = account.get_positions(symbol=symbol)
605
615
  if positions is not None:
606
616
  prices = np.array([
@@ -610,10 +620,12 @@ class MT5Strategy(Strategy):
610
620
  return prices
611
621
  return np.array([])
612
622
 
613
- def get_current_dt(self, time_zone: str = 'US/Eastern') -> datetime:
623
+ @staticmethod
624
+ def get_current_dt(time_zone: str = 'US/Eastern') -> datetime:
614
625
  return datetime.now(pytz.timezone(time_zone))
615
626
 
616
- def convert_time_zone(self, dt: datetime | int | pd.Timestamp,
627
+ @staticmethod
628
+ def convert_time_zone(dt: datetime | int | pd.Timestamp,
617
629
  from_tz: str = 'UTC',
618
630
  to_tz: str = 'US/Eastern'
619
631
  ) -> pd.Timestamp:
@@ -641,6 +653,19 @@ class MT5Strategy(Strategy):
641
653
  dt_to = dt.tz_convert(pytz.timezone(to_tz))
642
654
  return dt_to
643
655
 
656
+ @staticmethod
657
+ def get_mt5_equivalent(symbols, type='STK', path: str = None) -> List[str]:
658
+ account = Account(path=path)
659
+ mt5_symbols = account.get_symbols(symbol_type=type)
660
+ mt5_equivalent = []
661
+ if account.broker == AdmiralMarktsGroup():
662
+ for s in mt5_symbols:
663
+ _s = s[1:] if s[0] in string.punctuation else s
664
+ for symbol in symbols:
665
+ if _s.split('.')[0] == symbol or _s.split('_')[0] == symbol:
666
+ mt5_equivalent.append(s)
667
+ return mt5_equivalent
668
+
644
669
 
645
670
  class TWSStrategy(Strategy):
646
671
  ...
bbstrader/config.py CHANGED
@@ -1,9 +1,12 @@
1
1
  import logging
2
- from typing import List
3
2
  from pathlib import Path
4
- from datetime import datetime
3
+ from typing import List
4
+
5
+ ADMIRAL_PATH = "C:\\Program Files\\Admirals Group MT5 Terminal\\terminal64.exe"
6
+ FTMO_PATH = "C:\\Program Files\\FTMO MetaTrader 5\\terminal64.exe"
5
7
 
6
- def get_config_dir(name: str=".bbstrader") -> Path:
8
+
9
+ def get_config_dir(name: str = ".bbstrader") -> Path:
7
10
  """
8
11
  Get the path to the configuration directory.
9
12
 
@@ -18,6 +21,7 @@ def get_config_dir(name: str=".bbstrader") -> Path:
18
21
  home_dir.mkdir()
19
22
  return home_dir
20
23
 
24
+
21
25
  BBSTRADER_DIR = get_config_dir()
22
26
 
23
27
 
@@ -43,7 +47,7 @@ class LogLevelFilter(logging.Filter):
43
47
  True if the record's level is in the allowed levels, False otherwise.
44
48
  """
45
49
  return record.levelno in self.levels
46
-
50
+
47
51
 
48
52
  class CustomFormatter(logging.Formatter):
49
53
  def formatTime(self, record, datefmt=None):
@@ -57,7 +61,7 @@ class CustomLogger(logging.Logger):
57
61
  def __init__(self, name, level=logging.NOTSET):
58
62
  super().__init__(name, level)
59
63
 
60
- def _log(self, level, msg, args, exc_info=None,
64
+ def _log(self, level, msg, args, exc_info=None,
61
65
  extra=None, stack_info=False, stacklevel=1, custom_time=None):
62
66
  if extra is None:
63
67
  extra = {}
@@ -73,13 +77,15 @@ class CustomLogger(logging.Logger):
73
77
  self._log(logging.DEBUG, msg, args, custom_time=custom_time, **kwargs)
74
78
 
75
79
  def warning(self, msg, *args, custom_time=None, **kwargs):
76
- self._log(logging.WARNING, msg, args, custom_time=custom_time, **kwargs)
80
+ self._log(logging.WARNING, msg, args,
81
+ custom_time=custom_time, **kwargs)
77
82
 
78
83
  def error(self, msg, *args, custom_time=None, **kwargs):
79
84
  self._log(logging.ERROR, msg, args, custom_time=custom_time, **kwargs)
80
85
 
81
86
  def critical(self, msg, *args, custom_time=None, **kwargs):
82
- self._log(logging.CRITICAL, msg, args, custom_time=custom_time, **kwargs)
87
+ self._log(logging.CRITICAL, msg, args,
88
+ custom_time=custom_time, **kwargs)
83
89
 
84
90
 
85
91
  def config_logger(log_file: str, console_log=True):
@@ -108,4 +114,4 @@ def config_logger(log_file: str, console_log=True):
108
114
  console_handler.setFormatter(formatter)
109
115
  logger.addHandler(console_handler)
110
116
 
111
- return logger
117
+ return logger
File without changes
bbstrader/core/data.py ADDED
@@ -0,0 +1,22 @@
1
+ from financetoolkit import Toolkit
2
+
3
+ __all__ = [
4
+ 'FMP',
5
+ ]
6
+
7
+
8
+ class FMP(Toolkit):
9
+ """
10
+ FMPData class for fetching data from Financial Modeling Prep API
11
+ using the Toolkit class from financetoolkit package.
12
+
13
+ See `financetoolkit` for more details.
14
+
15
+ """
16
+
17
+ def __init__(self, api_key: str = '', symbols: str | list = 'AAPL'):
18
+ super().__init__(tickers=symbols, api_key=api_key)
19
+
20
+
21
+ class DataBendo:
22
+ ...
@@ -0,0 +1,57 @@
1
+ from enum import Enum
2
+ from dataclasses import dataclass
3
+
4
+
5
+ class TradeAction(Enum):
6
+ """
7
+ An enumeration class for trade actions.
8
+ """
9
+ BUY = "LONG"
10
+ LONG = "LONG"
11
+ SELL = "SHORT"
12
+ EXIT = "EXIT"
13
+ BMKT = "BMKT"
14
+ SMKT = "SMKT"
15
+ BLMT = "BLMT"
16
+ SLMT = "SLMT"
17
+ BSTP = "BSTP"
18
+ SSTP = "SSTP"
19
+ SHORT = "SHORT"
20
+ BSTPLMT = "BSTPLMT"
21
+ SSTPLMT = "SSTPLMT"
22
+ EXIT_LONG = "EXIT_LONG"
23
+ EXIT_SHORT = "EXIT_SHORT"
24
+ EXIT_STOP = "EXIT_STOP"
25
+ EXIT_LIMIT = "EXIT_LIMIT"
26
+ EXIT_LONG_STOP = "EXIT_LONG_STOP"
27
+ EXIT_LONG_LIMIT = "EXIT_LONG_LIMIT"
28
+ EXIT_SHORT_STOP = "EXIT_SHORT_STOP"
29
+ EXIT_SHORT_LIMIT = "EXIT_SHORT_LIMIT"
30
+ EXIT_LONG_STOP_LIMIT = "EXIT_LONG_STOP_LIMIT"
31
+ EXIT_SHORT_STOP_LIMIT = "EXIT_SHORT_STOP_LIMIT"
32
+ EXIT_PROFITABLES = "EXIT_PROFITABLES"
33
+ EXIT_LOSINGS = "EXIT_LOSINGS"
34
+ EXIT_ALL_POSITIONS = "EXIT_ALL_POSITIONS"
35
+ EXIT_ALL_LONGS = "EXIT_ALL_LONGS"
36
+
37
+ def __str__(self):
38
+ return self.value
39
+
40
+
41
+ @dataclass()
42
+ class TradeSignal:
43
+ """
44
+ A dataclass for storing trading signal.
45
+ """
46
+
47
+ id: int
48
+ symbol: str
49
+ action: TradeAction
50
+ price: float = None
51
+ stoplimit: float = None
52
+
53
+ def __repr__(self):
54
+ return (
55
+ f"TradeSignal(id={self.id}, symbol='{self.symbol}', "
56
+ f"action='{self.action.value}', price={self.price}, stoplimit={self.stoplimit})"
57
+ )
File without changes
File without changes
@@ -1,6 +1,6 @@
1
1
 
2
- from bbstrader.metatrader.account import *
3
- from bbstrader.metatrader.rates import *
4
- from bbstrader.metatrader.risk import *
5
- from bbstrader.metatrader.trade import *
6
- from bbstrader.metatrader.utils import *
2
+ from bbstrader.metatrader.account import * # noqa: F403
3
+ from bbstrader.metatrader.rates import * # noqa: F403
4
+ from bbstrader.metatrader.risk import * # noqa: F403
5
+ from bbstrader.metatrader.trade import * # noqa: F403
6
+ from bbstrader.metatrader.utils import * # noqa: F403