bbstrader 0.2.5__py3-none-any.whl → 0.2.7__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 CHANGED
@@ -3,11 +3,12 @@ Simplified Investment & Trading Toolkit
3
3
  =======================================
4
4
 
5
5
  """
6
- __author__ = "Bertin Balouki SIMYELI"
7
- __copyright__ = "2023-2025 Bertin Balouki SIMYELI"
8
- __email__ = "bertin@bbstrader.com"
9
- __license__ = "MIT"
10
- __version__ = '0.2.0'
6
+
7
+ __author__ = "Bertin Balouki SIMYELI"
8
+ __copyright__ = "2023-2025 Bertin Balouki SIMYELI"
9
+ __email__ = "bertin@bbstrader.com"
10
+ __license__ = "MIT"
11
+ __version__ = "0.2.0"
11
12
 
12
13
 
13
14
  from bbstrader import btengine # noqa: F401
@@ -124,7 +124,7 @@ class MT5ExecutionHandler(ExecutionHandler):
124
124
  self.events = events
125
125
  self.bardata = data
126
126
  self.logger = kwargs.get("logger")
127
- self.__account = Account()
127
+ self.__account = Account(**kwargs)
128
128
 
129
129
  def _calculate_lot(self, symbol, quantity, price):
130
130
  symbol_type = self.__account.get_symbol_type(symbol)
@@ -10,7 +10,11 @@ import pytz
10
10
 
11
11
  from bbstrader.btengine.data import DataHandler
12
12
  from bbstrader.btengine.event import FillEvent, SignalEvent
13
- from bbstrader.metatrader.account import Account, AdmiralMarktsGroup
13
+ from bbstrader.metatrader.account import (
14
+ Account,
15
+ AdmiralMarktsGroup,
16
+ PepperstoneGroupLimited,
17
+ )
14
18
  from bbstrader.metatrader.rates import Rates
15
19
  from bbstrader.models.optimization import optimized_weights
16
20
  from bbstrader.core.utils import TradeSignal
@@ -122,7 +126,7 @@ class MT5Strategy(Strategy):
122
126
  for asset in self.symbols:
123
127
  if asset not in weights:
124
128
  raise ValueError(f"Risk budget for asset {asset} is missing.")
125
- total_risk = sum(weights.values())
129
+ total_risk = float(round(sum(weights.values())))
126
130
  if not np.isclose(total_risk, 1.0):
127
131
  raise ValueError(f"Risk budget weights must sum to 1. got {total_risk}")
128
132
  return weights
@@ -226,7 +230,7 @@ class MT5Strategy(Strategy):
226
230
  prices = prices.dropna(axis=0, how="any")
227
231
  try:
228
232
  weights = optimized_weights(prices=prices, freq=freq, method=optimer)
229
- return {symbol: weight for symbol, weight in weights.items()}
233
+ return {symbol: abs(weight) for symbol, weight in weights.items()}
230
234
  except Exception:
231
235
  return {symbol: 0.0 for symbol in symbols}
232
236
 
@@ -605,6 +609,7 @@ class MT5Strategy(Strategy):
605
609
  bars: DataHandler = None,
606
610
  mode: Literal["backtest", "live"] = "backtest",
607
611
  tf: str = "D1",
612
+ error: Literal["ignore", "raise"] = None,
608
613
  ) -> Dict[str, np.ndarray | pd.Series] | None:
609
614
  """
610
615
  Get the historical OHLCV value or returns or custum value
@@ -618,6 +623,7 @@ class MT5Strategy(Strategy):
618
623
  mode : Mode of operation for the strategy.
619
624
  window : The lookback period for resquesting the data.
620
625
  tf : The time frame for the strategy.
626
+ error : The error handling method for the function.
621
627
 
622
628
  Returns:
623
629
  asset_values : Historical values of the assets in the symbol list.
@@ -651,6 +657,10 @@ class MT5Strategy(Strategy):
651
657
  if all(len(values) >= window for values in asset_values.values()):
652
658
  return {a: v[-window:] for a, v in asset_values.items()}
653
659
  else:
660
+ if error == "raise":
661
+ raise ValueError("Not enough data to calculate the values.")
662
+ elif error == "ignore":
663
+ return asset_values
654
664
  return None
655
665
 
656
666
  @staticmethod
@@ -763,8 +773,20 @@ class MT5Strategy(Strategy):
763
773
  return dt_to
764
774
 
765
775
  @staticmethod
766
- def get_mt5_equivalent(symbols, type="STK", path: str = None) -> List[str]:
767
- account = Account(path=path)
776
+ def get_mt5_equivalent(symbols, type="STK", **kwargs) -> List[str]:
777
+ """
778
+ Get the MetaTrader 5 equivalent symbols for the symbols in the list.
779
+ This method is used to get the symbols that are available on the MetaTrader 5 platform.
780
+
781
+ Args:
782
+ symbols : The list of symbols to get the MetaTrader 5 equivalent symbols for.
783
+ type : The type of symbols to get (e.g., STK, CFD, etc.).
784
+ **kwargs : Additional keyword arguments for the `bbstrader.metatrader.Account` object.
785
+
786
+ Returns:
787
+ mt5_equivalent : The MetaTrader 5 equivalent symbols for the symbols in the list.
788
+ """
789
+ account = Account(**kwargs)
768
790
  mt5_symbols = account.get_symbols(symbol_type=type)
769
791
  mt5_equivalent = []
770
792
  if account.broker == AdmiralMarktsGroup():
@@ -773,6 +795,11 @@ class MT5Strategy(Strategy):
773
795
  for symbol in symbols:
774
796
  if _s.split(".")[0] == symbol or _s.split("_")[0] == symbol:
775
797
  mt5_equivalent.append(s)
798
+ elif account.broker == PepperstoneGroupLimited():
799
+ for s in mt5_symbols:
800
+ for symbol in symbols:
801
+ if s.split(".")[0] == symbol:
802
+ mt5_equivalent.append(s)
776
803
  return mt5_equivalent
