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

@@ -3,13 +3,66 @@ import MetaTrader5 as Mt5
3
3
  from datetime import datetime
4
4
  from typing import Union, Optional
5
5
  from bbstrader.metatrader.utils import (
6
- raise_mt5_error, TimeFrame, TIMEFRAMES)
6
+ raise_mt5_error,
7
+ TimeFrame,
8
+ TIMEFRAMES
9
+ )
10
+ from bbstrader.metatrader.account import Account
11
+ from bbstrader.metatrader.account import AMG_EXCHANGES
7
12
  from bbstrader.metatrader.account import check_mt5_connection
8
13
  from pandas.tseries.offsets import CustomBusinessDay
9
14
  from pandas.tseries.holiday import USFederalHolidayCalendar
15
+ from exchange_calendars import(
16
+ get_calendar,
17
+ get_calendar_names
18
+ )
19
+
20
+ __all__ = [
21
+ 'Rates',
22
+ 'download_historical_data',
23
+ 'get_data_from_pos'
24
+ ]
10
25
 
11
26
  MAX_BARS = 10_000_000
12
27
 
28
+ IDX_CALENDARS = {
29
+ "CAD": "XTSE",
30
+ "AUD": "XASX",
31
+ "GBP": "XLON",
32
+ "HKD": "XSHG",
33
+ "ZAR": "XJSE",
34
+ "CHF": "XSWX",
35
+ "NOK": "XOSL",
36
+ "EUR": "XETR",
37
+ "SGD": "XSES",
38
+ "USD": "us_futures",
39
+ "JPY": "us_futures",
40
+ }
41
+
42
+ COMD_CALENDARS = {
43
+ "Energies" : "us_futures",
44
+ "Metals" : "us_futures",
45
+ "Agricultures" : "CBOT",
46
+ "Bonds": {"USD" : "CBOT", "EUR": "EUREX"},
47
+ }
48
+
49
+ CALENDARS = {
50
+ "FX" : "us_futures",
51
+ "STK" : AMG_EXCHANGES,
52
+ "ETF" : AMG_EXCHANGES,
53
+ "IDX" : IDX_CALENDARS,
54
+ "COMD" : COMD_CALENDARS,
55
+ "CRYPTO": "24/7",
56
+ "FUT" : None,
57
+ }
58
+
59
+ SESSION_TIMEFRAMES = [
60
+ Mt5.TIMEFRAME_D1,
61
+ Mt5.TIMEFRAME_W1,
62
+ Mt5.TIMEFRAME_H12,
63
+ Mt5.TIMEFRAME_MN1
64
+ ]
65
+
13
66
 
14
67
  class Rates(object):
