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,47 +1,37 @@
1
- import re
2
1
  import os
3
- import pandas as pd
2
+ import re
4
3
  import urllib.request
5
4
  from datetime import datetime
5
+ from typing import Any, Dict, List, Literal, Optional, Tuple, Union
6
+
6
7
  import MetaTrader5 as mt5
8
+ import pandas as pd
9
+ from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
7
10
  from dotenv import load_dotenv
8
- from currency_converter import (
9
- SINGLE_DAY_ECB_URL,
10
- CurrencyConverter
11
- )
11
+
12
12
  from bbstrader.metatrader.utils import (
13
- raise_mt5_error,
14
- AccountInfo,
15
- TerminalInfo,
13
+ AccountInfo,
16
14
  InvalidBroker,
17
- SymbolInfo,
18
- TickInfo,
19
- TradeRequest,
20
15
  OrderCheckResult,
21
- OrderSentResult,
22
- TradePosition,
23
- TradeOrder,
16
+ OrderSentResult,
17
+ SymbolInfo,
18
+ TerminalInfo,
19
+ TickInfo,
24
20
  TradeDeal,
25
- )
26
- from typing import (
27
- Tuple,
28
- Union,
29
- List,
30
- Dict,
31
- Any,
32
- Optional,
33
- Literal,
34
- overload
21
+ TradeOrder,
22
+ TradePosition,
23
+ TradeRequest,
24
+ raise_mt5_error,
35
25
  )
36
26
 
37
27
  load_dotenv()
38
28
 
39
29
  __all__ = [
40
- "Account",
41
- "Broker",
42
- "AdmiralMarktsGroup",
43
- "JustGlobalMarkets",
44
- "FTMO",
30
+ "Account",
31
+ "Broker",
32
+ "AdmiralMarktsGroup",
33
+ "JustGlobalMarkets",
34
+ "FTMO",
45
35
  ]
46
36
 
