bbstrader 0.3.2__py3-none-any.whl → 0.3.4__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.

@@ -1,21 +1,28 @@
1
1
  import os
2
2
  import re
3
3
  import urllib.request
4
- from datetime import datetime, timedelta
4
+ from datetime import datetime
5
5
  from typing import Any, Dict, List, Literal, Optional, Tuple, Union
6
6
 
7
+ import numpy as np
7
8
  import pandas as pd
8
9
  from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
10
+ from numpy.typing import NDArray
9
11
 
10
12
  from bbstrader.metatrader.utils import (
13
+ TIMEFRAMES,
11
14
  AccountInfo,
12
15
  BookInfo,
13
16
  InvalidBroker,
14
17
  OrderCheckResult,
15
18
  OrderSentResult,
19
+ RateDtype,
20
+ RateInfo,
16
21
  SymbolInfo,
17
22
  SymbolType,
18
23
  TerminalInfo,
24
+ TickDtype,
25
+ TickFlag,
19
26
  TickInfo,
20
27
  TradeDeal,
21
28
  TradeOrder,
@@ -33,6 +40,7 @@ except ImportError:
33
40
  __all__ = [
34
41
  "Account",
35
42
  "Broker",
43
+ "MetaQuotes",
36
44
  "AdmiralMarktsGroup",
37
45
  "JustGlobalMarkets",
38
46
  "PepperstoneGroupLimited",
@@ -41,6 +49,7 @@ __all__ = [
41
49
  ]
42
50
 
43
51
  __BROKERS__ = {
52
+ "MQL": "MetaQuotes Ltd.",
44
53
  "AMG": "Admirals Group AS",
45
54
  "JGM": "Just Global Markets Ltd.",
46
55
  "FTMO": "FTMO S.R.O.",
@@ -67,14 +76,13 @@ _ADMIRAL_MARKETS_PRODUCTS_ = [
67
76
  ]
68
77
  _JUST_MARKETS_PRODUCTS_ = ["Stocks", "Crypto", "indices", "Commodities", "Forex"]
69
78
 
70
- SUPPORTED_BROKERS = [__BROKERS__[b] for b in {"AMG", "JGM", "FTMO"}]
79
+ SUPPORTED_BROKERS = [__BROKERS__[b] for b in {"MQL", "AMG", "JGM", "FTMO"}]
71
80
  INIT_MSG = (
72
- f"\n* Ensure you have a good and stable internet connexion\n"
73
- f"* Ensure you have an activete MT5 terminal install on your machine\n"
74
- f"* Ensure you have an active MT5 Account with {' or '.join(SUPPORTED_BROKERS)}\n"
75
- f"* If you want to trade {', '.join(_ADMIRAL_MARKETS_PRODUCTS_)}, See [{_ADMIRAL_MARKETS_URL_}]\n"
76
- f"* If you want to trade {', '.join(_JUST_MARKETS_PRODUCTS_)}, See [{_JUST_MARKETS_URL_}]\n"
77
- f"* If you are looking for a prop firm, See [{_FTMO_URL_}]\n"
81
+ f"\n* Check your internet connection\n"
82
+ f"* Make sure MT5 is installed and active\n"
83
+ f"* Looking for a boker? See [{_ADMIRAL_MARKETS_URL_}] "
84
+ f"or [{_JUST_MARKETS_URL_}]\n"
85
+ f"* Looking for a prop firm? See [{_FTMO_URL_}]\n"
78
86
  )
79
87
 
80
88
  amg_url = _ADMIRAL_MARKETS_URL_
@@ -136,7 +144,16 @@ AMG_EXCHANGES = {
136
144
  }
137
145
 
138
146
 
139
- def check_mt5_connection(**kwargs) -> bool:
147
+ def check_mt5_connection(
148
+ *,
149
+ path=None,
150
+ login=None,
151
+ password=None,
152
+ server=None,
153
+ timeout=60_000,
154
+ portable=False,
155
+ **kwargs,
156
+ ) -> bool:
140
157
  """
141
158
  Initialize the connection to the MetaTrader 5 terminal.
142
159
 
@@ -156,12 +173,11 @@ def check_mt5_connection(**kwargs) -> bool:
156
173
  - Follow these instructions to lunch each terminal in portable mode first:
157
174
  https://www.metatrader5.com/en/terminal/help/start_advanced/start#configuration_file
158
175
  """
159
- path = kwargs.get("path", None)
160
- login = kwargs.get("login", None)
161
- password = kwargs.get("password", None)
162
- server = kwargs.get("server", None)
163
- timeout = kwargs.get("timeout", 60_000)
164
- portable = kwargs.get("portable", False)
176
+ if login is not None and server is not None:
177
+ account_info = mt5.account_info()
178
+ if account_info is not None:
179
+ if account_info.login == login and account_info.server == server:
180
+ return True
165
181
 
166
182
  init = False
167
183
  if path is None and (login or password or server):
@@ -221,6 +237,11 @@ class Broker(object):
221
237
  return f"{self.__class__.__name__}({self.name})"
222
238
 
223
239
 
240
+ class MetaQuotes(Broker):
241
+ def __init__(self, **kwargs):
242
+ super().__init__(__BROKERS__["MQL"], **kwargs)
243
+
244
+
224
245
  class AdmiralMarktsGroup(Broker):
225
246
  def __init__(self, **kwargs):
226
247
  super().__init__(__BROKERS__["AMG"], **kwargs)
@@ -262,6 +283,7 @@ class AMP(Broker): ...
262
283
 
263
284
  BROKERS: Dict[str, Broker] = {
264
285
  "FTMO": FTMO(),
286
+ "MQL": MetaQuotes(),
265
287
  "AMG": AdmiralMarktsGroup(),
266
288
  "JGM": JustGlobalMarkets(),
267
289
  "PGL": PepperstoneGroupLimited(),
@@ -503,7 +525,7 @@ class Account(object):
503
525
  """Helper function to print account info"""
504
526
  self._show_info(self.get_account_info, "account")
505
527
 
506
- def get_terminal_info(self, show=False) -> Union[TerminalInfo, None]:
528
+ def get_terminal_info(self, show=False) -> TerminalInfo | None:
507
529
  """
508
530
  Get the connected MetaTrader 5 client terminal status and settings.
509
531
 
@@ -951,7 +973,7 @@ class Account(object):
951
973
  futures_types["bonds"].append(info.name)
952
974
  return futures_types[category]
953
975
 
954
- def get_symbol_info(self, symbol: str) -> Union[SymbolInfo, None]:
976
+ def get_symbol_info(self, symbol: str) -> SymbolInfo | None:
955
977
  """Get symbol properties
956
978
 
957
979
  Args:
@@ -984,6 +1006,14 @@ class Account(object):
984
1006
  msg = self._symbol_info_msg(symbol)
985
1007
  raise_mt5_error(message=f"{str(e)} {msg}")
986
1008
 
1009
+ def _symbol_info_msg(self, symbol):
1010
+ return (
1011
+ f"No history found for {symbol} in Market Watch.\n"
1012
+ f"* Ensure {symbol} is selected and displayed in the Market Watch window.\n"
1013
+ f"* See https://www.metatrader5.com/en/terminal/help/trading/market_watch\n"
1014
+ f"* Ensure the symbol name is correct.\n"
1015
+ )
1016
+
987
1017
  def show_symbol_info(self, symbol: str):
988
1018
  """
989
1019
  Print symbol properties
@@ -993,15 +1023,7 @@ class Account(object):
993
1023
  """
994
1024
  self._show_info(self.get_symbol_info, "symbol", symbol=symbol)
995
1025
 
996
- def _symbol_info_msg(self, symbol):
997
- return (
998
- f"No history found for {symbol} in Market Watch.\n"
999
- f"* Ensure {symbol} is selected and displayed in the Market Watch window.\n"
1000
- f"* See https://www.metatrader5.com/en/terminal/help/trading/market_watch\n"
1001
- f"* Ensure the symbol name is correct.\n"
1002
- )
1003
-
1004
- def get_tick_info(self, symbol: str) -> Union[TickInfo, None]:
1026
+ def get_tick_info(self, symbol: str) -> TickInfo | None:
1005
1027
  """Get symbol tick properties
1006
1028
 
1007
1029
  Args:
@@ -1043,7 +1065,148 @@ class Account(object):
1043
1065
  """
1044
1066
  self._show_info(self.get_tick_info, "tick", symbol=symbol)
1045
1067
 
1046
- def get_market_book(self, symbol: str) -> Union[Tuple[BookInfo], None]:
1068
+ def get_rate_info(self, symbol: str, timeframe: str = "1m") -> RateInfo | None:
1069
+ """Get the most recent bar for a specified symbol and timeframe.
1070
+
1071
+ Args:
1072
+ symbol (str): The symbol for which to get the rate information.
1073
+ timeframe (str): The timeframe for the rate information. Default is '1m'.
1074
+ See ``bbstrader.metatrader.utils.TIMEFRAMES`` for supported timeframes.
1075
+ Returns:
1076
+ RateInfo: The most recent bar as a RateInfo named tuple.
1077
+ None: If no rates are found or an error occurs.
1078
+ Raises:
1079
+ MT5TerminalError: A specific exception based on the error code.
1080
+ """
1081
+ rates = mt5.copy_rates_from_pos(symbol, TIMEFRAMES[timeframe], 0, 1)
1082
+ if rates is None or len(rates) == 0:
1083
+ return None
1084
+ rate = rates[0]
1085
+ return RateInfo(*rate)
1086
+
1087
+ def get_rates_from_pos(
1088
+ self, symbol: str, timeframe: str, start_pos: int = 0, bars: int = 1
1089
+ ) -> NDArray[np.void]:
1090
+ """
1091
+ Get bars from the MetaTrader 5 terminal starting from the specified index.
1092
+
1093
+ Args:
1094
+ symbol: Financial instrument name, for example, "EURUSD"
1095
+ timeframe: Timeframe the bars are requested for.
1096
+ start_pos: Initial index of the bar the data are requested from.
1097
+ bars: Number of bars to receive.
1098
+
1099
+ Returns:
1100
+ bars as the numpy array with the named time, open, high, low, close, tick_volume, spread and real_volume columns.
1101
+ Returns an empty array in case of an error.
1102
+ """
1103
+ rates = mt5.copy_rates_from_pos(symbol, TIMEFRAMES[timeframe], start_pos, bars)
1104
+ if rates is None or len(rates) == 0:
1105
+ return np.array([], dtype=RateDtype)
1106
+ return rates
1107
+
1108
+ def get_rates_from_date(
1109
+ self,
1110
+ symbol: str,
1111
+ timeframe: str,
1112
+ date_from: datetime | pd.Timestamp,
1113
+ bars: int = 1,
1114
+ ) -> NDArray[np.void]:
1115
+ """
1116
+ Get bars from the MetaTrader 5 terminal starting from the specified date.
1117
+
1118
+ Args:
1119
+ symbol: Financial instrument name, for example, "EURUSD"
1120
+ timeframe: Timeframe the bars are requested for.
1121
+ date_from: Date of opening of the first bar from the requested sample.
1122
+ bars: Number of bars to receive.
1123
+
1124
+ Returns:
1125
+ bars as the numpy array with the named time, open, high, low, close, tick_volume, spread and real_volume columns.
1126
+ Returns an empty array in case of an error.
1127
+
1128
+ """
1129
+ rates = mt5.copy_rates_from(symbol, TIMEFRAMES[timeframe], date_from, bars)
1130
+ if rates is None or len(rates) == 0:
1131
+ return np.array([], dtype=RateDtype)
1132
+ return rates
1133
+
1134
+ def get_rates_range(
1135
+ self,
1136
+ symbol: str,
1137
+ timeframe: str,
1138
+ date_from: datetime | pd.Timestamp,
1139
+ date_to: datetime | pd.Timestamp = datetime.now(),
1140
+ ) -> NDArray[np.void]:
1141
+ """
1142
+ Get bars in the specified date range from the MetaTrader 5 terminal.
1143
+
1144
+ Args:
1145
+ symbol: Financial instrument name, for example, "EURUSD"
1146
+ timeframe: Timeframe the bars are requested for.
1147
+ date_from: Date the bars are requested from.
1148
+ date_to: Date, up to which the bars are requested.
1149
+
1150
+ Returns:
1151
+ bars as the numpy array with the named time, open, high, low, close, tick_volume, spread and real_volume columns.
1152
+ Returns an empty array in case of an error.
1153
+ """
1154
+ rates = mt5.copy_rates_range(symbol, TIMEFRAMES[timeframe], date_from, date_to)
1155
+ if rates is None or len(rates) == 0:
1156
+ return np.array([], dtype=RateDtype)
1157
+ return rates
1158
+
1159
+ def get_tick_from_date(
1160
+ self,
1161
+ symbol: str,
1162
+ date_from: datetime | pd.Timestamp,
1163
+ ticks: int = 1,
1164
+ flag: Literal["all", "info", "trade"] = "all",
1165
+ ) -> NDArray[np.void]:
1166
+ """
1167
+ Get ticks from the MetaTrader 5 terminal starting from the specified date.
1168
+
1169
+ Args:
1170
+ symbol: Financial instrument name, for example, "EURUSD"
1171
+ date_from: Date the ticks are requested from.
1172
+ ticks: Number of bars to receive.
1173
+ flag: A flag to define the type of the requested ticks ("all", "info", "trade").
1174
+
1175
+ Returns:
1176
+ Returns ticks as the numpy array with the named time, bid, ask, last and flags columns.
1177
+ Return an empty array in case of an error.
1178
+ """
1179
+ ticks_data = mt5.copy_ticks_from(symbol, date_from, ticks, TickFlag[flag])
1180
+ if ticks_data is None or len(ticks_data) == 0:
1181
+ return np.array([], dtype=TickDtype)
1182
+ return ticks_data
1183
+
1184
+ def get_tick_range(
1185
+ self,
1186
+ symbol: str,
1187
+ date_from: datetime | pd.Timestamp,
1188
+ date_to: datetime | pd.Timestamp = datetime.now(),
1189
+ flag: Literal["all", "info", "trade"] = "all",
1190
+ ) -> NDArray[np.void]:
1191
+ """
1192
+ Get ticks for the specified date range from the MetaTrader 5 terminal.
1193
+
1194
+ Args:
1195
+ symbol: Financial instrument name, for example, "EURUSD"
1196
+ date_from: Date the ticks are requested from.
1197
+ date_to: Date, up to which the ticks are requested.
1198
+ flag: A flag to define the type of the requested ticks ("all", "info", "trade").
1199
+
1200
+ Returns:
1201
+ Returns ticks as the numpy array with the named time, bid, ask, last and flags columns.
1202
+ Return an empty array in case of an error.
1203
+ """
1204
+ ticks_data = mt5.copy_ticks_range(symbol, date_from, date_to, TickFlag[flag])
1205
+ if ticks_data is None or len(ticks_data) == 0:
1206
+ return np.array([], dtype=TickDtype)
1207
+ return ticks_data
1208
+
1209
+ def get_market_book(self, symbol: str) -> Tuple[BookInfo]:
1047
1210
  """
1048
1211
  Get the Market Depth content for a specific symbol.
1049
1212
  Args:
@@ -1185,7 +1348,7 @@ class Account(object):
1185
1348
  group: Optional[str] = None,
1186
1349
  ticket: Optional[int] = None,
1187
1350
  to_df: bool = False,
1188
- ) -> Union[pd.DataFrame, Tuple[TradePosition], None]:
1351
+ ) -> Union[pd.DataFrame, Tuple[TradePosition]]:
1189
1352
  """
1190
1353
  Get open positions with the ability to filter by symbol or ticket.
1191
1354
  There are four call options:
@@ -1215,7 +1378,6 @@ class Account(object):
1215
1378
  Returns:
1216
1379
  Union[pd.DataFrame, Tuple[TradePosition], None]:
1217
1380
  - `TradePosition` in the form of a named tuple structure (namedtuple) or pd.DataFrame.
1218
- - `None` in case of an error.
1219
1381
 
1220
1382
  Notes:
1221
1383
  The method allows receiving all open positions within a specified period.
@@ -1271,7 +1433,7 @@ class Account(object):
1271
1433
  position: Optional[int] = None, # TradePosition.ticket
1272
1434
  to_df: bool = True,
1273
1435
  save: bool = False,
1274
- ) -> Union[pd.DataFrame, Tuple[TradeDeal], None]:
1436
+ ) -> Union[pd.DataFrame, Tuple[TradeDeal]]:
1275
1437
  """
1276
1438
  Get deals from trading history within the specified interval
1277
1439
  with the ability to filter by `ticket` or `position`.
@@ -1309,7 +1471,6 @@ class Account(object):
1309
1471
  Returns:
1310
1472
  Union[pd.DataFrame, Tuple[TradeDeal], None]:
1311
1473
  - `TradeDeal` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
1312
- - `None` in case of an error.
1313
1474
 
1314
1475
  Notes:
1315
1476
  The method allows receiving all history orders within a specified period.
@@ -1372,7 +1533,7 @@ class Account(object):
1372
1533
  group: Optional[str] = None,
1373
1534
  ticket: Optional[int] = None,
1374
1535
  to_df: bool = False,
1375
- ) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
1536
+ ) -> Union[pd.DataFrame, Tuple[TradeOrder]]:
1376
1537
  """
1377
1538
  Get active orders with the ability to filter by symbol or ticket.
1378
1539
  There are four call options:
@@ -1399,7 +1560,6 @@ class Account(object):
1399
1560
  Returns:
1400
1561
  Union[pd.DataFrame, Tuple[TradeOrder], None]:
1401
1562
  - `TradeOrder` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
1402
- - `None` in case of an error.
1403
1563
 
1404
1564
  Notes:
1405
1565
  The method allows receiving all history orders within a specified period.
@@ -1464,7 +1624,7 @@ class Account(object):
1464
1624
  position: Optional[int] = None, # position ticket
1465
1625
  to_df: bool = True,
1466
1626
  save: bool = False,
1467
- ) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
1627
+ ) -> Union[pd.DataFrame, Tuple[TradeOrder]]:
1468
1628
  """
1469
1629
  Get orders from trading history within the specified interval
1470
1630
  with the ability to filter by `ticket` or `position`.
@@ -1502,7 +1662,6 @@ class Account(object):
1502
1662
  Returns:
1503
1663
  Union[pd.DataFrame, Tuple[TradeOrder], None]
1504
1664
  - `TradeOrder` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
1505
- - `None` in case of an error.
1506
1665
 
1507
1666
  Notes:
1508
1667
  The method allows receiving all history orders within a specified period.
@@ -1583,16 +1742,11 @@ class Account(object):
1583
1742
  Returns:
1584
1743
  List[TradeDeal]: List of today deals
1585
1744
  """
