bbstrader 0.1.8__py3-none-any.whl → 0.1.91__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bbstrader might be problematic. Click here for more details.

@@ -4,18 +4,50 @@ import pandas as pd
4
4
  import urllib.request
5
5
  from datetime import datetime
6
6
  import MetaTrader5 as mt5
7
- from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
7
+ from dotenv import load_dotenv
8
+ from currency_converter import (
9
+ SINGLE_DAY_ECB_URL,
10
+ CurrencyConverter
11
+ )
8
12
  from bbstrader.metatrader.utils import (
9
- raise_mt5_error, AccountInfo, TerminalInfo, InvalidBroker,
10
- SymbolInfo, TickInfo, TradeRequest, OrderCheckResult,
11
- OrderSentResult, TradePosition, TradeOrder, TradeDeal,)
12
- from typing import Tuple, Union, List, Dict, Any, Optional, Literal
13
+ raise_mt5_error,
14
+ AccountInfo,
15
+ TerminalInfo,
16
+ InvalidBroker,
17
+ SymbolInfo,
18
+ TickInfo,
19
+ TradeRequest,
20
+ OrderCheckResult,
21
+ OrderSentResult,
22
+ TradePosition,
23
+ TradeOrder,
24
+ TradeDeal,
25
+ )
26
+ from typing import (
27
+ Tuple,
28
+ Union,
29
+ List,
30
+ Dict,
31
+ Any,
32
+ Optional,
33
+ Literal,
34
+ overload
35
+ )
36
+
37
+ load_dotenv()
13
38
 
39
+ __all__ = [
40
+ "Account",
41
+ "Broker",
42
+ "AdmiralMarktsGroup",
43
+ "JustGlobalMarkets",
44
+ "FTMO",
45
+ ]
14
46
 
15
47
  __BROKERS__ = {
16
- 'AMG': "Admirals Group AS",
17
- 'JGM': "Just Global Markets Ltd.",
18
- 'FTMO': "FTMO S.R.O."
48
+ 'AMG': 'Admirals Group AS',
49
+ 'JGM': 'Just Global Markets Ltd.',
50
+ 'FTMO': 'FTMO S.R.O.'
19
51
  }
20
52
 
