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

@@ -8,6 +8,7 @@ from queue import Queue
8
8
  from abc import ABCMeta, abstractmethod
9
9
  from bbstrader.metatrader.rates import Rates
10
10
  from bbstrader.btengine.event import MarketEvent
11
+ from datetime import datetime
11
12
 
12
13
 
13
14
  __all__ = [
@@ -260,7 +261,7 @@ class HistoricCSVDataHandler(BaseCSVDataHandler):
260
261
  csv_dir = kwargs.get("csv_dir")
261
262
  super().__init__(events, symbol_list, csv_dir)
262
263
 
263
- MAX_BARS = 10_000_000
264
+
264
265
  class MT5HistoricDataHandler(BaseCSVDataHandler):
265
266
  """
266
267
  Downloads historical data from MetaTrader 5 (MT5) and provides
@@ -281,19 +282,16 @@ class MT5HistoricDataHandler(BaseCSVDataHandler):
281
282
  symbol_list (List[str]): A list of symbol strings to download data for.
282
283
  **kwargs: Keyword arguments for data retrieval:
283
284
  time_frame (str): MT5 time frame (e.g., 'D1' for daily).
284
- max_bars (int): Maximum number of bars to download per symbol.
285
- start_pos (int | str, optional): Starting bar position (default: 0).
286
- If it set to `str`, it must be in 'YYYY-MM-DD' format and
287
- session_duration (int | float): Number of trading hours per day.
285
+ mt5_start (datetime): Start date for historical data.
286
+ mt5_end (datetime): End date for historical data.
288
287
  mt5_data (str): Directory for storing data (default: 'mt5_data').
289
288
 
290
289
  Note:
291
290
  Requires a working connection to an MT5 terminal.
292
291
  """
293
292
  self.tf = kwargs.get('time_frame', 'D1')
294
- self.start_pos = kwargs.get('start_pos', 0)
295
- self.max_bars = kwargs.get('max_bars', MAX_BARS)
296
- self.sd = kwargs.get('session_duration', 6.5)
293
+ self.start = kwargs.get('mt5_start')
294
+ self.end = kwargs.get('mt5_end', datetime.now())
297
295
  self.data_dir = kwargs.get('mt5_data', 'mt5_data')
298
296
  self.symbol_list = symbol_list
299
297
  csv_dir = self._download_data(self.data_dir)
@@ -304,8 +302,10 @@ class MT5HistoricDataHandler(BaseCSVDataHandler):
304
302
  data_dir.mkdir(parents=True, exist_ok=True)
305
303
  for symbol in self.symbol_list:
306
304
  try:
307
- rate = Rates(symbol, self.tf, self.start_pos, self.max_bars, self.sd)
308
- data = rate.get_rates_from_pos()
305
+ rate = Rates(symbol=symbol, time_frame=self.tf)
306
+ data = rate.get_historical_data(
307
+ date_from=self.start, date_to=self.end
308
+ )
309
309
  if data is None:
310
310
  raise ValueError(f"No data found for {symbol}")
311
311
  data.to_csv(data_dir / f'{symbol}.csv')
@@ -359,16 +359,29 @@ class YFHistoricDataHandler(BaseCSVDataHandler):
359
359
  return cache_dir
360
360
 
361
361
 
362
- # TODO # Get data from FinancialModelingPrep ()
362
+ # TODO # Get data from EODHD
363
+ # https://eodhd.com/
364
+ class EODHDHistoricDataHandler(BaseCSVDataHandler):
365
+ ...
366
+
367
+ # TODO # Get data from FMP using Financialtoolkit API
368
+ # https://github.com/bbalouki/FinanceToolkit
363
369
  class FMPHistoricDataHandler(BaseCSVDataHandler):
364
370
  ...
365
371
 
366
372
 
367
373
  class BaseFMPDataHanler(object):
374
+ """
375
+ This will serve as the base class for all other FMP data
376
+ that is not historical data and does not have an OHLC structure.
377
+ """
368
378
  ...
369
379
 
370
380
 
371
381
  class FMPFundamentalDataHandler(BaseFMPDataHanler):
372
382
  ...
373
383
 
374
- # TODO Add other Handlers
384
+ # TODO Add other Handlers for FMP
385
+
386
+
387
+ # TODO Add data Handlers for Interactive Brokers
@@ -1,4 +1,5 @@
1
1
  from datetime import datetime
2
+ from typing import Literal
2
3
 
3
4
  __all__ = [
4
5
  "Event",
@@ -53,9 +54,10 @@ class SignalEvent(Event):
53
54
  strategy_id: int,
54
55
  symbol: str,
55
56
  datetime: datetime,
56
- signal_type: str,
57
+ signal_type: Literal['LONG', 'SHORT', 'EXIT'],
57
58
  quantity: int | float = 100,
58
- strength: int | float = 1.0
59
+ strength: int | float = 1.0,
60
+ price: int | float = None
59
61
  ):
60
62
  """
61
63
  Initialises the SignalEvent.
@@ -66,10 +68,11 @@ class SignalEvent(Event):
66
68
 
67
69
  symbol (str): The ticker symbol, e.g. 'GOOG'.
68
70
  datetime (datetime): The timestamp at which the signal was generated.
69
- signal_type (str): 'LONG' or 'SHORT'.
71
+ signal_type (str): 'LONG' or 'SHORT' or 'EXIT'.
70
72
  quantity (int | float): An optional integer (or float) representing the order size.
71
73
  strength (int | float): An adjustment factor "suggestion" used to scale
72
74
  quantity at the portfolio level. Useful for pairs strategies.
75
+ price (int | float): An optional price to be used when the signal is generated.
73
76
  """
74
77
  self.type = 'SIGNAL'
75
78
  self.strategy_id = strategy_id
@@ -78,6 +81,7 @@ class SignalEvent(Event):
78
81
  self.signal_type = signal_type
79
82
  self.quantity = quantity
80
83
  self.strength = strength
84
+ self.price = price
81
85
 
82
86
 
83
87
  class OrderEvent(Event):
@@ -95,9 +99,10 @@ class OrderEvent(Event):
95
99
 
96
100
  def __init__(self,
97
101
  symbol: str,
98
- order_type: str,
102
+ order_type: Literal['MKT', 'LMT'],
99
103
  quantity: int | float,
100
- direction: str
104
+ direction: Literal['BUY', 'SELL'],
105
+ price: int | float = None
101
106
  ):
102
107
  """
103
108
  Initialises the order type, setting whether it is
@@ -109,20 +114,22 @@ class OrderEvent(Event):
109
114
  order_type (str): 'MKT' or 'LMT' for Market or Limit.
110
115
  quantity (int | float): Non-negative number for quantity.
111
116
  direction (str): 'BUY' or 'SELL' for long or short.
117
+ price (int | float): The price at which to order.
112
118
  """
113
119
  self.type = 'ORDER'
114
120
  self.symbol = symbol
115
121
  self.order_type = order_type
116
122
  self.quantity = quantity
117
123
  self.direction = direction
124
+ self.price = price
118
125
 
119
126
  def print_order(self):
120
127
  """
121
128
  Outputs the values within the Order.
122
129
  """
123
130
  print(
124
- "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" %
125
- (self.symbol, self.order_type, self.quantity, self.direction)
131
+ "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s, Price=%s" %
132
+ (self.symbol, self.order_type, self.quantity, self.direction, self.price)
126
133
  )
127
134
 
128
135
 
@@ -150,8 +157,8 @@ class FillEvent(Event):
150
157
  symbol: str,
151
158
  exchange: str,
152
159
  quantity: int | float,
153
- direction: str,
154
- fill_cost: int | float,
160
+ direction: Literal['BUY', 'SELL'],
161
+ fill_cost: int | float | None,
155
162
  commission: float | None = None
156
163
  ):