15
68
  """
@@ -26,8 +79,8 @@ class Rates(object):
26
79
  or just set it to Unlimited.
27
80
  In your MT5 terminal, go to `Tools` -> `Options` -> `Charts` -> `Max bars in chart`.
28
81
 
29
- 2. The `get_open, get_high, get_low, get_close, get_adj_close, get_returns,
30
- get_volume` properties returns data in Broker's timezone.
82
+ 2. The `open, high, low, close, adjclose, returns,
83
+ volume` properties returns data in Broker's timezone by default.
31
84
 
32
85
  Example:
33
86
  >>> rates = Rates("EURUSD", "1h")
@@ -70,9 +123,9 @@ class Rates(object):
70
123
  self.start_pos = self._get_start_pos(start_pos, time_frame)
71
124
  self.count = count
72
125
  self._mt5_initialized()
126
+ self.__account = Account()
73
127
  self.__data = self.get_rates_from_pos()
74
128
 
75
-
76
129
  def _mt5_initialized(self):
77
130
  check_mt5_connection()
78
131
 
@@ -125,8 +178,10 @@ class Rates(object):
125
178
  return TIMEFRAMES[time_frame]
126
179
 
127
180
  def _fetch_data(
128
- self, start: Union[int, datetime],
129
- count: Union[int, datetime]
181
+ self,
182
+ start: Union[int, datetime, pd.Timestamp],
183
+ count: Union[int, datetime, pd.Timestamp],
184
+ lower_colnames=False, utc=False,
130
185
  ) -> Union[pd.DataFrame, None]:
131
186
  """Fetches data from MT5 and returns a DataFrame or None."""
132
187
  try:
@@ -134,7 +189,17 @@ class Rates(object):
134
189
  rates = Mt5.copy_rates_from_pos(
135
190
  self.symbol, self.time_frame, start, count
136
191
  )
137
- elif isinstance(start, datetime) and isinstance(count, datetime):
192
+ elif (
193
+ isinstance(start, (datetime, pd.Timestamp)) and
194
+ isinstance(count, int)
195
+ ):
196
+ rates = Mt5.copy_rates_from(
197
+ self.symbol, self.time_frame, start, count
198
+ )
199
+ elif (
200
+ isinstance(start, (datetime, pd.Timestamp)) and
201
+ isinstance(count, (datetime, pd.Timestamp))
202
+ ):
138
203
  rates = Mt5.copy_rates_range(
139
204
  self.symbol, self.time_frame, start, count
140
205
  )
@@ -142,28 +207,103 @@ class Rates(object):
142
207
  return None
143
208
 
144
209
  df = pd.DataFrame(rates)
145
- return self._format_dataframe(df)
210
+ return self._format_dataframe(df, lower_colnames=lower_colnames, utc=utc)
146
211
  except Exception as e:
147
212
  raise_mt5_error(e)
148
213
 
149
- def _format_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
214
+ def _format_dataframe(self, df: pd.DataFrame,
215
+ lower_colnames=False, utc=False) -> pd.DataFrame:
150
216
  """Formats the raw MT5 data into a standardized DataFrame."""
151
217
  df = df.copy()
152
218
  df = df[['time', 'open', 'high', 'low', 'close', 'tick_volume']]
153
219
  df.columns = ['Date', 'Open', 'High', 'Low', 'Close', 'Volume']
154
220
  df['Adj Close'] = df['Close']
155
221
  df = df[['Date', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume']]
156
- df['Date'] = pd.to_datetime(df['Date'], unit='s')
222
+ #df = df.columns.rename(str.lower).str.replace(' ', '_')
223
+ df['Date'] = pd.to_datetime(df['Date'], unit='s', utc=utc)
157
224
  df.set_index('Date', inplace=True)
225
+ if lower_colnames:
226
+ df.columns = df.columns.str.lower().str.replace(' ', '_')
227
+ df.index.name = df.index.name.lower().replace(' ', '_')
158
228
  return df
159
229
 
160
- def get_rates_from_pos(self) -> Union[pd.DataFrame, None]:
230
+ def _filter_data(self, df: pd.DataFrame, date_from=None, date_to=None, fill_na=False) -> pd.DataFrame:
231
+ df = df.copy()
232
+ symbol_type = self.__account.get_symbol_type(self.symbol)
233
+ currencies = self.__account.get_currency_rates(self.symbol)
234
+ s_info = self.__account.get_symbol_info(self.symbol)
235
+ if symbol_type in CALENDARS:
236
+ if symbol_type == 'STK' or symbol_type == 'ETF':
237
+ for exchange in CALENDARS[symbol_type]:
238
+ if exchange in get_calendar_names():
239
+ symbols = self.__account.get_stocks_from_exchange(
240
+ exchange_code=exchange)
241
+ if self.symbol in symbols:
242
+ calendar = get_calendar(exchange, side='right')
243
+ break
244
+ elif symbol_type == 'IDX':
245
+ calendar = get_calendar(CALENDARS[symbol_type][currencies['mc']], side='right')
246
+ elif symbol_type == 'COMD':
247
+ for commodity in CALENDARS[symbol_type]:
248
+ if commodity in s_info.path:
249
+ calendar = get_calendar(CALENDARS[symbol_type][commodity], side='right')
250
+ elif symbol_type == 'FUT':
251
+ if 'Index' in s_info.path:
252
+ calendar = get_calendar(CALENDARS['IDX'][currencies['mc']], side='right')
253
+ else:
254
+ for commodity, cal in COMD_CALENDARS.items():
255
+ if self.symbol in self.__account.get_future_symbols(category=commodity):
256
+ if commodity == 'Bonds':
257
+ calendar = get_calendar(cal[currencies['mc']], side='right')
258
+ else:
259
+ calendar = get_calendar(cal, side='right')
260
+ else:
261
+ calendar = get_calendar(CALENDARS[symbol_type], side='right')
262
+ date_from = date_from or df.index[0]
263
+ date_to = date_to or df.index[-1]
264
+ if self.time_frame in SESSION_TIMEFRAMES:
265
+ valid_sessions = calendar.sessions_in_range(date_from, date_to)
266
+ else:
267
+ valid_sessions = calendar.minutes_in_range(date_from, date_to)
268
+ if self.time_frame in [Mt5.TIMEFRAME_M1, Mt5.TIMEFRAME_D1]:
269
+ # save the index name of the dataframe
270
+ index_name = df.index.name
271
+ if fill_na:
272
+ if isinstance(fill_na, bool):
273
+ method = 'nearest'
274
+ if isinstance(fill_na, str):
275
+ method = fill_na
276
+ df = df.reindex(valid_sessions, method=method)
277
+ else:
278
+ df.reindex(valid_sessions, method=None)
279
+ df.index = df.index.rename(index_name)
280
+ else:
281
+ df = df[df.index.isin(valid_sessions)]
282
+ return df
283
+
284
+ def _check_filter(self, filter, utc):
285
+ if filter and self.time_frame not in SESSION_TIMEFRAMES and not utc:
286
+ utc = True
287
+ elif filter and self.time_frame in SESSION_TIMEFRAMES and utc:
288
+ utc = False
289
+ return utc
290
+
291
+ def get_rates_from_pos(self, filter=False, fill_na=False,
292
+ lower_colnames=False, utc=False
293
+ ) -> Union[pd.DataFrame, None]:
161
294
  """