777
804
 
778
805
 
bbstrader/config.py CHANGED
@@ -2,16 +2,22 @@ import logging
2
2
  from pathlib import Path
3
3
  from typing import List
4
4
 
5
- AMG_PATH = "C:\\Program Files\\Admirals Group MT5 Terminal\\terminal64.exe"
6
- FTMO_PATH = "C:\\Program Files\\FTMO MetaTrader 5\\terminal64.exe"
7
- XCB_PATH = "C:\\Program Files\\4xCube MT5 Terminal\\terminal64.exe"
8
- TML_PATH = "C:\\Program Files\\Trinota Markets MetaTrader 5 Terminal\\terminal64.exe"
5
+
6
+ TERMINAL = "\\terminal64.exe"
7
+ BASE_FOLDER = "C:\\Program Files\\"
8
+
9
+ AMG_PATH = BASE_FOLDER + "Admirals Group MT5 Terminal" + TERMINAL
10
+ XCB_PATH = BASE_FOLDER + "4xCube MT5 Terminal" + TERMINAL
11
+ TML_PATH = BASE_FOLDER + "Trinota Markets MetaTrader 5 Terminal" + TERMINAL
12
+ PGL_PATH = BASE_FOLDER + "Pepperstone MetaTrader 5" + TERMINAL
13
+ FTMO_PATH = BASE_FOLDER + "FTMO MetaTrader 5" + TERMINAL
9
14
 
10
15
  BROKERS_PATHS = {
11
16
  "AMG": AMG_PATH,
12
17
  "FTMO": FTMO_PATH,
13
18
  "XCB": XCB_PATH,
14
19
  "TML": TML_PATH,
20
+ "PGL": PGL_PATH,
15
21
  }
16
22
 
17
23
 
@@ -28,6 +28,9 @@ __all__ = [
28
28
  "Broker",
29
29
  "AdmiralMarktsGroup",
30
30
  "JustGlobalMarkets",
31
+ "TrinotaMarkets",
32
+ "XCubeLimited",
33
+ "PepperstoneGroupLimited",
31
34
  "FTMO",
32
35
  ]
33
36
 
@@ -36,7 +39,8 @@ __BROKERS__ = {
36
39
  "JGM": "Just Global Markets Ltd.",
37
40
  "FTMO": "FTMO S.R.O.",
38
41
  "XCB": "4xCube Limited",
39
- "TML": "Trinota Markets (Global) Limited"
42
+ "TML": "Trinota Markets (Global) Limited",
43
+ "PGL": "Pepperstone Group Limited",
40
44
  }
41
45
 