157
164
  """
@@ -168,8 +175,8 @@ class FillEvent(Event):
168
175
  symbol (str): The instrument which was filled.
169
176
  exchange (str): The exchange where the order was filled.
170
177
  quantity (int | float): The filled quantity.
171
- direction (str): The direction of fill ('BUY' or 'SELL')
172
- fill_cost (int | float): The holdings value in dollars.
178
+ direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
179
+ fill_cost (int | float): Price of the shares when filled.
173
180
  commission (float | None): An optional commission sent from IB.
174
181
  """
175
182
  self.type = 'FILL'
@@ -1,11 +1,13 @@
1
- import datetime
1
+ from datetime import datetime
2
2
  from queue import Queue
3
3
  from abc import ABCMeta, abstractmethod
4
4
  from bbstrader.btengine.event import FillEvent, OrderEvent
5
+ from bbstrader.metatrader.account import Account
5
6
 
6
7
  __all__ = [
7
8
  "ExecutionHandler",
8
- "SimulatedExecutionHandler"
9
+ "SimulatedExecutionHandler",
10
+ "MT5ExecutionHandler"
9
11
  ]
10
12
 
11
13
 
@@ -51,7 +53,7 @@ class SimulatedExecutionHandler(ExecutionHandler):
51
53
  handler.