162
295
  Retrieves historical data starting from a specific position.
163
296
 
164
297
  Uses the `start_pos` and `count` attributes specified during
165
298
  initialization to fetch data.
166
299
 
300
+ Args:
301
+ filter : See `Rates.get_historical_data` for more details.
302
+ fill_na : See `Rates.get_historical_data` for more details.
303
+ lower_colnames : If True, the column names will be converted to lowercase.
304
+ utc (bool, optional): If True, the data will be in UTC timezone.
305
+ Defaults to False.
306
+
167
307
  Returns:
168
308
  Union[pd.DataFrame, None]: A DataFrame containing historical
169
309
  data if successful, otherwise None.
@@ -180,31 +320,66 @@ class Rates(object):
180
320
  "Both 'start_pos' and 'count' must be provided "
181
321
  "when calling 'get_rates_from_pos'."
182
322
  )
183
- df = self._fetch_data(self.start_pos, self.count)
323
+ utc = self._check_filter(filter, utc)
324
+ df = self._fetch_data(self.start_pos, self.count,
325
+ lower_colnames=lower_colnames, utc=utc)
326
+ if df is None:
327
+ return None
328
+ if filter:
329
+ return self._filter_data(df, fill_na=fill_na)
184
330
  return df
185
331
 
332
+ def get_rates_from(self, date_from: datetime | pd.Timestamp, count: int=MAX_BARS,
333
+ filter=False, fill_na=False, lower_colnames=False, utc=False) -> Union[pd.DataFrame, None]:
334
+ """
335
+ Retrieves historical data within a specified date range.
336
+
337
+ Args:
338
+ date_from : Starting date for data retrieval.
339
+ The data will be retrieved from this date going to the past.
340
+
341
+ count : Number of bars to retrieve.
342
+
343
+ filter : See `Rates.get_historical_data` for more details.
344
+ fill_na : See `Rates.get_historical_data` for more details.
345
+ lower_colnames : If True, the column names will be converted to lowercase.
346
+ utc (bool, optional): If True, the data will be in UTC timezone.
347
+ Defaults to False.
348
+
349
+ Returns:
350
+ Union[pd.DataFrame, None]: A DataFrame containing historical
351
+ data if successful, otherwise None.
352
+ """
353
+ utc = self._check_filter(filter, utc)
354
+ df = self._fetch_data(date_from, count, lower_colnames=lower_colnames, utc=utc)
355
+ if df is None:
356
+ return None
357
+ if filter:
358
+ return self._filter_data(df, fill_na=fill_na)
359
+ return df
360
+
186
361
  @property