42
46
  BROKERS_TIMEZONES = {
@@ -45,6 +49,7 @@ BROKERS_TIMEZONES = {
45
49
  "FTMO": "Europe/Helsinki",
46
50
  "XCB": "Europe/Helsinki",
47
51
  "TML": "Europe/Helsinki",
52
+ "PGL": "Europe/Helsinki",
48
53
  }
49
54
 
50
55
  _ADMIRAL_MARKETS_URL_ = (
@@ -52,8 +57,6 @@ _ADMIRAL_MARKETS_URL_ = (
52
57
  )
53
58
  _JUST_MARKETS_URL_ = "https://one.justmarkets.link/a/tufvj0xugm/registration/trader"
54
59
  _FTMO_URL_ = "https://trader.ftmo.com/?affiliates=JGmeuQqepAZLMcdOEQRp"
55
- _XCB_URL_ = ""
56
- _TML_URL_ = ""
57
60
  _ADMIRAL_MARKETS_PRODUCTS_ = [
58
61
  "Stocks",
59
62
  "ETFs",
@@ -72,27 +75,26 @@ INIT_MSG = (
72
75
  f"* If you want to trade {', '.join(_ADMIRAL_MARKETS_PRODUCTS_)}, See [{_ADMIRAL_MARKETS_URL_}]\n"
73
76
  f"* If you want to trade {', '.join(_JUST_MARKETS_PRODUCTS_)}, See [{_JUST_MARKETS_URL_}]\n"
74
77
  f"* If you are looking for a prop firm, See [{_FTMO_URL_}]\n"
75
- f"* You can also look at 4xCube Limited [{_XCB_URL_}] \n and Trinota Markets (Global) Limited [{_TML_URL_}]\n"
76
78
  )
77
79
 
78
80
  amg_url = _ADMIRAL_MARKETS_URL_
79
81
  jgm_url = _JUST_MARKETS_URL_
80
82
  ftmo_url = _FTMO_URL_
81
- xcb_url = _XCB_URL_
82
- tml_url = _TML_URL_
83
+
83
84
 
84
85
  _SYMBOLS_TYPE_ = {
85
- "STK": r"\b(Stocks?|Equities?|Shares?)\b",
86
86
  "ETF": r"\b(ETFs?)\b",
87
- "IDX": r"\b(?:Indices?|Cash|Index)\b(?!.*\\(?:UKOIL|USOIL))",
87
+ "BOND": r"\b(Treasuries?)\b",
88
88
  "FX": r"\b(Forex|Exotics?)\b",
89
- "COMD": r"\b(Metals?|Agricultures?|Energies?|OIL|Oil|USOIL|UKOIL)\b",
90
- "FUT": r"\b(Futures?)\b",
89
+ "FUT": r"\b(Futures?|Forwards)\b",
90
+ "STK": r"\b(Stocks?|Equities?|Shares?)\b",
91
+ "IDX": r"\b(?:Indices?|Cash|Index)\b(?!.*\\(?:UKOIL|USOIL))",
92
+ "COMD": r"\b(Commodity|Commodities?|Metals?|Agricultures?|Energies?|OIL|Oil|USOIL|UKOIL)\b",
91
93
  "CRYPTO": r"\b(Cryptos?|Cryptocurrencies|Cryptocurrency)\b",
92
94
  }
93
95
 
94
96
  _COUNTRY_MAP_ = {
95
- "USA": r"\b(US)\b",
97
+ "USA": r"\b(US|USA)\b",
96
98
  "AUS": r"\b(Australia)\b",
97
99
  "BEL": r"\b(Belgium)\b",
98
100
  "DNK": r"\b(Denmark)\b",
@@ -106,6 +108,9 @@ _COUNTRY_MAP_ = {
106
108
  "SWE": r"\b(Sweden)\b",
107
109
  "GBR": r"\b(UK)\b",
108
110
  "CHE": r"\b(Switzerland)\b",
111
+ "HKG": r"\b(Hong Kong)\b",
112
+ "IRL": r"\b(Ireland)\b",
113
+ "AUT": r"\b(Austria)\b",
109
114
  }
110
115
 
111
116
  AMG_EXCHANGES = {
@@ -205,6 +210,9 @@ class Broker(object):
205
210
  def __ne__(self, orther) -> bool:
206
211
  return self.name != orther.name
207
212
 
213
+ def __repr__(self):
214
+ return f"{self.__class__.__name__}({self.name})"
215
+
208
216
 
209
217
  class AdmiralMarktsGroup(Broker):
210
218
  def __init__(self, **kwargs):
@@ -232,6 +240,7 @@ class FTMO(Broker):
232
240
  def timezone(self) -> str:
233
241
  return BROKERS_TIMEZONES["FTMO"]
234
242
 
243
+
235
244
  class XCubeLimited(Broker):
236
245
  def __init__(self, **kwargs):
237
246
  super().__init__("4xCube Limited", **kwargs)
@@ -239,7 +248,8 @@ class XCubeLimited(Broker):
239
248
  @property
240
249
  def timezone(self) -> str:
241
250
  return BROKERS_TIMEZONES["XCB"]
242
-
251
+
252
+
243
253
  class TrinotaMarkets(Broker):
244
254
  def __init__(self, **kwargs):
245
255
  super().__init__("Trinota Markets (Global) Limited", **kwargs)
@@ -249,6 +259,15 @@ class TrinotaMarkets(Broker):
249
259
  return BROKERS_TIMEZONES["TML"]
250
260
 
251
261
 
262
+ class PepperstoneGroupLimited(Broker):
263
+ def __init__(self, **kwargs):
264
+ super().__init__("Pepperstone Group Limited", **kwargs)
265
+
266
+ @property
267
+ def timezone(self) -> str:
268
+ return BROKERS_TIMEZONES["PGL"]
269
+
270
+
252
271
  class AMP(Broker): ...
253
272
 
254
273
 
@@ -258,6 +277,7 @@ BROKERS: Dict[str, Broker] = {
258
277
  "JGM": JustGlobalMarkets(),
259
278
  "XCB": XCubeLimited(),
260
279
  "TML": TrinotaMarkets(),
280
+ "PGL": PepperstoneGroupLimited(),
261
281
  }
262
282
 
263
283
 
@@ -316,8 +336,6 @@ class Account(object):
316
336
  f"For {supported['AMG'].name}, See [{amg_url}]\n"
317
337
  f"For {supported['JGM'].name}, See [{jgm_url}]\n"
318
338
  f"For {supported['FTMO'].name}, See [{ftmo_url}]\n"
319
- f"For {supported['XCB'].name}, See [{xcb_url}]\n"
320
- f"For {supported['TML'].name}, See [{tml_url}]\n"
321
339
  )
322
340
  raise InvalidBroker(message=msg)
323
341
 
@@ -584,6 +602,7 @@ class Account(object):
584
602
  - `COMD`: Commodities (e.g., 'CRUDOIL', 'GOLD')
585
603
  - `FUT`: Futures (e.g., 'USTNote_U4'),
586
604
  - `CRYPTO`: Cryptocurrencies (e.g., 'BTC', 'ETH')
605
+ - `BOND`: Bonds (e.g., 'USTN10YR')
587
606
 
588
607
  check_etf (bool): If True and symbol_type is 'etf', check if the
589
608
  ETF description contains 'ETF'.
@@ -669,6 +688,7 @@ class Account(object):
669
688
  "COMD": "Commodities",
670
689
  "FUT": "Futures",
671
690
  "CRYPTO": "Cryptos Assets",
691
+ "BOND": "Bonds",
672
692
  }
673
693
  print(f"Total {names[symbol_type]}: {len(symbol_list)}")
674
694
 
@@ -684,7 +704,7 @@ class Account(object):
684
704
 
685
705
  def get_symbol_type(
686
706
  self, symbol: str
687
- ) -> Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "unknown"]:
707
+ ) -> Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "BOND", "unknown"]:
688
708
  """
689
709
  Determines the type of a given financial instrument symbol.
690
710
 
@@ -692,7 +712,7 @@ class Account(object):
692
712
  symbol (str): The symbol of the financial instrument (e.g., `GOOGL`, `EURUSD`).
693
713
 
694
714
  Returns:
695
- Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "unknown"]:
715
+ Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "BOND", "unknown"]:
696
716
  The type of the financial instrument, one of the following:
697
717
 
698
718
  - `STK`: For Stocks (e.g., `GOOGL`)
@@ -702,16 +722,27 @@ class Account(object):
702
722
  - `COMD`: For Commodities (e.g., `CRUDOIL`, `GOLD`)
703
723
  - `FUT` : For Futures (e.g., `USTNote_U4`)
704
724
  - `CRYPTO`: For Cryptocurrencies (e.g., `BTC`, `ETH`)
725
+ - `BOND`: For Bonds (e.g., `USTN10YR`)
705
726
 
706
727
  Returns `unknown` if the type cannot be determined.
707
728
  """
708
729
 
709
730
  patterns = _SYMBOLS_TYPE_
710
731
  info = self.get_symbol_info(symbol)
732
+ indices = self.get_symbols(symbol_type="IDX")
733
+ commodity = self.get_symbols(symbol_type="COMD")
711
734
  if info is not None:
712
735
  for symbol_type, pattern in patterns.items():