47
37
  __BROKERS__ = {
@@ -58,9 +48,10 @@ BROKERS_TIMEZONES = {
58
48
 
59
49
  _ADMIRAL_MARKETS_URL_ = os.getenv("ADMIRAL_MARKETS_URL")
60
50
  _ADMIRAL_MARKETS_PRODUCTS_ = ["Stocks", "ETFs",
61
- "Indices", "Commodities", "Futures", "Forex"]
51
+ "Indices", "Commodities", "Futures", "Forex"]
62
52
  _JUST_MARKETS_URL_ = os.getenv("JUST_MARKETS_URL")
63
- _JUST_MARKETS_PRODUCTS_ = ["Stocks", "Crypto", "indices", "Commodities", "Forex"]
53
+ _JUST_MARKETS_PRODUCTS_ = ["Stocks", "Crypto",
54
+ "indices", "Commodities", "Forex"]
64
55
  _FTMO_URL_ = os.getenv("FTMO_URL")
65
56
 
66
57
  INIT_MSG = (
@@ -125,6 +116,7 @@ AMG_EXCHANGES = {
125
116
  'XSWX': r"Switzerland.*\(SWX\)"
126
117
  }
127
118
 
119
+
128
120
  def check_mt5_connection(**kwargs):
129
121
  """
130
122
  Initialize the connection to the MetaTrader 5 terminal.
@@ -139,29 +131,29 @@ def check_mt5_connection(**kwargs):
139
131
  timeout (int, optional): Connection timeout in milliseconds. Defaults to 60_000.
140
132
  portable (bool, optional): If True, the portable mode of the terminal is used.
141
133
  Defaults to False (See https://www.metatrader5.com/en/terminal/help/start_advanced/start#portable).
142
-
134
+
143
135
  Notes:
144
136
  If you want to lunch multiple terminal instances:
145
137
  - Follow these instructions to lunch each terminal in portable mode first:
146
138
  https://www.metatrader5.com/en/terminal/help/start_advanced/start#configuration_file
147
139
  """
148
- path = kwargs.get('path', None)
149
- login = kwargs.get('login', None)
140
+ path = kwargs.get('path', None)
141
+ login = kwargs.get('login', None)
150
142
  password = kwargs.get('password', None)
151
- server = kwargs.get('server', None)
152
- timeout = kwargs.get('timeout', 60_000)
143
+ server = kwargs.get('server', None)
144
+ timeout = kwargs.get('timeout', 60_000)
153
145
  portable = kwargs.get('portable', False)
154
146
 
155
147
  if path is None and (login or password or server):
156
148
  raise ValueError(
157
- f"You must provide a path to the terminal executable file"
158
- f"when providing login, password or server"
149
+ "You must provide a path to the terminal executable file"
150
+ "when providing login, password or server"
159
151
  )
160
152
  try:
161
153
  if path is not None:
162
154
  if (
163
- login is not None and
164
- password is not None and
155
+ login is not None and
156
+ password is not None and
165
157
  server is not None
166
158
  ):
167
159
  init = mt5.initialize(
@@ -183,23 +175,23 @@ def check_mt5_connection(**kwargs):
183
175
 
184
176
 
185
177
  class Broker(object):
186
- def __init__(self, name: str=None, **kwargs):
178
+ def __init__(self, name: str = None, **kwargs):
187
179
  if name is None:
188
180
  check_mt5_connection(**kwargs)
189
181
  self._name = mt5.account_info().company
190
182
  else:
191
183
  self._name = name
192
-
184
+
193
185
  @property
194
186
  def name(self):
195
187
  return self._name
196
-
188
+
197
189
  def __str__(self):
198
190
  return self.name
199
-
191
+
200
192
  def __eq__(self, orther) -> bool:
201
193
  return self.name == orther.name
202
-
194
+
203
195
  def __ne__(self, orther) -> bool:
204
196
  return self.name != orther.name
205
197
 
@@ -207,7 +199,7 @@ class Broker(object):
207
199
  class AdmiralMarktsGroup(Broker):
208
200
  def __init__(self, **kwargs):
209
201
  super().__init__("Admirals Group AS", **kwargs)
210
-
202
+
211
203
  @property
212
204
  def timezone(self) -> str:
213
205
  return BROKERS_TIMEZONES['AMG']
@@ -225,11 +217,11 @@ class JustGlobalMarkets(Broker):
225
217
  class FTMO(Broker):
226
218
  def __init__(self, **kwargs):
227
219
  super().__init__("FTMO S.R.O.", **kwargs)
228
-
220
+
229
221
  @property
230
222
  def timezone(self) -> str:
231
223
  return BROKERS_TIMEZONES['FTMO']
232
-
224
+
233
225
 
234
226
  class AMP(Broker):
235
227
  ...
@@ -299,58 +291,58 @@ class Account(object):
299
291
  f"For {supported['FTMO'].name}, See [{ftmo_url}]\n"
300
292
  )
301
293
  raise InvalidBroker(message=msg)
302
-
294
+
303
295
  @property
304
296
  def broker(self) -> Broker:
305
297
  return Broker(self.get_terminal_info().company)
306
-
298
+
307
299
  @property
308
300
  def timezone(self) -> str:
309
301
  for broker in BROKERS.values():
310
302
  if broker == self.broker:
311
303
  return broker.timezone
312
-
304
+
313
305
  @property
314
- def name(self)-> str:
306
+ def name(self) -> str:
315
307
  return self.get_account_info().name
316
-
308
+
317
309
  @property
318
- def number(self)-> int:
310
+ def number(self) -> int:
319
311
  return self.get_account_info().login
320
-
312
+
321
313
  @property
322
- def server(self)-> str:
314
+ def server(self) -> str:
323
315
  """The name of the trade server to which the client terminal is connected.
324
316
  (e.g., 'AdmiralsGroup-Demo')
325
317
  """
326
318
  return self.get_account_info().server
327
-
319
+
328
320
  @property
329
321
  def balance(self) -> float:
330
322
  return self.get_account_info().balance
331
-
323
+
332
324
  @property
333
325
  def leverage(self) -> int:
334
326
  return self.get_account_info().leverage
335
-
327
+
336
328
  @property
337
329
  def equity(self) -> float:
338
330
  return self.get_account_info().equity
339
-
331
+
340
332
  @property
341
333
  def currency(self) -> str:
342
334
  return self.get_account_info().currency
343
-
335
+
344
336
  @property
345
337
  def language(self) -> str:
346
338
  """The language of the terminal interface."""
347
339
  return self.get_terminal_info().language
348
-
340
+
349
341
  @property
350
342
  def maxbars(self) -> int:
351
343
  """The maximal bars count on the chart."""
352
344
  return self.get_terminal_info().maxbars
353
-
345
+
354
346
  def get_account_info(
355
347
  self,
356
348
  account: Optional[int] = None,
@@ -364,11 +356,11 @@ class Account(object):
364
356
  Args:
365
357
  account (int, optinal) : MT5 Trading account number.
366
358
  password (str, optinal): MT5 Trading account password.
367
-
359
+
368
360
  server (str, optinal): MT5 Trading account server
369
361
  [Brokers or terminal server ["demo", "real"]]
370
362
  If no server is set, the last used server is applied automaticall
371
-
363
+
372
364
  timeout (int, optinal):
373
365
  Connection timeout in milliseconds. Optional named parameter.
374
366
  If not specified, the value of 60 000 (60 seconds) is applied.
@@ -516,7 +508,7 @@ class Account(object):
516
508
  os.remove(filename)
517
509
  supported = c.currencies
518
510
  if (from_c not in supported
519
- or to_c not in supported
511
+ or to_c not in supported
520
512
  ):
521
513
  rate = qty
522
514
  else:
@@ -674,7 +666,7 @@ class Account(object):
674
666
  Returns:
675
667
  Literal["STK", "ETF", "IDX", "FX", "COMD", "FUT", "CRYPTO", "unknown"]:
676
668
  The type of the financial instrument, one of the following:
677
-
669
+
678
670
  - `STK`: For Stocks (e.g., `GOOGL`)
679
671
  - `ETF`: For ETFs (e.g., `QQQ`)
680
672
  - `IDX`: For Indices (e.g., `SP500`)
@@ -682,7 +674,7 @@ class Account(object):
682
674
  - `COMD`: For Commodities (e.g., `CRUDOIL`, `GOLD`)
683
675
  - `FUT` : For Futures (e.g., `USTNote_U4`)
684
676
  - `CRYPTO`: For Cryptocurrencies (e.g., `BTC`, `ETH`)
685
-
677
+
686
678
  Returns `unknown` if the type cannot be determined.
687
679
  """
688
680
 
@@ -778,17 +770,19 @@ class Account(object):
778
770
  This mthods works primarly with Admirals Group AS products,
779
771
  For other brokers use `get_symbols()` or this method will use it by default.
780
772
  """
781
-
773
+
782
774
  if self.broker != AdmiralMarktsGroup():
783
775
  stocks = self.get_symbols(symbol_type='STK')
784
776
  return stocks
785
777
  else:
786
778
  country_map = _COUNTRY_MAP_
787
- stocks = self._get_symbols_by_category('STK', country_code, country_map)
779
+ stocks = self._get_symbols_by_category(
780
+ 'STK', country_code, country_map)
788
781
  if etf:
789
- etfs = self._get_symbols_by_category('ETF', country_code, country_map)
790
- return stocks + etfs
791
- return stocks
782
+ etfs = self._get_symbols_by_category(
783
+ 'ETF', country_code, country_map)
784
+ return stocks + etfs
785
+ return stocks
792
786
 
793
787
  def get_stocks_from_exchange(self, exchange_code: str = 'XNYS', etf=True) -> List[str]:
794
788
  """
@@ -834,9 +828,11 @@ class Account(object):
834
828
  return stocks
835
829
  else:
836
830
  exchange_map = AMG_EXCHANGES
837
- stocks = self._get_symbols_by_category('STK', exchange_code, exchange_map)
831
+ stocks = self._get_symbols_by_category(
832
+ 'STK', exchange_code, exchange_map)
838
833
  if etf:
839
- etfs = self._get_symbols_by_category('ETF', exchange_code, exchange_map)
834
+ etfs = self._get_symbols_by_category(
835
+ 'ETF', exchange_code, exchange_map)
840
836
  return stocks + etfs
841
837
  return stocks
842
838
 
@@ -861,15 +857,15 @@ class Account(object):
861
857
  """
862
858
  category = category.lower()
863
859
  if self.broker != AdmiralMarktsGroup():
864
- return self.get_symbols(symbol_type='FUT')
860
+ return self.get_symbols(symbol_type='FUT')
865
861
  elif category in ['all', 'index']:
866
862
  categories = {
867
863
  "all": r"\b(Futures?)\b",
868
- "index": r"\b(Index)\b",
864
+ "index": r"\b(Index)\b",
869
865
  }
870
866
  return self._get_symbols_by_category('FUT', category, categories)
871
867
  else:
872
- metals = []
868
+ metals = []
873
869
  energies = []
874
870
  agricultures = []
875
871
  bonds = []
@@ -916,7 +912,7 @@ class Account(object):
916
912
 
917
913
  Raises:
918
914
  MT5TerminalError: A specific exception based on the error code.
919
-
915
+
920
916
  Notes:
921
917
  The `time` property is converted to a `datetime` object using Broker server time.
922
918
  """
@@ -962,7 +958,7 @@ class Account(object):
962
958
 
963
959
  Raises:
964
960
  MT5TerminalError: A specific exception based on the error code.
965
-
961
+
966
962
  Notes:
967
963
  The `time` property is converted to a `datetime` object using Broker server time.
968
964
  """
@@ -988,10 +984,10 @@ class Account(object):
988
984
  """
989
985
  self._show_info(self.get_tick_info, "tick", symbol=symbol)
990
986
 
991
- def calculate_margin(self,
992
- action: Literal['buy', 'sell'],
993
- symbol: str,
994
- lot: float,
987
+ def calculate_margin(self,
988
+ action: Literal['buy', 'sell'],
989
+ symbol: str,
990
+ lot: float,
995
991
  price: float) -> float:
996
992
  """
997
993
  Calculate margin required for an order.
@@ -1004,7 +1000,7 @@ class Account(object):
1004
1000
 
1005
1001
  Returns:
1006
1002
  float: The margin required for the order.
1007
-
1003
+
1008
1004
  Raises:
1009
1005
  MT5TerminalError: A specific exception based on the error code.
1010
1006
  """
@@ -1020,7 +1016,7 @@ class Account(object):
1020
1016
  except Exception as e:
1021
1017
  raise_mt5_error(e)
1022
1018
 
1023
- def check_order(self,
1019
+ def check_order(self,
1024
1020
  request: Dict[str, Any]) -> OrderCheckResult:
1025
1021
  """
1026
1022
  Check funds sufficiency for performing a required trading operation.
@@ -1031,7 +1027,7 @@ class Account(object):
1031
1027
  Returns:
1032
1028
  OrderCheckResult:
1033
1029
  The check result as the `OrderCheckResult` structure.
1034
-
1030
+
1035
1031
  The `request` field in the returned structure contains the trading request passed to `check_order()`.
1036
1032
 
1037
1033
  Raises:
@@ -1052,7 +1048,7 @@ class Account(object):
1052
1048
  raise_mt5_error(e)
1053
1049
 
1054
1050
  def send_order(self,
1055
- request: Dict[str, Any]) -> OrderSentResult:
1051
+ request: Dict[str, Any]) -> OrderSentResult:
1056
1052
  """
1057
1053
  Send a request to perform a trading operation from the terminal to the trade server.
1058
1054
 
@@ -1062,7 +1058,7 @@ class Account(object):
1062
1058
  Returns:
1063
1059
  OrderSentResult:
1064
1060
  The execution result as the `OrderSentResult` structure.
1065
-
1061
+
1066
1062
  The `request` field in the returned structure contains the trading request passed to `send_order()`.
1067
1063
 
1068
1064
  Raises:
@@ -1078,11 +1074,11 @@ class Account(object):
1078
1074
  raise_mt5_error(e)
1079
1075
 
1080
1076
  def get_positions(self,
1081
- symbol: Optional[str] = None,
1082
- group: Optional[str] = None,
1083
- ticket: Optional[int] = None,
1084
- to_df: bool = False
1085
- ) -> Union[pd.DataFrame, Tuple[TradePosition], None]:
1077
+ symbol: Optional[str] = None,
1078
+ group: Optional[str] = None,
1079
+ ticket: Optional[int] = None,
1080
+ to_df: bool = False
1081
+ ) -> Union[pd.DataFrame, Tuple[TradePosition], None]:
1086
1082
  """
1087
1083
  Get open positions with the ability to filter by symbol or ticket.
1088
1084
  There are four call options:
@@ -1116,16 +1112,16 @@ class Account(object):
1116
1112
 
1117
1113
  Notes:
1118
1114
  The method allows receiving all open positions within a specified period.
1119
-
1115
+
1120
1116
  The `group` parameter may contain several comma-separated conditions.
1121
-
1117
+
1122
1118
  A condition can be set as a mask using '*'.
1123
-
1119
+
1124
1120
  The logical negation symbol '!' can be used for exclusion.
1125
-
1121
+
1126
1122
  All conditions are applied sequentially, which means conditions for inclusion
1127
1123
  in a group should be specified first, followed by an exclusion condition.
1128
-
1124
+
1129
1125
  For example, `group="*, !EUR"` means that deals for all symbols should be selected first,
1130
1126
  and those containing "EUR" in symbol names should be excluded afterward.
1131
1127
  """
@@ -1171,11 +1167,11 @@ class Account(object):
1171
1167
  with the ability to filter by `ticket` or `position`.
1172
1168
 
1173
1169
  You can call this method in the following ways:
1174
-
1170
+
1175
1171
  - Call with a `time interval`. Returns all deals falling within the specified interval.
1176
-
1172
+
1177
1173
  - Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.
1178
-
1174
+
1179
1175
  - Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.
1180
1176
 
1181
1177
  Args:
@@ -1207,16 +1203,16 @@ class Account(object):
1207
1203
 
1208
1204
  Notes:
1209
1205
  The method allows receiving all history orders within a specified period.
1210
-
1206
+
1211
1207
  The `group` parameter may contain several comma-separated conditions.
1212
-
1208
+
1213
1209
  A condition can be set as a mask using '*'.
1214
-
1210
+
1215
1211
  The logical negation symbol '!' can be used for exclusion.
1216
-
1212
+
1217
1213
  All conditions are applied sequentially, which means conditions for inclusion
1218
1214
  in a group should be specified first, followed by an exclusion condition.
1219
-
1215
+
1220
1216
  For example, `group="*, !EUR"` means that deals for all symbols should be selected first
1221
1217
  and those containing "EUR" in symbol names should be excluded afterward.
1222
1218
 
@@ -1265,11 +1261,11 @@ class Account(object):
1265
1261
  return tuple(position_deals)
1266
1262
 
1267
1263
  def get_orders(self,
1268
- symbol: Optional[str] = None,
1269
- group: Optional[str] = None,
1270
- ticket: Optional[int] = None,
1271
- to_df: bool = False
1272
- ) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
1264
+ symbol: Optional[str] = None,
1265
+ group: Optional[str] = None,
1266
+ ticket: Optional[int] = None,
1267
+ to_df: bool = False
1268
+ ) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
1273
1269
  """
1274
1270
  Get active orders with the ability to filter by symbol or ticket.
1275
1271
  There are four call options:
@@ -1353,11 +1349,11 @@ class Account(object):
1353
1349
  with the ability to filter by `ticket` or `position`.
1354
1350
 
1355
1351
  You can call this method in the following ways:
1356
-
1352
+
1357
1353
  - Call with a `time interval`. Returns all deals falling within the specified interval.
1358
-
1354
+
1359
1355
  - Call specifying the `order ticket`. Returns all deals having the specified `order ticket` in the `DEAL_ORDER` property.
1360
-
1356
+
1361
1357
  - Call specifying the `position ticket`. Returns all deals having the specified `position ticket` in the `DEAL_POSITION_ID` property.
1362
1358
 
1363
1359
  Args:
@@ -1389,16 +1385,16 @@ class Account(object):
1389
1385
 
1390
1386
  Notes:
1391
1387
  The method allows receiving all history orders within a specified period.
1392
-
1388
+
1393
1389
  The `group` parameter may contain several comma-separated conditions.
1394
-
1390
+
1395
1391
  A condition can be set as a mask using '*'.
1396
-
1392
+
1397
1393
  The logical negation symbol '!' can be used for exclusion.
1398
-
1394
+
1399
1395
  All conditions are applied sequentially, which means conditions for inclusion
1400
1396
  in a group should be specified first, followed by an exclusion condition.
1401
-
1397
+
1402
1398
  For example, `group="*, !EUR"` means that deals for all symbols should be selected first
1403
1399
  and those containing "EUR" in symbol names should be excluded afterward.
1404
1400