187
- def get_open(self):
362
+ def open(self):
188
363
  return self.__data['Open']
189
364
 
190
365
  @property
191
- def get_high(self):
366
+ def high(self):
192
367
  return self.__data['High']
193
368
 
194
369
  @property
195
- def get_low(self):
370
+ def low(self):
196
371
  return self.__data['Low']
197
372
 
198
373
  @property
199
- def get_close(self):
374
+ def close(self):
200
375
  return self.__data['Close']
201
376
 
202
377
  @property
203
- def get_adj_close(self):
378
+ def adjclose(self):
204
379
  return self.__data['Adj Close']
205
380
 
206
381
  @property
207
- def get_returns(self):
382
+ def returns(self):
208
383
  """
209
384
  Fractional change between the current and a prior element.
210
385
 
@@ -222,23 +397,52 @@ class Rates(object):
222
397
  return data['Returns']
223
398
 
224
399
  @property
225
- def get_volume(self):
400
+ def volume(self):
226
401
  return self.__data['Volume']
227
402
 
228
403
  def get_historical_data(
229
404
  self,
230
- date_from: datetime,
231
- date_to: datetime = datetime.now(),
405
+ date_from: datetime | pd.Timestamp,
406
+ date_to: datetime | pd.Timestamp = pd.Timestamp.now(),
407
+ utc: bool = False,
408
+ filter: Optional[bool] = False,
409
+ fill_na: Optional[bool | str] = False,
410
+ lower_colnames: Optional[bool] = True,
232
411
  save_csv: Optional[bool] = False,
233
412
  ) -> Union[pd.DataFrame, None]:
234
413
  """
235
414
  Retrieves historical data within a specified date range.
236
415
 
237
416
  Args:
238
- date_from (datetime): Starting date for data retrieval.
239
- date_to (datetime, optional): Ending date for data retrieval.
417
+ date_from : Starting date for data retrieval.
418
+
419
+ date_to : Ending date for data retrieval.
240
420
  Defaults to the current time.
241
- save_csv (str, optional): File path to save the data as a CSV.
421
+
422
+ utc : If True, the data will be in UTC timezone.
423
+ Defaults to False.
424
+
425
+ filter : If True, the data will be filtered based
426
+ on the trading sessions for the symbol.
427
+ This is use when we want to use the data for backtesting using Zipline.
428
+
429
+ fill_na : If True, the data will be filled with the nearest value.
430
+ This is use only when `filter` is True and time frame is "1m" or "D1",
431
+ this is because we use ``calendar.minutes_in_range`` or ``calendar.sessions_in_range``
432
+ where calendar is the ``ExchangeCalendar`` from `exchange_calendars` package.
433
+ So, for "1m" or "D1" time frame, the data will be filled with the nearest value
434
+ because the data from MT5 will have approximately the same number of rows as the
435
+ number of trading days or minute in the exchange calendar, so we can fill the missing
436
+ data with the nearest value.
437
+
438
+ But for other time frames, the data will be reindexed with the exchange calendar
439
+ because the data from MT5 will have more rows than the number of trading days or minute
440
+ in the exchange calendar. So we only take the data that is in the range of the exchange
441
+ calendar sessions or minutes.
442
+
443
+ lower_colnames : If True, the column names will be converted to lowercase.
444
+
445
+ save_csv : File path to save the data as a CSV.
242
446
  If None, the data won't be saved.
243
447
 
244
448
  Returns:
@@ -249,9 +453,58 @@ class Rates(object):
249
453
  ValueError: If the starting date is greater than the ending date.
250
454
 
251
455
  Notes:
252
- The Datetime for this method is in Local timezone.
456
+ The `filter` for this method can be use only for Admira Markets Group (AMG) symbols.
457
+ The Datetime for this method is in Local timezone by default.
458
+ All STK symbols are filtered based on the the exchange calendar.
459
+ All FX symbols are filtered based on the ``us_futures`` calendar.
460
+ All IDX symbols are filtered based on the exchange calendar of margin currency.
461
+ All COMD symbols are filtered based on the exchange calendar of the commodity.
253
462
  """
254
- df = self._fetch_data(date_from, date_to)
255
- if save_csv and df is not None:
463
+ utc = self._check_filter(filter, utc)
464
+ df = self._fetch_data(date_from, date_to,
465
+ lower_colnames=lower_colnames, utc=utc)
466
+ if df is None:
467
+ return None
468
+ if filter:
469
+ df = self._filter_data(df, date_from=date_from, date_to=date_to, fill_na=fill_na)
470
+ if save_csv:
256
471
  df.to_csv(f"{self.symbol}.csv")
257
472
  return df
473
+
474
+ def download_historical_data(symbol, time_frame, date_from,
475
+ date_to=pd.Timestamp.now(),lower_colnames=True,
476
+ utc=False, filter=False, fill_na=False, save_csv=False):
477
+ """Download historical data from MetaTrader 5 terminal.
478
+ See `Rates.get_historical_data` for more details.
479
+ """
480
+ rates = Rates(symbol, time_frame)
481
+ data = rates.get_historical_data(
482
+ date_from=date_from,
483
+ date_to=date_to,
484
+ save_csv=save_csv,
485
+ utc=utc,
486
+ filter=filter,
487
+ lower_colnames=lower_colnames
488
+ )
489
+ return data
490
+
491
+ def get_data_from_pos(symbol, time_frame, start_pos=0, fill_na=False,
492
+ count=MAX_BARS, lower_colnames=False, utc=False, filter=False,
493
+ session_duration=23.0):
494
+ """Get historical data from a specific position.
495
+ See `Rates.get_rates_from_pos` for more details.
496
+ """
497
+ rates = Rates(symbol, time_frame, start_pos, count, session_duration)
498
+ data = rates.get_rates_from_pos(filter=filter, fill_na=fill_na,
499
+ lower_colnames=lower_colnames, utc=utc)
500
+ return data
501
+
502
+ def get_data_from_date(symbol, time_frame, date_from, count=MAX_BARS, fill_na=False,
503
+ lower_colnames=False, utc=False, filter=False):
504
+ """Get historical data from a specific date.
505
+ See `Rates.get_rates_from` for more details.
506
+ """
507
+ rates = Rates(symbol, time_frame)
508
+ data = rates.get_rates_from(date_from, count, filter=filter, fill_na=fill_na,
509
+ lower_colnames=lower_colnames, utc=utc)
510
+ return data
@@ -7,8 +7,18 @@ import MetaTrader5 as Mt5
7
7
  from bbstrader.metatrader.account import Account
8
8
  from bbstrader.metatrader.rates import Rates
9
9
  from bbstrader.metatrader.utils import (
10
- TIMEFRAMES, raise_mt5_error, TimeFrame)
11
- from typing import List, Dict, Optional, Literal, Union, Any
10
+ TIMEFRAMES,
11
+ raise_mt5_error,
12
+ TimeFrame
13
+ )
14
+ from typing import (
15
+ List,
16
+ Dict,
17
+ Optional,
18
+ Literal,
19
+ Union,
20
+ Any
21
+ )
12
22
 
13
23
 
14
24
  _COMMD_SUPPORTED_ = [
@@ -16,7 +26,6 @@ _COMMD_SUPPORTED_ = [
16
26
  'XAGEUR', 'XAGUSD', 'XAUAUD', 'XAUEUR', 'XAUUSD', 'XAUGBP', 'USOIL'
17
27
  ]
18
28
 
19
-
20
29
  _ADMIRAL_MARKETS_FUTURES_ = [
21
30
  '#USTNote_', '#Bund_', '#USDX_', '_AUS200_', '_Canada60_', '_SouthAfrica40_',
22
31
  '_STXE600_', '_EURO50_', '_GER40_', '_GermanyTech30_', '_MidCapGER50_',
@@ -25,6 +34,7 @@ _ADMIRAL_MARKETS_FUTURES_ = [
25
34
  '_XAU_', '_HK50_', '_HSCEI50_'
26
35
  ]
27
36
 
37
+ __all__ = ['RiskManagement']
28
38
 
29
39
  class RiskManagement(Account):
30
40
  """
@@ -135,7 +145,7 @@ class RiskManagement(Account):
135
145
  self.pchange = pchange_sl
136
146
  self.var_level = var_level