52
54
  """
53
55
 
54
- def __init__(self, events: Queue):
56
+ def __init__(self, events: Queue, **kwargs):
55
57
  """
56
58
  Initialises the handler, setting the event queues
57
59
  up internally.
@@ -71,13 +73,71 @@ class SimulatedExecutionHandler(ExecutionHandler):
71
73
  """
72
74
  if event.type == 'ORDER':
73
75
  fill_event = FillEvent(
74
- datetime.datetime.now(), event.symbol,
76
+ datetime.now(), event.symbol,
75
77
  'ARCA', event.quantity, event.direction, None
76
78
  )
77
79
  self.events.put(fill_event)
78
80
 
79
- # TODO # Use in live execution
80
-
81
81
 
82
82
  class MT5ExecutionHandler(ExecutionHandler):
83
- ...
83
+ def __init__(self, events: Queue, **kwargs):
84
+ """
85
+ """
86
+ self.events = events
87
+ self.account = Account()
88
+
89
+ def _estimate_total_fees(self, symbol, lot):
90
+ # TODO: Implement the calculation of fees based on the broker's fees
91
+ # https://www.metatrader5.com/en/terminal/help/trading/market_watch
92
+ # Calculate fees based on the broker's fees , swap and commission
93
+ return 0.0
94
+
95
+ def _calculate_lot(self, symbol, quantity, price):
96
+ FX = self.account.get_symbol_type(symbol) == 'FX'
97
+ COMD = self.account.get_symbol_type(symbol) == 'COMD'
98
+ FUT = self.account.get_symbol_type(symbol) == 'FUT'
99
+ CRYPTO = self.account.get_symbol_type(symbol) == 'CRYPTO'
100
+ symbol_info = self.account.get_symbol_info(symbol)
101
+ contract_size = symbol_info.trade_contract_size
102
+
103
+ lot = (quantity*price) / (contract_size * price)
104
+ if contract_size == 1:
105
+ lot = quantity
106
+ if COMD or FUT or CRYPTO and contract_size > 1:
107
+ lot = quantity / contract_size
108
+ if FX:
109
+ lot = (quantity*price / contract_size)
110
+ return self._check_lot(symbol, lot)
111
+
112
+ def _check_lot(self, symbol, lot):
113
+ symbol_info = self.account.get_symbol_info(symbol)
114
+ if lot < symbol_info.volume_min:
115
+ return symbol_info.volume_min
116
+ elif lot > symbol_info.volume_max:
117
+ return symbol_info.volume_max
118
+ return round(lot, 2)
119
+
120
+ def execute_order(self, event: OrderEvent):
121
+ """
122
+ Executes an Order event by converting it into a Fill event.
123
+
124
+ Args:
125
+ event (OrderEvent): Contains an Event object with order information.
126
+ """
127
+ if event.type == 'ORDER':
128
+ symbol = event.symbol
129
+ direction = event.direction
130
+ quantity = event.quantity
131
+ price = event.price
132
+ lot = self._calculate_lot(symbol, quantity, price)
133
+ fees = self._estimate_total_fees(symbol, lot)
134
+ fill_event = FillEvent(
135
+ timeindex=datetime.now(), symbol=symbol,
136
+ exchange='MT5', quantity=quantity, direction=direction,
137
+ fill_cost=None, commission=fees
138
+ )
139
+ self.events.put(fill_event)
140
+
141
+
142
+ class IBExecutionHandler(ExecutionHandler):
143
+ ...
@@ -6,9 +6,11 @@ import yfinance as yf
6
6
  from scipy.stats import mstats
7
7
  import matplotlib.pyplot as plt
8
8
  from matplotlib.ticker import MaxNLocator
9
+ import quantstats as qs
9
10
 
10
11
  import warnings
11
12
  warnings.filterwarnings("ignore")
13
+ warnings.simplefilter(action='ignore', category=FutureWarning)
12
14
  sns.set_theme()
13
15
 
14
16
  __all__ = [
@@ -17,7 +19,8 @@ __all__ = [
17
19
  "create_sharpe_ratio",
18
20
  "create_sortino_ratio",
19
21
  "plot_returns_and_dd",
20
- "plot_monthly_yearly_returns"
22
+ "plot_monthly_yearly_returns",
23
+ "show_qs_stats"
21
24
  ]
22
25
 
23
26
 
@@ -307,3 +310,33 @@ def plot_monthly_yearly_returns(df, title):
307
310
 
308
311
  # Show the plot
309
312
  plt.show()
313
+
314
+ def show_qs_stats(equity_curve, benchmark, strategy_name):
315
+ """
316
+ Generate the full quantstats report for the strategy.
317
+
318
+ Args:
319
+ equity_curve (pd.DataFrame):
320
+ The DataFrame containing the strategy returns and drawdowns.
321
+ benchmark (str):
322
+ The ticker symbol of the benchmark to compare the strategy to.
323
+ strategy_name (str): The name of the strategy.
324
+ """
325
+ # Load the returns data
326
+ returns = equity_curve.copy()
327
+
328
+ # Drop duplicate index entries
329
+ returns = returns[~returns.index.duplicated(keep='first')]
330
+
331
+ # Extend pandas functionality with quantstats
332
+ qs.extend_pandas()
333
+
334
+ # Generate the full report with a benchmark
335
+ file = strategy_name.replace(' ', '_')
336
+ qs.reports.full(
337
+ returns['Returns'], mode='full', benchmark=benchmark)
338
+ qs.reports.html(
339
+ returns['Returns'],
340
+ benchmark='SPY',
341
+ output=f"{file}_report.html",
342
+ title=strategy_name)
@@ -6,10 +6,10 @@ from bbstrader.btengine.event import (
6
6
  )
7
7
  from bbstrader.btengine.data import DataHandler
8
8
  from bbstrader.btengine.performance import (
9
- create_drawdowns, plot_performance,
10
- create_sharpe_ratio, create_sortino_ratio,
9
+ create_drawdowns, plot_performance, show_qs_stats,
11
10
  plot_returns_and_dd, plot_monthly_yearly_returns
12
11
  )
12
+ import quantstats as qs
13
13
 
14
14
 
15
15
  class Portfolio(object):
@@ -20,7 +20,7 @@ class Portfolio(object):
20
20
  sizing tools (such as the `Kelly Criterion`).
21
21
 
22
22
  The portfolio order management system is possibly the most complex component of
23
- an eventdriven backtester. Its role is to keep track of all current market positions
23
+ an event driven backtester. Its role is to keep track of all current market positions
24
24
  as well as the market value of the positions (known as the "holdings").
25
25
  This is simply an estimate of the liquidation value of the position and is derived in part
26
26
  from the data handling facility of the backtester.
@@ -95,7 +95,7 @@ class Portfolio(object):
95
95
  self.timeframe = kwargs.get("time_frame", "D1")
96
96
  self.trading_hours = kwargs.get("session_duration", 6.5)
97
97
  self.benchmark = kwargs.get('benchmark', 'SPY')
98
- self.strategy_name = kwargs.get('strategy_name', 'Strategy')
98
+ self.strategy_name = kwargs.get('strategy_name', '')
99
99
  if self.timeframe not in self._tf_mapping():
100
100
  raise ValueError(
101
101
  f"Timeframe not supported,"
@@ -227,10 +227,10 @@ class Portfolio(object):
227
227
  fill_dir = -1
228
228
 
229
229
  # Update holdings list with new quantities
230
- fill_cost = self.bars.get_latest_bar_value(
230
+ price = self.bars.get_latest_bar_value(
231
231
  fill.symbol, "Adj Close"
232
232
  )
233
- cost = fill_dir * fill_cost * fill.quantity
233
+ cost = fill_dir * price * fill.quantity
234
234
  self.current_holdings[fill.symbol] += cost
235
235
  self.current_holdings['Commission'] += fill.commission
236
236
  self.current_holdings['Cash'] -= (cost + fill.commission)
@@ -260,20 +260,21 @@ class Portfolio(object):
260
260
  direction = signal.signal_type
261
261
  quantity = signal.quantity
262
262
  strength = signal.strength
263
+ price = signal.price
263
264
  cur_quantity = self.current_positions[symbol]
264
265
 
265
266
  order_type = 'MKT'
266
267
  mkt_quantity = round(quantity * strength)
267
268
 
268
269
  if direction == 'LONG' and cur_quantity == 0:
269
- order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
270
+ order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY', price)
270
271
  if direction == 'SHORT' and cur_quantity == 0:
271
- order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')
272
+ order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL', price)
272
273
 
273
274
  if direction == 'EXIT' and cur_quantity > 0:
274
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
275
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price)
275
276
  if direction == 'EXIT' and cur_quantity < 0:
276
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
277
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price)
277
278
 
278
279
  return order
279
280
 
@@ -305,9 +306,12 @@ class Portfolio(object):
305
306
  returns = self.equity_curve['Returns']
306
307
  pnl = self.equity_curve['Equity Curve']
307
308
 
308
- sharpe_ratio = create_sharpe_ratio(returns, periods=self.tf)
309
- sortino_ratio = create_sortino_ratio(returns, periods=self.tf)
310
- drawdown, max_dd, dd_duration = create_drawdowns(pnl)
309
+ sharpe_ratio = qs.stats.sharpe(returns, periods=self.tf)
310
+ sortino_ratio = qs.stats.sortino(returns, periods=self.tf)
311
+ drawdown, _, _ = create_drawdowns(pnl)
312
+ max_dd = qs.stats.max_drawdown(returns)
313
+ dd_details = qs.stats.drawdown_details(drawdown)
314
+ dd_duration = dd_details['days'].max()
311
315
  self.equity_curve['Drawdown'] = drawdown
312
316
 
313
317
  stats = [
@@ -317,10 +321,16 @@ class Portfolio(object):
317
321
  ("Max Drawdown", f"{max_dd * 100.0:.2f}%"),
318
322
  ("Drawdown Duration", f"{dd_duration}")
319
323
  ]
320
- self.equity_curve.to_csv('equity.csv')
324
+ now = datetime.now().strftime('%Y%m%d%H%M%S')
325
+ strategy_name = self.strategy_name.replace(' ', '_')
326
+ file = f"{strategy_name}_{now}_equity.csv"
327
+ self.equity_curve.to_csv(file)
321
328
  plot_performance(self.equity_curve, self.strategy_name)
322
329
  plot_returns_and_dd(self.equity_curve,
323
330
  self.benchmark, self.strategy_name)
331
+ qs.plots.monthly_heatmap(returns, savefig='monthly_heatmap.png')
324
332
  plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
333
+ show_qs_stats(self.equity_curve, self.benchmark, self.strategy_name)
325
334
 
326
335
  return stats
336
+
@@ -1,5 +1,5 @@
1
1
  from abc import ABCMeta, abstractmethod
2
-
2
+ from typing import Dict, Union
3
3
 
4
4
  class Strategy(metaclass=ABCMeta):
5
5
  """
