bbstrader 0.1.9__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,45 @@ _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
+
57
128
  def check_mt5_connection():
58
129
  try:
59
130
  init = mt5.initialize()
@@ -62,6 +133,67 @@ def check_mt5_connection():
62
133
  except Exception:
63
134
  raise_mt5_error(INIT_MSG)
64
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
+
65
197
  class Account(object):
66
198
  """
67
199
  The `Account` class is utilized to retrieve information about
@@ -103,18 +235,68 @@ class Account(object):
103
235
  self._check_brokers()
104
236
 
105
237
  def _check_brokers(self):
106
- supported = __BROKERS__.copy()
107
- broker = self.get_terminal_info().company
108
- if broker not in supported.values():
238
+ supported = BROKERS.copy()
239
+ if self.broker not in supported.values():
109
240
  msg = (
110
- f"{broker} is not currently supported broker for the Account() class\n"
111
- f"Currently Supported brokers are: {', '.join(supported.values())}\n"
112
- f"For {supported['AMG']}, See [{amg_url}]\n"
113
- f"For {supported['JGM']}, See [{jgm_url}]\n"
114
- 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"
115
246
  )
116
247
  raise InvalidBroker(message=msg)
117
-
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
+
118
300
  def get_account_info(
119
301
  self,
120
302
  account: Optional[int] = None,
@@ -279,8 +461,8 @@ class Account(object):
279
461
  c = CurrencyConverter(filename)
280
462
  os.remove(filename)
281
463
  supported = c.currencies
282
- if (from_c not in supported or
283
- to_c not in supported
464
+ if (from_c not in supported
465
+ or to_c not in supported
284
466
  ):
285
467
  rate = qty
286
468
  else:
@@ -300,7 +482,7 @@ class Account(object):
300
482
 
301
483
  Exemple:
302
484
  >>> account = Account()
303
- >>> account.get_rates('EURUSD')
485
+ >>> account.get_currency_rates('EURUSD')
304
486
  {'bc': 'EUR', 'mc': 'EUR', 'pc': 'USD', 'ac': 'USD'}
305
487
  """
306
488
  info = self.get_symbol_info(symbol)
@@ -496,16 +678,19 @@ class Account(object):
496
678
 
497
679
  Notes:
498
680
  This mthods works primarly with Admirals Group AS products,
499
- For other brokers use `get_symbols()`
681
+ For other brokers use `get_symbols()` or this method will use it by default.
500
682
  """
501
- fx_categories = {
502
- "majors": r"\b(Majors?)\b",
503
- "minors": r"\b(Minors?)\b",
504
- "exotics": r"\b(Exotics?)\b",
505
- }
506
- 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)
507
692
 
508
- 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]:
509
694
  """
510
695
  Retrieves a list of stock symbols from a specific country.
511
696
 
@@ -537,25 +722,133 @@ class Account(object):
537
722
 
538
723
  Notes:
539
724
  This mthods works primarly with Admirals Group AS products,
540
- For other brokers use `get_symbols()`
725
+ For other brokers use `get_symbols()` or this method will use it by default.
541
726
  """
542
- country_map = {
543
- "USA": r"\b(US)\b",
544
- "AUS": r"\b(Australia)\b",
545
- "BEL": r"\b(Belgium)\b",
546
- "DNK": r"\b(Denmark)\b",
547
- "FIN": r"\b(Finland)\b",
548
- "FRA": r"\b(France)\b",
549
- "DEU": r"\b(Germany)\b",
550
- "NLD": r"\b(Netherlands)\b",
551
- "NOR": r"\b(Norway)\b",
552
- "PRT": r"\b(Portugal)\b",
553
- "ESP": r"\b(Spain)\b",
554
- "SWE": r"\b(Sweden)\b",
555
- "GBR": r"\b(UK)\b",
556
- "CHE": r"\b(Switzerland)\b",
557
- }
558
- 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
559
852
 
560
853
  def get_symbol_info(self, symbol: str) -> Union[SymbolInfo, None]:
561
854
  """Get symbol properties
@@ -661,12 +954,12 @@ class Account(object):
661
954
  Raises:
662
955
  MT5TerminalError: A specific exception based on the error code.
663
956
  """
664
- _action = {
957
+ actions = {
665
958
  'buy': mt5.ORDER_TYPE_BUY,
666
959
  'sell': mt5.ORDER_TYPE_SELL
667
960
  }
668
961
  try:
669
- margin = mt5.order_calc_margin(_action[action], symbol, lot, price)
962
+ margin = mt5.order_calc_margin(actions[action], symbol, lot, price)
670
963
  if margin is None:
671
964
  return None
672
965
  return margin
@@ -698,7 +991,7 @@ class Account(object):
698
991
  try:
699
992
  result = mt5.order_check(request)
700
993
  result_dict = result._asdict()
701
- trade_request = TradeRequest(**result.request._asdict())
994
+ trade_request = TradeRequest(**result.request._asdict())
702
995
  result_dict['request'] = trade_request
703
996
  return OrderCheckResult(**result_dict)
704
997
  except Exception as e:
@@ -724,7 +1017,7 @@ class Account(object):
724
1017
  try:
725
1018
  result = mt5.order_send(request)
726
1019
  result_dict = result._asdict()
727
- trade_request = TradeRequest(**result.request._asdict())
1020
+ trade_request = TradeRequest(**result.request._asdict())
728
1021
  result_dict['request'] = trade_request
729
1022
  return OrderSentResult(**result_dict)
730
1023
  except Exception as e:
@@ -905,6 +1198,8 @@ class Account(object):
905
1198
  df = pd.DataFrame(list(position_deals),
906
1199
  columns=position_deals[0]._asdict())
907
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)
908
1203
  if save:
909
1204
  file = "trade_history.csv"
910
1205
  df.to_csv(file)