1586
- date_from = datetime.now() - timedelta(days=2)
1587
- history = (
1588
- self.get_trades_history(date_from=date_from, group=group, to_df=False) or []
1589
- )
1745
+ history = self.get_trades_history(group=group, to_df=False) or []
1590
1746
  positions_ids = set([deal.position_id for deal in history if deal.magic == id])
1591
1747
  today_deals = []
1592
1748
  for position in positions_ids:
1593
- deal = self.get_trades_history(
1594
- date_from=date_from, position=position, to_df=False
1595
- )
1749
+ deal = self.get_trades_history(position=position, to_df=False) or []
1596
1750
  if deal is not None and len(deal) == 2:
1597
1751
  deal_time = datetime.fromtimestamp(deal[1].time)
1598
1752
  if deal_time.date() == datetime.now().date():
@@ -4,17 +4,15 @@ import numpy as np
4
4
  import pandas as pd
5
5
  import seaborn as sns
6
6
 
7
- from bbstrader.metatrader.account import check_mt5_connection
7
+ from bbstrader.metatrader.account import check_mt5_connection, shutdown_mt5
8
8
  from bbstrader.metatrader.utils import TIMEFRAMES
9
9
 
10
10
  sns.set_theme()
11
11
 
12
12
 
13
- def _get_data(path, symbol, timeframe, bars):
14
- check_mt5_connection(path=path)
13
+ def _get_data(symbol, timeframe, bars):
15
14
  rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, bars)
16
15
  df = pd.DataFrame(rates)
17
- df["time"] = pd.to_datetime(df["time"], unit="s")
18
16
  return df
19
17
 
20
18
 
@@ -76,10 +74,14 @@ def display_volume_profile(
76
74
  Returns:
77
75
  None: Displays a matplotlib chart of the volume profile.
78
76
  """
79
- df = _get_data(path, symbol, TIMEFRAMES[timeframe], bars)
77
+ check_mt5_connection(path=path)
78
+ df = _get_data(symbol, TIMEFRAMES[timeframe], bars)
79
+ if df.empty:
80
+ raise ValueError(f"No data found for {symbol} in {path}")
80
81
  hist, bin_edges, bin_centers = volume_profile(df, bins)
81
82
  poc, vah, val = value_area(hist, bin_centers, va_percentage)
82
83
  current_price = mt5.symbol_info_tick(symbol).bid
84
+ shutdown_mt5()
83
85
 
84
86
  plt.figure(figsize=(6, 10))
85
87
  plt.barh(bin_centers, hist, height=bin_centers[1] - bin_centers[0], color="skyblue")