@@ -22,10 +22,11 @@ class Strategy(metaclass=ABCMeta):
22
22
  """
23
23
 
24
24
  @abstractmethod
25
- def calculate_signals(self):
25
+ def calculate_signals(self, *args, **kwargs) -> Dict[str, Union[str, None]]:
26
26
  """
27
27
  Provides the mechanisms to calculate the list of signals.
28
+ This methods should return a dictionary of symbols and their respective signals.
28
29
  """
29
30
  raise NotImplementedError(
30
31
  "Should implement calculate_signals()"
31
- )
32
+ )
@@ -555,13 +555,19 @@ class Account(object):
555
555
 
556
556
  Raises:
557
557
  MT5TerminalError: A specific exception based on the error code.
558
+
559
+ Notes:
560
+ The `time` property is converted to a `datetime` object using Broker server time.
558
561
  """
559
562
  try:
560
563
  symbol_info = mt5.symbol_info(symbol)
561
564
  if symbol_info is None:
562
565
  return None
563
566
  else:
564
- return SymbolInfo(**symbol_info._asdict())
567
+ symbol_info_dict = symbol_info._asdict()
568
+ time = datetime.fromtimestamp(symbol_info.time)
569
+ symbol_info_dict['time'] = time
570
+ return SymbolInfo(**symbol_info_dict)
565
571
  except Exception as e:
566
572
  msg = self._symbol_info_msg(symbol)
567
573
  raise_mt5_error(message=f"{e+msg}")
@@ -595,13 +601,19 @@ class Account(object):
595
601
 
596
602
  Raises:
597
603
  MT5TerminalError: A specific exception based on the error code.
604
+
605
+ Notes:
606
+ The `time` property is converted to a `datetime` object using Broker server time.
598
607
  """
599
608
  try:
600
609
  tick_info = mt5.symbol_info_tick(symbol)
601
610
  if tick_info is None:
602
611
  return None
603
612
  else:
604
- return TickInfo(**tick_info._asdict())
613
+ info_dict = tick_info._asdict()
614
+ time = datetime.fromtimestamp(tick_info.time)
615
+ info_dict['time'] = time
616
+ return TickInfo(**info_dict)
605
617
  except Exception as e:
606
618
  msg = self._symbol_info_msg(symbol)
607
619
  raise_mt5_error(message=f"{e+msg}")
@@ -770,7 +782,7 @@ class Account(object):
770
782
  else:
771
783
  positions = mt5.positions_get()
772
784
 
773
- if positions is None:
785
+ if positions is None or len(positions) == 0:
774
786
  return None
775
787
  if to_df:
776
788
  df = pd.DataFrame(list(positions), columns=positions[0]._asdict())
@@ -873,7 +885,7 @@ class Account(object):
873
885
  else:
874
886
  position_deals = mt5.history_deals_get(date_from, date_to)
875
887
 
876
- if position_deals is None:
888
+ if position_deals is None or len(position_deals) == 0:
877
889
  return None
878
890
 
879
891
  df = pd.DataFrame(list(position_deals),
@@ -949,7 +961,7 @@ class Account(object):
949
961
  else:
950
962
  orders = mt5.orders_get()
951
963
 
952
- if orders is None:
964
+ if orders is None or len(orders) == 0:
953
965
  return None
954
966
 
955
967
  if to_df:
@@ -1052,7 +1064,7 @@ class Account(object):
1052
1064
  else:
1053
1065
  history_orders = mt5.history_orders_get(date_from, date_to)
1054
1066
 
1055
- if history_orders is None:
1067
+ if history_orders is None or len(history_orders) == 0:
1056
1068
  return None
1057
1069
 
1058
1070
  df = pd.DataFrame(list(history_orders),
@@ -21,6 +21,15 @@ class Rates(object):
21
21
  flexibility in retrieving data either by specifying a starting position
22
22
  and count of bars or by providing a specific date range.
23
23
 
24
+ Notes:
25
+ 1. Befor using this class, ensure that the `Max bars in chart` in you terminal
26
+ is set to a value that is greater than the number of bars you want to retrieve
27
+ or just set it to Unlimited.
28
+ In your MT5 terminal, go to `Tools` -> `Options` -> `Charts` -> `Max bars in chart`.
29
+
30
+ 2. The `get_open, get_high, get_low, get_close, get_adj_close, get_returns,
31
+ get_volume` properties return data in Broker's timezone.
32
+
24
33
  Example:
25
34
  >>> rates = Rates("EURUSD", "1h")
26
35
  >>> df = rates.get_historical_data(
@@ -62,8 +71,14 @@ class Rates(object):
62
71
  self.start_pos = self._get_start_pos(start_pos, time_frame)
63
72
  self.count = count
64
73
  self._mt5_initialized()
65
- self.data = self.get_rates_from_pos()
74
+ self.__data = self.get_rates_from_pos()
75
+
66
76
 
77
+ def _mt5_initialized(self):
78
+ """Ensures the MetaTrader 5 Terminal is initialized."""
79
+ if not Mt5.initialize():
80
+ raise_mt5_error(message=INIT_MSG)
81
+
67
82
  def _get_start_pos(self, index, time_frame):
68
83
  if isinstance(index, int):
69
84
  start_pos = index
@@ -112,11 +127,6 @@ class Rates(object):
112
127
  )
113
128
  return TIMEFRAMES[time_frame]
114
129
 
115
- def _mt5_initialized(self):
116
- """Ensures the MetaTrader 5 Terminal is initialized."""
117
- if not Mt5.initialize():
118
- raise_mt5_error(message=INIT_MSG)
119
-
120
130
  def _fetch_data(
121
131
  self, start: Union[int, datetime],
122
132
  count: Union[int, datetime]
@@ -160,6 +170,13 @@ class Rates(object):
160
170
  Returns:
161
171
  Union[pd.DataFrame, None]: A DataFrame containing historical
162
172
  data if successful, otherwise None.
173
+
174
+ Raises:
175
+ ValueError: If `start_pos` or `count` is not provided during
176
+ initialization.
177
+
178
+ Notes:
179
+ The Datetime for this method is in Broker's timezone.
163
180
  """