713
- match = re.search(pattern, info.path) # , re.IGNORECASE
714
- if match:
736
+ if (
737
+ symbol_type in ["IDX", "COMD"]
738
+ and self.broker == PepperstoneGroupLimited()
739
+ and info.name.endswith("-F")
740
+ and info.name in indices + commodity
741
+ ):
742
+ symbol_type = "FUT"
743
+ pattern = r"\b(Forwards?)\b"
744
+ search = re.compile(pattern)
745
+ if re.search(search, info.path):
715
746
  return symbol_type
716
747
  return "unknown"
717
748
 
@@ -733,14 +764,15 @@ class Account(object):
733
764
  return symbol_list
734
765
 
735
766
  def get_fx_symbols(
736
- self, category: Literal["majors", "minors", "exotics"] = "majors"
767
+ self,
768
+ category: Literal["majors", "minors", "exotics", "crosses", "ndfs"] = "majors",
737
769
  ) -> List[str]:
738
770
  """
739
771
  Retrieves a list of forex symbols belonging to a specific category.
740
772
 
741
773
  Args:
742
774
  category (str, optional): The category of forex symbols to retrieve.
743
- Possible values are 'majors', 'minors', 'exotics'.
775
+ Possible values are 'majors', 'minors', 'exotics', 'crosses', 'ndfs'.
744
776
  Defaults to 'majors'.
745
777
 
746
778
  Returns:
@@ -750,20 +782,22 @@ class Account(object):
750
782
  ValueError: If an unsupported category is provided.
751
783
 
752
784
  Notes:
753
- This mthods works primarly with Admirals Group AS products,
785
+ This mthods works primarly with Admirals Group AS products and Pepperstone Group Limited,
754
786
  For other brokers use `get_symbols()` or this method will use it by default.
755
787
  """
756
- if self.broker != AdmiralMarktsGroup():
788
+ if self.broker not in [AdmiralMarktsGroup(), PepperstoneGroupLimited()]:
757
789
  return self.get_symbols(symbol_type="FX")
758
790
  else:
759
791
  fx_categories = {
760
792
  "majors": r"\b(Majors?)\b",
761
793
  "minors": r"\b(Minors?)\b",
762
794
  "exotics": r"\b(Exotics?)\b",
795
+ "crosses": r"\b(Crosses?)\b",
796
+ "ndfs": r"\b(NDFs?)\b",
763
797
  }
764
798
  return self._get_symbols_by_category("FX", category, fx_categories)
765
799
 
766
- def get_stocks_from_country(self, country_code: str = "USA", etf=True) -> List[str]:
800
+ def get_stocks_from_country(self, country_code: str = "USA", etf=False) -> List[str]:
767
801
  """
768
802
  Retrieves a list of stock symbols from a specific country.
769
803
 
@@ -782,6 +816,9 @@ class Account(object):
782
816
  * **United Kingdom:** GBR
783
817
  * **United States:** USA
784
818
  * **Switzerland:** CHE
819
+ * **Hong Kong:** HKG
820
+ * **Ireland:** IRL
821
+ * **Austria:** AUT
785
822
 
786
823
  Args:
787
824
  country (str, optional): The country code of stocks to retrieve.
@@ -794,11 +831,11 @@ class Account(object):
794
831
  ValueError: If an unsupported country is provided.
795
832
 
796
833
  Notes:
797
- This mthods works primarly with Admirals Group AS products,
834
+ This mthods works primarly with Admirals Group AS products and Pepperstone Group Limited,
798
835
  For other brokers use `get_symbols()` or this method will use it by default.
799
836
  """
800
837
 
801
- if self.broker != AdmiralMarktsGroup():
838
+ if self.broker not in [AdmiralMarktsGroup(), PepperstoneGroupLimited()]:
802
839
  stocks = self.get_symbols(symbol_type="STK")
803
840
  return stocks
804
841
  else:
@@ -11,7 +11,6 @@ from bbstrader.metatrader.utils import TIMEFRAMES, TimeFrame
11
11
 