21
53
  BROKERS_TIMEZONES = {
@@ -24,12 +56,12 @@ BROKERS_TIMEZONES = {
24
56
  'FTMO': 'Europe/Helsinki'
25
57
  }
26
58
 
27
- _ADMIRAL_MARKETS_URL_ = "https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets"
59
+ _ADMIRAL_MARKETS_URL_ = os.getenv("ADMIRAL_MARKETS_URL")
28
60
  _ADMIRAL_MARKETS_PRODUCTS_ = ["Stocks", "ETFs",
29
61
  "Indices", "Commodities", "Futures", "Forex"]
30
- _JUST_MARKETS_URL_ = "https://one.justmarkets.link/a/tufvj0xugm/registration/trader"
62
+ _JUST_MARKETS_URL_ = os.getenv("JUST_MARKETS_URL")
31
63
  _JUST_MARKETS_PRODUCTS_ = ["Stocks", "Crypto", "indices", "Commodities", "Forex"]
32
- _FTMO_URL_ = "https://trader.ftmo.com/?affiliates=JGmeuQqepAZLMcdOEQRp"
64
+ _FTMO_URL_ = os.getenv("FTMO_URL")
33
65
 
34
66
  INIT_MSG = (
35
67
  f"\n* Ensure you have a good and stable internet connexion\n"
@@ -54,6 +86,114 @@ _SYMBOLS_TYPE_ = {
54
86
  "CRYPTO": r'\b(Cryptos?)\b'
55
87
  }
56
88
 
89
+ _COUNTRY_MAP_ = {
90
+ "USA": r"\b(US)\b",
91
+ "AUS": r"\b(Australia)\b",
92
+ "BEL": r"\b(Belgium)\b",
93
+ "DNK": r"\b(Denmark)\b",
94
+ "FIN": r"\b(Finland)\b",
95
+ "FRA": r"\b(France)\b",
96
+ "DEU": r"\b(Germany)\b",
97
+ "NLD": r"\b(Netherlands)\b",
98
+ "NOR": r"\b(Norway)\b",
99
+ "PRT": r"\b(Portugal)\b",
100
+ "ESP": r"\b(Spain)\b",
101
+ "SWE": r"\b(Sweden)\b",
102
+ "GBR": r"\b(UK)\b",
103
+ "CHE": r"\b(Switzerland)\b",
104
+ }
105
+
106
+ AMG_EXCHANGES = {
107
+ 'XASX': r"Australia.*\(ASX\)",
108
+ 'XBRU': r"Belgium.*\(Euronext\)",
109
+ 'XCSE': r"Denmark.*\(CSE\)",
110
+ 'XHEL': r"Finland.*\(NASDAQ\)",
111
+ 'XPAR': r"France.*\(Euronext\)",
112
+ 'XETR': r"Germany.*\(Xetra\)",
113
+ 'XAMS': r"Netherlands.*\(Euronext\)",
114
+ 'XOSL': r"Norway.*\(NASDAQ\)",
115
+ 'XLIS': r"Portugal.*\(Euronext\)",
116
+ 'XMAD': r"Spain.*\(BME\)",
117
+ 'XSTO': r"Sweden.*\(NASDAQ\)",
118
+ 'XLON': r"UK.*\(LSE\)",
119
+ 'XNYS': r"US.*\((NYSE|ARCA|AMEX)\)",
120
+ 'NYSE': r"US.*\(NYSE\)",
121
+ 'ARCA': r"US.*\(ARCA\)",
122
+ 'AMEX': r"US.*\(AMEX\)",
123
+ 'NASDAQ': r"US.*\(NASDAQ\)",
124
+ 'BATS': r"US.*\(BATS\)",
125
+ 'XSWX': r"Switzerland.*\(SWX\)"
126
+ }
127
+
128
+ def check_mt5_connection():
129
+ try:
130
+ init = mt5.initialize()
131
+ if not init:
132
+ raise_mt5_error(INIT_MSG)
133
+ except Exception:
134
+ raise_mt5_error(INIT_MSG)
135
+
136
+
137
+ class Broker(object):
138
+ def __init__(self, name: str=None):
139
+ if name is None:
140
+ check_mt5_connection()
141
+ self._name = mt5.account_info().company
142
+ else:
143
+ self._name = name
144
+
145
+ @property
146
+ def name(self):
147
+ return self._name
148
+
149
+ def __str__(self):
150
+ return self.name
151
+
152
+ def __eq__(self, orther) -> bool:
153
+ return self.name == orther.name
154
+
155
+ def __ne__(self, orther) -> bool:
156
+ return self.name != orther.name
157
+
158
+
159
+ class AdmiralMarktsGroup(Broker):
160
+ def __init__(self):
161
+ super().__init__("Admirals Group AS")
162
+
163
+ @property
164
+ def timezone(self) -> str:
165
+ return BROKERS_TIMEZONES['AMG']
166
+
167
+
168
+ class JustGlobalMarkets(Broker):
169
+ def __init__(self):
170
+ super().__init__("Just Global Markets Ltd.")
171
+
172
+ @property
173
+ def timezone(self) -> str:
174
+ return BROKERS_TIMEZONES['JGM']
175
+
176
+
177
+ class FTMO(Broker):
178
+ def __init__(self):
179
+ super().__init__("FTMO S.R.O.")
180
+
181
+ @property
182
+ def timezone(self) -> str:
183
+ return BROKERS_TIMEZONES['FTMO']
184
+
185
+
186
+ class AMP(Broker):
187
+ ...
188
+
189
+
190
+ BROKERS: Dict[str, Broker] = {
191
+ 'AMG': AdmiralMarktsGroup(),
192
+ 'JGM': JustGlobalMarkets(),
193
+ 'FTMO': FTMO()
194
+ }
195
+
196
+
57
197
  class Account(object):
58
198
  """
59
199
  The `Account` class is utilized to retrieve information about
@@ -91,23 +231,72 @@ class Account(object):
91
231
  """
92
232
 
93
233
  def __init__(self):
94
- if not mt5.initialize():
95
- raise_mt5_error(message=INIT_MSG)
234
+ check_mt5_connection()
96
235
  self._check_brokers()
97
236
 
98
237
  def _check_brokers(self):
99
- supported = __BROKERS__.copy()
100
- broker = self.get_terminal_info().company
101
- if broker not in supported.values():
238
+ supported = BROKERS.copy()
239
+ if self.broker not in supported.values():
102
240
  msg = (
103
- f"{broker} is not currently supported broker for the Account() class\n"
104
- f"Currently Supported brokers are: {', '.join(supported.values())}\n"
105
- f"For {supported['AMG']}, See [{amg_url}]\n"
106
- f"For {supported['JGM']}, See [{jgm_url}]\n"
107
- f"For {supported['FTMO']}, See [{ftmo_url}]\n"
241
+ f"{self.broker.name} is not currently supported broker for the Account() class\n"
242
+ f"Currently Supported brokers are: {', '.join([b.name for b in supported.values()])}\n"
243
+ f"For {supported['AMG'].name}, See [{amg_url}]\n"
244
+ f"For {supported['JGM'].name}, See [{jgm_url}]\n"
245
+ f"For {supported['FTMO'].name}, See [{ftmo_url}]\n"
108
246
  )
109
247
  raise InvalidBroker(message=msg)
110
-
248
+
249
+ @property
250
+ def broker(self) -> Broker:
251
+ return Broker(self.get_terminal_info().company)
252
+
253
+ @property
254
+ def timezone(self) -> str:
255
+ for broker in BROKERS.values():
256
+ if broker == self.broker:
257
+ return broker.timezone
258
+
259
+ @property
260
+ def name(self)-> str:
261
+ return self.get_account_info().name
262
+
263
+ @property
264
+ def number(self)-> int:
265
+ return self.get_account_info().login
266
+
267
+ @property
268
+ def server(self)-> str:
269
+ """The name of the trade server to which the client terminal is connected.
270
+ (e.g., 'AdmiralsGroup-Demo')
271
+ """
272
+ return self.get_account_info().server
273
+
274
+ @property
275
+ def balance(self) -> float:
276
+ return self.get_account_info().balance
277
+
278
+ @property
279
+ def leverage(self) -> int:
280
+ return self.get_account_info().leverage
281
+
282
+ @property
283
+ def equity(self) -> float:
284
+ return self.get_account_info().equity
285
+
286
+ @property
287
+ def currency(self) -> str:
288
+ return self.get_account_info().currency
289
+
290
+ @property
291
+ def language(self) -> str:
292
+ """The language of the terminal interface."""
293
+ return self.get_terminal_info().language
294
+
295
+ @property
296
+ def maxbars(self) -> int:
297
+ """The maximal bars count on the chart."""
298
+ return self.get_terminal_info().maxbars
299
+
111
300
  def get_account_info(
112
301
  self,
113
302
  account: Optional[int] = None,
@@ -272,8 +461,8 @@ class Account(object):
272
461
  c = CurrencyConverter(filename)
273
462
  os.remove(filename)
274
463
  supported = c.currencies
275
- if (from_c not in supported or
276
- to_c not in supported
464
+ if (from_c not in supported
465
+ or to_c not in supported
277
466
  ):
278
467
  rate = qty
279
468
  else:
@@ -293,7 +482,7 @@ class Account(object):
293
482
 
294
483
  Exemple:
295
484
  >>> account = Account()
296
- >>> account.get_rates('EURUSD')
485
+ >>> account.get_currency_rates('EURUSD')
297
486
  {'bc': 'EUR', 'mc': 'EUR', 'pc': 'USD', 'ac': 'USD'}
298
487
  """
299
488
  info = self.get_symbol_info(symbol)
@@ -489,16 +678,19 @@ class Account(object):
489
678
 
490
679
  Notes:
491
680
  This mthods works primarly with Admirals Group AS products,
492
- For other brokers use `get_symbols()`
681
+ For other brokers use `get_symbols()` or this method will use it by default.
493
682
  """
494
- fx_categories = {
495
- "majors": r"\b(Majors?)\b",
496
- "minors": r"\b(Minors?)\b",
497
- "exotics": r"\b(Exotics?)\b",
498
- }
499
- return self._get_symbols_by_category('forex', category, fx_categories)
683
+ if self.broker != AdmiralMarktsGroup():
684
+ return self.get_symbols(symbol_type='FX')
685
+ else:
686
+ fx_categories = {
687
+ "majors": r"\b(Majors?)\b",
688
+ "minors": r"\b(Minors?)\b",
689
+ "exotics": r"\b(Exotics?)\b",
690
+ }
691
+ return self._get_symbols_by_category('forex', category, fx_categories)
500
692
 
501
- def get_stocks_from(self, country_code: str = 'USA') -> List[str]:
693
+ def get_stocks_from_country(self, country_code: str = 'USA', etf=True) -> List[str]:
502
694
  """
503
695
  Retrieves a list of stock symbols from a specific country.
504
696
 
@@ -530,25 +722,133 @@ class Account(object):
530
722
 
531
723
  Notes:
532
724
  This mthods works primarly with Admirals Group AS products,
533
- For other brokers use `get_symbols()`
725
+ For other brokers use `get_symbols()` or this method will use it by default.
534
726
  """
535
- country_map = {
536
- "USA": r"\b(US)\b",
537
- "AUS": r"\b(Australia)\b",
538
- "BEL": r"\b(Belgium)\b",
539
- "DNK": r"\b(Denmark)\b",
540
- "FIN": r"\b(Finland)\b",
541
- "FRA": r"\b(France)\b",
542
- "DEU": r"\b(Germany)\b",
543
- "NLD": r"\b(Netherlands)\b",
544
- "NOR": r"\b(Norway)\b",
545
- "PRT": r"\b(Portugal)\b",
546
- "ESP": r"\b(Spain)\b",
547
- "SWE": r"\b(Sweden)\b",
548
- "GBR": r"\b(UK)\b",
549
- "CHE": r"\b(Switzerland)\b",
550
- }
551
- return self._get_symbols_by_category('stocks', country_code, country_map)
727
+
728
+ if self.broker != AdmiralMarktsGroup():
729
+ stocks = self.get_symbols(symbol_type='STK')
730
+ return stocks
731
+ else:
732
+ country_map = _COUNTRY_MAP_
733
+ stocks = self._get_symbols_by_category('STK', country_code, country_map)
734
+ if etf:
735
+ etfs = self._get_symbols_by_category('ETF', country_code, country_map)
736
+ return stocks + etfs
737
+ return stocks
738
+
739
+ def get_stocks_from_exchange(self, exchange_code: str = 'XNYS', etf=True) -> List[str]:
740
+ """
741
+ Get stock symbols from a specific exchange using the ISO Code for the exchange.
742
+
743
+ Supported exchanges are from Admirals Group AS products:
744
+ * **XASX:** **Australian Securities Exchange**
745
+ * **XBRU:** **Euronext Brussels Exchange**
746
+ * **XCSE:** **Copenhagen Stock Exchange**
747
+ * **XHEL:** **NASDAQ OMX Helsinki**
748
+ * **XPAR:** **Euronext Paris**
749
+ * **XETR:** **Xetra Frankfurt**
750
+ * **XOSL:** **Oslo Stock Exchange**
751
+ * **XLIS:** **Euronext Lisbon**
752
+ * **XMAD:** **Bolsa de Madrid**
753
+ * **XSTO:** **NASDAQ OMX Stockholm**
754
+ * **XLON:** **London Stock Exchange**
755
+ * **NYSE:** **New York Stock Exchange**
756
+ * **ARCA:** **NYSE ARCA**
757
+ * **AMEX:** **NYSE AMEX**
758
+ * **XNYS:** **New York Stock Exchange (AMEX, ARCA, NYSE)**
759
+ * **NASDAQ:** **NASDAQ**
760
+ * **BATS:** **BATS Exchange**
761
+ * **XSWX:** **SWX Swiss Exchange**
762
+ * **XAMS:** **Euronext Amsterdam**
763
+
764
+ Args:
765
+ exchange_code (str, optional): The ISO code of the exchange.
766
+ etf (bool, optional): If True, include ETFs from the exchange. Defaults to True.
767
+
768
+ Returns:
769
+ list: A list of stock symbol names from the specified exchange.
770
+
771
+ Raises:
772
+ ValueError: If an unsupported exchange is provided.
773
+
774
+ Notes:
775
+ This mthods works primarly with Admirals Group AS products,
776
+ For other brokers use `get_symbols()` or this method will use it by default.
777
+ """
778
+ if self.broker != AdmiralMarktsGroup():
779
+ stocks = self.get_symbols(symbol_type='STK')
780
+ return stocks
781
+ else:
782
+ exchange_map = AMG_EXCHANGES
783
+ stocks = self._get_symbols_by_category('STK', exchange_code, exchange_map)
784
+ if etf:
785
+ etfs = self._get_symbols_by_category('ETF', exchange_code, exchange_map)
786
+ return stocks + etfs
787
+ return stocks
788
+
789
+ def get_future_symbols(self, category: str = 'ALL') -> List[str]:
790
+ """
791
+ Retrieves a list of future symbols belonging to a specific category.
792
+
793
+ Args:
794
+ category : The category of future symbols to retrieve.
795
+ Possible values are 'ALL', 'agricultures', 'energies', 'metals'.
796
+ Defaults to 'ALL'.
797
+
798
+ Returns:
799
+ list: A list of future symbol names matching the specified category.
800
+
801
+ Raises:
802
+ ValueError: If an unsupported category is provided.
803
+
804
+ Notes:
805
+ This mthods works primarly with Admirals Group AS products,
806
+ For other brokers use `get_symbols()` or this method will use it by default.
807
+ """
808
+ category = category.lower()
809
+ if self.broker != AdmiralMarktsGroup():
810
+ return self.get_symbols(symbol_type='FUT')
811
+ elif category in ['all', 'index']:
812
+ categories = {
813
+ "all": r"\b(Futures?)\b",
814
+ "index": r"\b(Index)\b",
815
+ }
816
+ return self._get_symbols_by_category('FUT', category, categories)
817
+ else:
818
+ metals = []
819
+ energies = []
820
+ agricultures = []
821
+ bonds = []
822
+ commodities = self.get_symbols(symbol_type='COMD')
823
+ futures = self.get_symbols(symbol_type='FUT')
824
+ for symbol in futures:
825
+ info = self.get_symbol_info(symbol)
826
+ if info.name.startswith('_'):
827
+ if 'XAU' in info.name:
828
+ metals.append(info.name)
829
+ if 'oil' in info.name.lower():
830
+ energies.append(info.name)
831
+ name = info.name.split('_')[1]
832
+ if name in commodities:
833
+ _info = self.get_symbol_info(name)
834
+ if 'Metals' in _info.path:
835
+ metals.append(info.name)
836
+ elif 'Energies' in _info.path:
837
+ energies.append(info.name)
838
+ elif 'Agricultures' in _info.path:
839
+ agricultures.append(info.name)
840
+
841
+ elif info.name.startswith('#'):
842
+ if 'Index' not in info.path:
843
+ bonds.append(info.name)
844
+ if category == 'metals':
845
+ return metals
846
+ elif category == 'energies':
847
+ return energies
848
+ elif category == 'agricultures':
849
+ return agricultures
850
+ elif category == 'bonds':
851
+ return bonds
552
852
 
553
853
  def get_symbol_info(self, symbol: str) -> Union[SymbolInfo, None]:
554
854
  """Get symbol properties
@@ -654,12 +954,12 @@ class Account(object):
654
954
  Raises:
655
955
  MT5TerminalError: A specific exception based on the error code.
656
956
  """
657
- _action = {
957
+ actions = {
658
958
  'buy': mt5.ORDER_TYPE_BUY,
659
959
  'sell': mt5.ORDER_TYPE_SELL
660
960
  }
661
961
  try:
662
- margin = mt5.order_calc_margin(_action[action], symbol, lot, price)
962
+ margin = mt5.order_calc_margin(actions[action], symbol, lot, price)
663
963
  if margin is None:
664
964
  return None
665
965
  return margin
@@ -691,7 +991,7 @@ class Account(object):
691
991
  try:
692
992
  result = mt5.order_check(request)
693
993
  result_dict = result._asdict()
694
- trade_request = TradeRequest(**result.request._asdict())
994
+ trade_request = TradeRequest(**result.request._asdict())
695
995
  result_dict['request'] = trade_request
696
996
  return OrderCheckResult(**result_dict)
697
997
  except Exception as e:
@@ -717,7 +1017,7 @@ class Account(object):
717
1017
  try:
718
1018
  result = mt5.order_send(request)
719
1019
  result_dict = result._asdict()
720
- trade_request = TradeRequest(**result.request._asdict())
1020
+ trade_request = TradeRequest(**result.request._asdict())
721
1021
  result_dict['request'] = trade_request
722
1022
  return OrderSentResult(**result_dict)
723
1023
  except Exception as e:
@@ -898,6 +1198,8 @@ class Account(object):
898
1198
  df = pd.DataFrame(list(position_deals),
899
1199
  columns=position_deals[0]._asdict())
900
1200
  df['time'] = pd.to_datetime(df['time'], unit='s')
1201
+ df.drop(['time_msc', 'external_id'], axis=1, inplace=True)
1202
+ df.set_index('time', inplace=True)
901
1203
  if save:
902
1204
  file = "trade_history.csv"
903
1205
  df.to_csv(file)