137
147
  self.var_tf = var_time_frame
138
- self.daily_dd = daily_risk
148
+ self.daily_dd = round(daily_risk, 5)
139
149
  self.max_risk = max_risk
140
150
  self.rr = rr
141
151
  self.sl = sl
@@ -193,7 +203,7 @@ class RiskManagement(Account):
193
203
  volume_step = s_info.volume_step
194
204
  lot = self.currency_risk()['lot']
195
205
  steps = self._volume_step(volume_step)
196
- if steps >= 2:
206
+ if float(steps) >= float(1):
197
207
  return round(lot, steps)
198
208
  else:
199
209
  return round(lot)
@@ -203,13 +213,13 @@ class RiskManagement(Account):
203
213
 
204
214
  value_str = str(value)
205
215
 
206
- if '.' in value_str:
216
+ if '.' in value_str and value_str != '1.0':
207
217
  decimal_index = value_str.index('.')
208
218
  num_digits = len(value_str) - decimal_index - 1
209
-
210
219
  return num_digits
211
- elif value_str == '1':
212
- return 1
220
+
221
+ elif value_str == '1.0':
222
+ return 0
213
223
  else:
214
224
  return 0
215
225
 
@@ -254,7 +264,7 @@ class RiskManagement(Account):
254
264
  interval = round((minutes / tf_int) * 252)
255
265
 
256
266
  rate = Rates(self.symbol, self._tf, 0, interval)
257
- returns = rate.get_returns*100
267
+ returns = rate.returns*100
258
268
  std = returns.std()
259
269
  point = self.get_symbol_info(self.symbol).point
260
270
  av_price = (self.symbol_info.bid + self.symbol_info.ask)/2
@@ -308,7 +318,7 @@ class RiskManagement(Account):
308
318
  interval = round((minutes / tf_int) * 252)
309
319
 
310
320
  rate = Rates(self.symbol, tf, 0, interval)
311
- returns = rate.get_returns*100
321
+ returns = rate.returns*100
312
322
  p = self.get_account_info().margin_free
313
323
  mu = returns.mean()
314
324
  sigma = returns.std()
@@ -405,10 +415,11 @@ class RiskManagement(Account):
405
415
 
406
416
  av_price = (s_info.bid + s_info.ask)/2
407
417
  trade_risk = self.get_trade_risk()
408
- FX = self.get_symbol_type(self.symbol) == 'FX'
409
- COMD = self.get_symbol_type(self.symbol) == 'COMD'
410
- FUT = self.get_symbol_type(self.symbol) == 'FUT'
411
- CRYPTO = self.get_symbol_type(self.symbol) == 'CRYPTO'
418
+ symbol_type = self.get_symbol_type(self.symbol)
419
+ FX = symbol_type == 'FX'
420
+ COMD = symbol_type == 'COMD'
421
+ FUT = symbol_type == 'FUT'
422
+ CRYPTO = symbol_type == 'CRYPTO'
412
423
  if COMD:
413
424
  supported = _COMMD_SUPPORTED_
414
425
  if self.symbol.split('.')[0] not in supported:
@@ -503,14 +514,14 @@ class RiskManagement(Account):
503
514
  trade_loss = (lot * contract_size) * tick_value_loss
504
515
  trade_profit = (lot * contract_size) * tick_value_profit
505
516
 
506
- if self.get_symbol_type(self.symbol) == 'IDX':
507
- rates = self.get_currency_rates(self.symbol)
508
- if rates['mc'] == rates['pc'] == 'JPY':
509
- lot = lot * contract_size
510
- lot = self._check_lot(lot)
511
- volume = round(lot * av_price * contract_size)
512
- if contract_size == 1:
513
- volume = round(lot * av_price)
517
+ # if self.get_symbol_type(self.symbol) == 'IDX':
518
+ # rates = self.get_currency_rates(self.symbol)
519
+ # if rates['mc'] == rates['pc'] == 'JPY':
520
+ # lot = lot * contract_size
521
+ # lot = self._check_lot(lot)
522
+ # volume = round(lot * av_price * contract_size)
523
+ # if contract_size == 1:
524
+ # volume = round(lot * av_price)
514
525
 
515
526
  return {
516
527
  'currency_risk': currency_risk,