12
12
  _COMMD_SUPPORTED_ = [
13
13
  "GOLD",
14
- "XAUEUR",
15
14
  "SILVER",
16
15
  "BRENT",
17
16
  "CRUDOIL",
@@ -19,11 +18,17 @@ _COMMD_SUPPORTED_ = [
19
18
  "UKOIL",
20
19
  "XAGEUR",
21
20
  "XAGUSD",
21
+ "XAGAUD",
22
+ "XAGGBP",
22
23
  "XAUAUD",
23
24
  "XAUEUR",
24
25
  "XAUUSD",
25
26
  "XAUGBP",
26
27
  "USOIL",
28
+ "SpotCrude",
29
+ "SpotBrent",
30
+ "NatGas",
31
+ "Soybeans",
27
32
  ]
28
33
 
29
34
  _ADMIRAL_MARKETS_FUTURES_ = [
@@ -54,6 +59,27 @@ _ADMIRAL_MARKETS_FUTURES_ = [
54
59
  "_HSCEI50_",
55
60
  ]
56
61
 
62
+ __PEPPERSTONE_FUTURES__ = [
63
+ "AUS200-F",
64
+ "GER40-F",
65
+ "HK50-F",
66
+ "JPN225-F",
67
+ "UK100-F",
68
+ "US30-F",
69
+ "NAS100-F",
70
+ "US500-F",
71
+ "Crude-F",
72
+ "Brent-F",
73
+ "XAUUSD-F",
74
+ "XAGUSD-F",
75
+ "USDX-F",
76
+ "EUSTX50-F",
77
+ "FRA40-F",
78
+ "GERTEC30-F",
79
+ "SPA35-F",
80
+ "SWI20-F",
81
+ ]
82
+
57
83
  __all__ = ["RiskManagement"]
58
84
 
59
85
 
@@ -303,7 +329,9 @@ class RiskManagement(Account):
303
329
  tf_int = self._convert_time_frame(self._tf)
304
330
  interval = round((minutes / tf_int) * 252)
305
331
 
306
- rate = Rates(self.symbol, self._tf, 0, interval, **self.kwargs)
332
+ rate = Rates(
333
+ self.symbol, timeframe=self._tf, start_pos=0, count=interval, **self.kwargs
334
+ )
307
335
  returns = rate.returns * 100
308
336
  std = returns.std()
309
337
  point = self.get_symbol_info(self.symbol).point
@@ -355,7 +383,9 @@ class RiskManagement(Account):
355
383
  tf_int = self._convert_time_frame(tf)
356
384
  interval = round((minutes / tf_int) * 252)
357
385
 
358
- rate = Rates(self.symbol, tf, 0, interval, **self.kwargs)
386
+ rate = Rates(
387
+ self.symbol, timeframe=tf, start_pos=0, count=interval, **self.kwargs
388
+ )
359
389
  returns = rate.returns * 100
360
390
  p = self.get_account_info().margin_free
361
391
  mu = returns.mean()
@@ -461,7 +491,7 @@ class RiskManagement(Account):
461
491
  symbol = self.symbol.split(".")[0]
462
492
  elif self.symbol.endswith("xx"):
463
493
  symbol = self.symbol[:-2]
464
- elif self.symbol.endswith('-'):
494
+ elif self.symbol.endswith("-"):
465
495
  symbol = self.symbol[:-1]
466
496
  else:
467
497
  symbol = self.symbol
@@ -471,8 +501,12 @@ class RiskManagement(Account):
471
501
  f"Supported commodity symbols are: {', '.join(supported)}"
472
502
  )
473
503
  if FUT:
474
- supported = _ADMIRAL_MARKETS_FUTURES_
475
- if self.symbol[:-2] not in supported:
504
+ if "_" in self.symbol:
505
+ symbol = self.symbol[:-2]
506
+ else:
507
+ symbol = self.symbol
508
+ supported = _ADMIRAL_MARKETS_FUTURES_ + __PEPPERSTONE_FUTURES__
509
+ if str(symbol) not in supported:
476
510
  raise ValueError(
477
511
  f"Currency risk calculation for '{self.symbol}' is not a currently supported. \n"
478
512
  f"Supported future symbols are: {', '.join(supported)}"
@@ -129,7 +129,7 @@ class Trade(RiskManagement):
129
129
  - sl
130
130
  - tp
131
131
  - be
132
- See the RiskManagement class for more details on these parameters.
132
+ See the ``bbstrader.metatrader.risk.RiskManagement`` class for more details on these parameters.
133
133
  See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
134
134
  """
135
135
  # Call the parent class constructor first
@@ -604,6 +604,11 @@ class Trade(RiskManagement):
604
604
  result = self.send_order(request)
605
605
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
606
606
  break
607
+ elif result.retcode == Mt5.TRADE_RETCODE_INVALID_VOLUME: #10014
608
+ new_volume = int(request["volume"])
609
+ if new_volume >= 1:
610
+ request["volume"] = new_volume
611
+ result = self.send_order(request)
607
612
  elif result.retcode not in self._retcodes:
608
613
  self._retcodes.append(result.retcode)
609
614
  msg = trade_retcode_message(result.retcode)
@@ -644,25 +649,26 @@ class Trade(RiskManagement):
644
649
  if type == "BMKT" or type == "SMKT":
645
650
  self.opened_positions.append(result.order)
646
651
  positions = self.get_positions(symbol=self.symbol)
647
- for position in positions:
648
- if position.ticket == result.order:
649
- if position.type == 0:
650
- order_type = "BUY"
651
- self.buy_positions.append(position.ticket)
652
- else:
653
- order_type = "SELL"
654
- self.sell_positions.append(position.ticket)
655
- profit = round(self.get_account_info().profit, 5)
656
- order_info = (
657
- f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open,5)}, "
658
- f"Sl: @{position.sl} Tp: @{position.tp}"
659
- )
660
- self.logger.info(order_info)
661
- pos_info = (
662
- f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
663
- f"{self.get_account_info().currency}]\n"
664
- )
665
- self.logger.info(pos_info)
652
+ if positions is not None:
653
+ for position in positions:
654
+ if position.ticket == result.order:
655
+ if position.type == 0:
656
+ order_type = "BUY"
657
+ self.buy_positions.append(position.ticket)
658
+ else:
659
+ order_type = "SELL"
660
+ self.sell_positions.append(position.ticket)
661
+ profit = round(self.get_account_info().profit, 5)
662
+ order_info = (
663
+ f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open,5)}, "
664
+ f"Sl: @{position.sl} Tp: @{position.tp}"
665
+ )
666
+ self.logger.info(order_info)
667
+ pos_info = (
668
+ f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
669
+ f"{self.get_account_info().currency}]\n"
670
+ )
671
+ self.logger.info(pos_info)
666
672
  else:
667
673
  msg = trade_retcode_message(result.retcode)
668
674
  self.logger.error(
@@ -1049,7 +1055,10 @@ class Trade(RiskManagement):
1049
1055
  spread = self.get_symbol_info(self.symbol).spread
1050
1056
  fees = self.get_stats()[0]["average_fee"] * -1
1051
1057
  risk = self.currency_risk()["trade_profit"]
1052
- fees_points = round((fees / risk), 3)
1058
+ try:
1059
+ fees_points = round((fees / risk), 3)
1060
+ except ZeroDivisionError:
1061
+ fees_points = 0
1053
1062
  # If Buy
1054
1063
  if position.type == 0 and position.price_current > position.price_open:
1055
1064
  # Calculate the break-even level and price
@@ -1156,7 +1165,10 @@ class Trade(RiskManagement):
1156
1165
  point = self.get_symbol_info(self.symbol).point
1157
1166
  fees = self.get_stats()[0]["average_fee"] * -1
1158
1167
  risk = self.currency_risk()["trade_profit"]
1159
- min_be = round((fees / risk)) + 2
1168
+ try:
1169
+ min_be = round((fees / risk)) + 2
1170
+ except ZeroDivisionError:
1171
+ min_be = self.symbol_info(self.symbol).spread
1160
1172
  be = self.get_break_even()
1161
1173
  if th is not None:
1162
1174
  win_be = th
@@ -1184,7 +1196,7 @@ class Trade(RiskManagement):
1184
1196
  # The first one is the opening order
1185
1197
  # The second is the closing order
1186
1198
  history = self.get_trades_history(position=position, to_df=False)
1187
- if len(history) == 2:
1199
+ if history is not None and len(history) == 2:
1188
1200
  profit += history[1].profit
1189
1201
  commission += history[0].commission
1190
1202
  swap += history[0].swap
@@ -1455,7 +1467,7 @@ class Trade(RiskManagement):
1455
1467
  for position in self.opened_positions:
1456
1468
  time.sleep(0.1)
1457
1469
  history = self.get_trades_history(position=position, to_df=False)
1458
- if len(history) == 2:
1470
+ if history is not None and len(history) == 2:
1459
1471
  result = history[1].profit
1460
1472
  comm = history[0].commission
1461
1473
  swap = history[0].swap
@@ -110,12 +110,13 @@ def _mt5_execution(
110
110
  notify = kwargs.get("notify", False)
111
111
  signal_tickers = kwargs.get("signal_tickers", symbols)
112
112
  debug_mode = kwargs.get("debug_mode", False)
113
+ delay = kwargs.get("delay", 0)
113
114
  if notify:
114
115
  telegram = kwargs.get("telegram", False)
115
116
  bot_token = kwargs.get("bot_token")
116
117
  chat_id = kwargs.get("chat_id")
117
118
 
118
- expert_ids = kwargs.get("expert_id")
119
+ expert_ids = kwargs.get("expert_ids")
119
120
  if expert_ids is None:
120
121
  expert_ids = list(
121
122
  set([trade.expert_id for trade in trades_instances.values()])
@@ -192,8 +193,9 @@ def _mt5_execution(
192
193
  symbol_type = account.get_symbol_type(symbol)
193
194
  desc = account.get_symbol_info(symbol).description
194
195
  sigmsg = (
195
- f"SIGNAL = {signal}, SYMBOL={symbol}, TYPE={symbol_type}, DESCRIPTION={desc}, "
196
- f"PRICE={price}, STOPLIMIT={stoplimit}, STRATEGY={STRATEGY}, TIMEFRAME={time_frame}"
196
+ f"SIGNAL = {signal}, \nSYMBOL={symbol}, \nTYPE={symbol_type}, \nDESCRIPTION={desc}, "
197
+ f"\nPRICE={price}, \nSTOPLIMIT={stoplimit}, \nSTRATEGY={STRATEGY}, \nTIMEFRAME={time_frame}"
198
+ f"\nBROKER={account.broker.name}, \nTIMESTAMP={datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
197
199
  )
198
200
  msg = f"Sending {signal} Order ... SYMBOL={symbol}, STRATEGY={STRATEGY}"
199
201
  tfmsg = f"Time Frame Not completed !!! SYMBOL={symbol}, STRATEGY={STRATEGY}"
@@ -399,10 +401,11 @@ def _mt5_execution(
399
401
  elif trade_time % iter_time == 0:
400
402
  time_intervals += iter_time
401
403
  else:
402
- raise ValueError(
403
- f"iter_time must be a multiple of the {time_frame} !!!"
404
- f"(e.g., if time_frame is 15m, iter_time must be 1.5, 3, 3, 15 etc)"
405
- )
404
+ if use_trade_time:
405
+ raise ValueError(
406
+ f"iter_time must be a multiple of the {time_frame} !!!"
407
+ f"(e.g., if time_frame is 15m, iter_time must be 1.5, 3, 5, 15 etc)"
408
+ )
406
409
  try:
407
410
  FRIDAY = "friday"
408
411
  check_mt5_connection(**kwargs)
@@ -449,15 +452,15 @@ def _mt5_execution(
449
452
  period_end_action == "sleep" and today != FRIDAY or not closing
450
453
  ):
451
454
  sleep_time = trades_instances[symbols[-1]].sleep_time()
452
- sleepmsg(sleep_time)
453
- time.sleep(60 * sleep_time)
455
+ sleepmsg(sleep_time + delay)
456
+ time.sleep(60 * sleep_time + delay)
454
457
  logger.info(sessionmsg)
455
458
  elif period_end_action == "sleep" and today == FRIDAY:
456
459
  sleep_time = trades_instances[symbols[-1]].sleep_time(
457
460
  weekend=True
458
461
  )
459
- sleepmsg(sleep_time)
460
- time.sleep(60 * sleep_time)
462
+ sleepmsg(sleep_time + delay)
463
+ time.sleep(60 * sleep_time + delay)
461
464
  logger.info(sessionmsg)
462
465
 
463
466
  elif period.lower() == "week":
@@ -476,8 +479,8 @@ def _mt5_execution(
476
479
 
477
480
  if day_end and today != FRIDAY:
478
481
  sleep_time = trades_instances[symbols[-1]].sleep_time()
479
- sleepmsg(sleep_time)
480
- time.sleep(60 * sleep_time)
482
+ sleepmsg(sleep_time + delay)
483
+ time.sleep(60 * sleep_time + delay)
481
484
  logger.info(sessionmsg)
482
485
  elif day_end and today == FRIDAY:
483
486
  strategy.perform_period_end_checks()
@@ -487,8 +490,8 @@ def _mt5_execution(
487
490
  sleep_time = trades_instances[symbols[-1]].sleep_time(
488
491
  weekend=True
489
492
  )
490
- sleepmsg(sleep_time)
491
- time.sleep(60 * sleep_time)
493
+ sleepmsg(sleep_time + delay)
494
+ time.sleep(60 * sleep_time + delay)
492
495
  logger.info(sessionmsg)
493
496
 
494
497
  elif period.lower() == "month":
@@ -501,7 +504,7 @@ def _mt5_execution(
501
504
  elif (
502
505
  trade.days_end()
503
506
  and today == FRIDAY
504
- and num_days / len(symbols) >= 20
507
+ and num_days >= 20
505
508
  ) and closing:
506
509
  for id in expert_ids:
507
510
  trade.close_positions(
@@ -511,17 +514,17 @@ def _mt5_execution(
511
514
  trade.statistics(save=True)
512
515
  if day_end and today != FRIDAY:
513
516
  sleep_time = trades_instances[symbols[-1]].sleep_time()
514
- sleepmsg(sleep_time)
515
- time.sleep(60 * sleep_time)
517
+ sleepmsg(sleep_time + delay)
518
+ time.sleep(60 * sleep_time + delay)
516
519
  logger.info(sessionmsg)
517
520
  num_days += 1
518
521
  elif day_end and today == FRIDAY:
519
522
  sleep_time = trades_instances[symbols[-1]].sleep_time(weekend=True)
520
- sleepmsg(sleep_time)
521
- time.sleep(60 * sleep_time)
523
+ sleepmsg(sleep_time + delay)
524
+ time.sleep(60 * sleep_time + delay)
522
525
  logger.info(sessionmsg)
523
526
  num_days += 1
524
- elif day_end and today == FRIDAY and num_days / len(symbols) >= 20:
527
+ elif day_end and today == FRIDAY and num_days >= 20:
525
528
  strategy.perform_period_end_checks()
526
529
  break
527
530
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: bbstrader
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Simplified Investment & Trading Toolkit
5
5
  Home-page: https://github.com/bbalouki/bbstrader
6
6
  Download-URL: https://pypi.org/project/bbstrader/
@@ -24,7 +24,7 @@ Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: pandas
26
26
  Requires-Dist: pandas_ta
27
- Requires-Dist: numpy==1.26.4
27
+ Requires-Dist: numpy<2.0.0
28
28
  Requires-Dist: yfinance
29
29
  Requires-Dist: scipy
30
30
  Requires-Dist: hmmlearn
@@ -74,6 +74,13 @@ Dynamic: summary
74
74
  ![bbstrader](https://github.com/bbalouki/bbstrader/blob/main/assets/bbstrader_logo.png?raw=true)
75
75
 
76
76
  [![Documentation Status](https://readthedocs.org/projects/bbstrader/badge/?version=latest)](https://bbstrader.readthedocs.io/en/latest/?badge=latest)
77
+ [![PYPI Version](https://img.shields.io/pypi/v/bbstrader)](https://pypi.org/project/bbstrader/)
78
+ [![PyPi status](https://img.shields.io/pypi/status/bbstrader.svg?maxAge=60)](https://pypi.python.org/pypi/bbstrader)
79
+ [![Supported Python Versions](https://img.shields.io/pypi/pyversions/bbstrader)](https://pypi.org/project/bbstrader/)
80
+ [![PyPI Downloads](https://static.pepy.tech/badge/bbstrader)](https://pepy.tech/projects/bbstrader)
81
+ [![CodeFactor](https://www.codefactor.io/repository/github/bbalouki/bbstrader/badge)](https://www.codefactor.io/repository/github/bbalouki/bbstrader)
82
+ [![LinkedIn](https://img.shields.io/badge/LinkedIn-grey?logo=Linkedin&logoColor=white)](https://www.linkedin.com/in/bertin-balouki-simyeli-15b17a1a6/)
83
+ [![PayPal Me](https://img.shields.io/badge/PayPal%20Me-blue?logo=paypal)](https://paypal.me/bertinbalouki?country.x=SN&locale.x=en_US)
77
84
 
78
85
  [Dcoumentation](https://bbstrader.readthedocs.io/en/latest/index.html)
79
86
 
@@ -86,7 +93,7 @@ BBSTrader is a trading system suite developed for MetaTrader 5 (MT5) and IBKR pl
86
93
  - **Backtesting Module (btengine)** : Enables traders to rigorously test their trading strategies using historical data to evaluate performance before live deployment.
87
94
  - **Trading Strategies Module**: A collection of predefined trading strategies, including ARIMA+GARCH models, Kalman Filters, and Simple Moving Averages, equipped with risk management through Hidden Markov Models.
88
95
  - **MetaTrader5 Module (metatrader)**: Facilitates the direct execution of trading strategies on the MetaTrader 5 platform, supporting real-time trading across multiple financial instruments.
89
- - **Modles Module**: Serves as a framework for implementing various types of financial models (risk managment models, Machine learing models etc).
96
+ - **Models Module**: Serves as a framework for implementing various types of financial models (risk managment models, Machine learing models etc).
90
97
  - **Time serie Module (tseries)** designed for conducting advanced time series analysis in financial markets.
91
98
  It leverages statistical models and algorithms to perform tasks such as cointegration testing, volatility modeling, and filter-based estimation to assist in trading strategy development, market analysis, and financial data exploration.
92
99
 
@@ -1,24 +1,24 @@
1
- bbstrader/__ini__.py,sha256=c2TwxnJi0PtvigdAbAS2wsTAd7CnvwRHUrYr-s4wEsI,567
2
- bbstrader/config.py,sha256=mdzx-xKLnEOpVYkS-0EarEl1BI1Mhcwgp7WgymJUgD0,4086
1
+ bbstrader/__ini__.py,sha256=x9sw2BKGnPi3QF4MwBqJpCpyUgui1StbvBWGNISVAhQ,548
2
+ bbstrader/config.py,sha256=USwdS5qaGbc1Wp5rF0ckP3R1HEZJ8tY0tkZX4CkgNoc,4204
3
3
  bbstrader/tseries.py,sha256=GYNDo03dYEnYHwcQYKpclNDGCwvZ_qAPyou0vhQndS0,69851
4
4
  bbstrader/btengine/__init__.py,sha256=FL0kC0NcsnlTH-yuTv4lu6AexY1wZKN1AQ9rv9MZagQ,3009
5
5
  bbstrader/btengine/backtest.py,sha256=ZzGhoN-_g0cF-OCyk173imze2OXEhykHTUiJ9MowDO8,14582
6
6
  bbstrader/btengine/data.py,sha256=qVDk-cIgtWAeBM5eW08j9MFXBzu4yeWChoFIL9WINYI,26899
7
7
  bbstrader/btengine/event.py,sha256=38mhZH9d53C4x7bZER2B0O6M18txzS4u7zveKyxeP5Y,8603
8
- bbstrader/btengine/execution.py,sha256=DTsONYzc6B3wgPVxhCjWCgQhPofMJQmVOycBVkhIdgM,10368
8
+ bbstrader/btengine/execution.py,sha256=6YfErbqJx2DTy6r4cfZLU8F1YsJG-p8jEhNepdb9Sxc,10376
9
9
  bbstrader/btengine/performance.py,sha256=0meGbMFYzzI9n_09qf4RFpdyqQmCa6C_iu6PvE2POIE,10787
10
10
  bbstrader/btengine/portfolio.py,sha256=M97FONcdZRoSKFFF66LRsO8_KLXr128AI5-LULhMiKk,16167
11
- bbstrader/btengine/strategy.py,sha256=21LSf3Jn2WBbqj9x0a8voU_xw_Q7_Uja-NLX00FRE9U,31749
11
+ bbstrader/btengine/strategy.py,sha256=EiE1P_V0wlFIsA-2U5POQ7qtRMkr1OTH1pQia4jBcGY,32937
12
12
  bbstrader/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  bbstrader/core/data.py,sha256=3_odj9jxokyKU15j1aHTlgLQDjW75tKqGpCUfkpYq2Q,452
14
14
  bbstrader/core/utils.py,sha256=oB4OC0tQDJ1FIaJCuNWUlTzOccHoACSJsP_f7ELrqXQ,1448
15
15
  bbstrader/ibkr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  bbstrader/ibkr/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  bbstrader/metatrader/__init__.py,sha256=rrL_EtecsOCD2Cwhmpgta5CjSGT0K6vzSBiQoyCLe3M,283
18
- bbstrader/metatrader/account.py,sha256=uuHZcc_bJOzrCIrXzMoMtkl41EVPep1jqNrOMfYl14E,56895
18
+ bbstrader/metatrader/account.py,sha256=Le46-lFqYm4VGQn3nPl1DUVCuiuxNi3EVf56kF8gA5Y,58221
19
19
  bbstrader/metatrader/rates.py,sha256=12hNcjRSKcOSjBAhDvpakqAiLaI0ndU6oBhCTIWDzr0,21089
20
- bbstrader/metatrader/risk.py,sha256=pflsprtnEG8BgudmV_IiQvxuUty4bKYnnzZ6s1Yplug,26910
21
- bbstrader/metatrader/trade.py,sha256=zlVzz5-z-1nJe3-BefTEHed6zQj6GOWkjHlALguoov4,72974
20
+ bbstrader/metatrader/risk.py,sha256=H8Iz93g3EVnb9bOdi4RSFF9JN1CY-IutYHs9-epBTpQ,27579
21
+ bbstrader/metatrader/trade.py,sha256=k6IeKaC83sH_th9uNxeEIlNBzVoSzK1J-0jKIDIA7rc,73639
22
22
  bbstrader/metatrader/utils.py,sha256=UnwWmmfgY-Cw1V0vL14ehuWr9AhjcJMtVZK8k19b0i4,17672
23
23
  bbstrader/models/__init__.py,sha256=SnGBMQ-zcUIpms3oNeqg7EVDFpg-7OPjNAD8kvi_Q84,508
24
24
  bbstrader/models/factors.py,sha256=dWuXh83hLkwxUp3XwjgUl-r3_cjVcV_s0aFRlSLIfo8,13332
@@ -27,11 +27,11 @@ bbstrader/models/optimization.py,sha256=gp0n9a9vwbUldaNiZUYry_4RP2NW0VFZ2k5NoOkz
27
27
  bbstrader/models/portfolio.py,sha256=-Zq9cmzyOZUlGq9RWfAxClpX0KJZqYZYpc5EGNTcPGI,8302
28
28
  bbstrader/models/risk.py,sha256=IFQoHXxpBwJiifANRgwyAUOp7EgTWBAhfJFCO1sGR3g,15405
29
29
  bbstrader/trading/__init__.py,sha256=2VoxbzfP1XBLVuxJtjRhjEBCtnv9HqwQzfMV4B5mM7M,468
30
- bbstrader/trading/execution.py,sha256=3HlsgXcCqG6P4_F3d5uUQPVE-clsX6_frxyo3fiEuRA,30723
30
+ bbstrader/trading/execution.py,sha256=fFywdEJXtVSSxKNGTpRnFHiprTI1jmDNd94Ox1F1aU4,30999
31
31
  bbstrader/trading/scripts.py,sha256=pNwHr-3mW87G5fyIMd93wS43NkzOZn4npt4fLNnSUyk,1922
32
32
  bbstrader/trading/strategies.py,sha256=rMvLIhX_8MQg7_Lbo127UqdTRxBUof2m3jgRQTm55p0,37019
33
- bbstrader-0.2.5.dist-info/LICENSE,sha256=P3PBO9RuYPzl6-PkjysTNnwmwMB64ph36Bz9DBj8MS4,1115
34
- bbstrader-0.2.5.dist-info/METADATA,sha256=vvgqvdWfhKFQcQdWlpHheGbOkGBcegJHmkk2NVyyLsc,10399
35
- bbstrader-0.2.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
36
- bbstrader-0.2.5.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
37
- bbstrader-0.2.5.dist-info/RECORD,,
33
+ bbstrader-0.2.7.dist-info/LICENSE,sha256=P3PBO9RuYPzl6-PkjysTNnwmwMB64ph36Bz9DBj8MS4,1115
34
+ bbstrader-0.2.7.dist-info/METADATA,sha256=Ce_IeZ9fc-yb5dGN3pmq60woLrcos_WBi3T_Tras30k,11271
35
+ bbstrader-0.2.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
36
+ bbstrader-0.2.7.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
37
+ bbstrader-0.2.7.dist-info/RECORD,,