164
181
  if self.start_pos is None or self.count is None:
165
182
  raise ValueError(
@@ -171,23 +188,23 @@ class Rates(object):
171
188
 
172
189
  @property
173
190
  def get_open(self):
174
- return self.data['Open']
191
+ return self.__data['Open']
175
192
 
176
193
  @property
177
194
  def get_high(self):
178
- return self.data['High']
195
+ return self.__data['High']
179
196
 
180
197
  @property
181
198
  def get_low(self):
182
- return self.data['Low']
199
+ return self.__data['Low']
183
200
 
184
201
  @property
185
202
  def get_close(self):
186
- return self.data['Close']
203
+ return self.__data['Close']
187
204
 
188
205
  @property
189
206
  def get_adj_close(self):
190
- return self.data['Adj Close']
207
+ return self.__data['Adj Close']
191
208
 
192
209
  @property
193
210
  def get_returns(self):
@@ -202,7 +219,7 @@ class Rates(object):
202
219
  It calculates fractional change (also known as `per unit change or relative change`)
203
220
  and `not percentage change`. If you need the percentage change, multiply these values by 100.
204
221
  """
205
- data = self.data.copy()
222
+ data = self.__data.copy()
206
223
  data['Returns'] = data['Adj Close'].pct_change()
207
224
  data = data.dropna()
208
225
  return data['Returns']
@@ -230,6 +247,12 @@ class Rates(object):
230
247
  Returns:
231
248
  Union[pd.DataFrame, None]: A DataFrame containing historical data
232
249
  if successful, otherwise None.
250
+
251
+ Raises:
252
+ ValueError: If the starting date is greater than the ending date.
253
+
254
+ Notes:
255
+ The Datetime for this method is in UTC timezone.
233
256
  """
234
257
  df = self._fetch_data(date_from, date_to)
235
258
  if save_csv and df is not None: