bbstrader 0.0.1__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.

@@ -0,0 +1,226 @@
1
+ import pandas as pd
2
+ import MetaTrader5 as Mt5
3
+ from datetime import datetime
4
+ from typing import Union, Optional
5
+ from bbstrader.metatrader.utils import (
6
+ raise_mt5_error, TimeFrame, TIMEFRAMES
7
+ )
8
+ from bbstrader.metatrader.account import INIT_MSG
9
+ from pandas.tseries.offsets import CustomBusinessDay
10
+ from pandas.tseries.holiday import USFederalHolidayCalendar
11
+
12
+ MAX_BARS = 10_000_000
13
+
14
+
15
+ class Rates(object):
16
+ """
17
+ Provides methods to retrieve historical financial data from MetaTrader 5.
18
+
19
+ This class encapsulates interactions with the MetaTrader 5 (MT5) terminal
20
+ to fetch historical price data for a given symbol and timeframe. It offers
21
+ flexibility in retrieving data either by specifying a starting position
22
+ and count of bars or by providing a specific date range.
23
+
24
+ Example:
25
+ >>> rates = Rates("EURUSD", "1h")
26
+ >>> df = rates.get_historical_data(
27
+ ... date_from=datetime(2023, 1, 1),
28
+ ... date_to=datetime(2023, 1, 10),
29
+ ... )
30
+ >>> print(df.head())
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ symbol: str,
36
+ time_frame: TimeFrame = 'D1',
37
+ start_pos: Union[int | str] = 0,
38
+ count: Optional[int] = MAX_BARS,
39
+ session_duration: Optional[float] = None
40
+ ):
41
+ """
42
+ Initializes a new Rates instance.
43
+
44
+ Args:
45
+ symbol (str): Financial instrument symbol (e.g., "EURUSD").
46
+ time_frame (str): Timeframe string (e.g., "D1", "1h", "5m").
47
+ start_pos (int, | str): Starting index (int) or date (str) for data retrieval.
48
+ count (int, optional): Number of bars to retrieve default is
49
+ the maximum bars availble in the MT5 terminal.
50
+ session_duration (float): Number of trading hours per day.
51
+
52
+ Raises:
53
+ ValueError: If the provided timeframe is invalid.
54
+
55
+ Notes:
56
+ If `start_pos` is an str, it must be in 'YYYY-MM-DD' format.
57
+ For `session_duration` check your broker symbols details
58
+ """
59
+ self.symbol = symbol
60
+ self.time_frame = self._validate_time_frame(time_frame)
61
+ self.sd = session_duration
62
+ self.start_pos = self._get_start_pos(start_pos, time_frame)
63
+ self.count = count
64
+ self._mt5_initialized()
65
+ self.data = self.get_rates_from_pos()
66
+
67
+ def _get_start_pos(self, index, time_frame):
68
+ if isinstance(index, int):
69
+ start_pos = index
70
+ elif isinstance(index, str):
71
+ assert self.sd is not None, \
72
+ ValueError("Please provide the session_duration in hour")
73
+ start_pos = self._get_pos_index(index, time_frame, self.sd)
74
+ return start_pos
75
+
76
+ def _get_pos_index(self, start_date, time_frame, sd):
77
+ # Create a custom business day calendar
78
+ us_business_day = CustomBusinessDay(
79
+ calendar=USFederalHolidayCalendar())
80
+
81
+ start_date = pd.to_datetime(start_date)
82
+ end_date = pd.to_datetime(datetime.now())
83
+
84
+ # Generate a range of business days
85
+ trading_days = pd.date_range(
86
+ start=start_date, end=end_date, freq=us_business_day)
87
+
88
+ # Calculate the number of trading days
89
+ trading_days = len(trading_days)
90
+ td = trading_days
91
+ time_frame_mapping = {}
92
+ for minutes in [1, 2, 3, 4, 5, 6, 10, 12, 15, 20,
93
+ 30, 60, 120, 180, 240, 360, 480, 720]:
94
+ key = f"{minutes//60}h" if minutes >= 60 else f"{minutes}m"
95
+ time_frame_mapping[key] = int(td * (60 / minutes) * sd)
96
+ time_frame_mapping['D1'] = int(td)
97
+
98
+ if time_frame not in time_frame_mapping:
99
+ pv = list(time_frame_mapping.keys())
100
+ raise ValueError(
101
+ f"Unsupported time frame, Possible Values are {pv}")
102
+
103
+ index = time_frame_mapping.get(time_frame, 0)-1
104
+ return max(index, 0)
105
+
106
+ def _validate_time_frame(self, time_frame: str) -> int:
107
+ """Validates and returns the MT5 timeframe code."""
108
+ if time_frame not in TIMEFRAMES:
109
+ raise ValueError(
110
+ f"Unsupported time frame '{time_frame}'. "
111
+ f"Possible values are: {list(TIMEFRAMES.keys())}"
112
+ )
113
+ return TIMEFRAMES[time_frame]
114
+
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
+ def _fetch_data(
121
+ self, start: Union[int, datetime],
122
+ count: Union[int, datetime]
123
+ ) -> Union[pd.DataFrame, None]:
124
+ """Fetches data from MT5 and returns a DataFrame or None."""
125
+ try:
126
+ if isinstance(start, int) and isinstance(count, int):
127
+ rates = Mt5.copy_rates_from_pos(
128
+ self.symbol, self.time_frame, start, count
129
+ )
130
+ elif isinstance(start, datetime) and isinstance(count, datetime):
131
+ rates = Mt5.copy_rates_range(
132
+ self.symbol, self.time_frame, start, count
133
+ )
134
+ if rates is None:
135
+ return None
136
+
137
+ df = pd.DataFrame(rates)
138
+ return self._format_dataframe(df)
139
+ except Exception as e:
140
+ raise_mt5_error(e)
141
+
142
+ def _format_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
143
+ """Formats the raw MT5 data into a standardized DataFrame."""
144
+ df = df.copy()
145
+ df = df[['time', 'open', 'high', 'low', 'close', 'tick_volume']]
146
+ df.columns = ['Date', 'Open', 'High', 'Low', 'Close', 'Volume']
147
+ df['Adj Close'] = df['Close']
148
+ df = df[['Date', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume']]
149
+ df['Date'] = pd.to_datetime(df['Date'], unit='s')
150
+ df.set_index('Date', inplace=True)
151
+ return df
152
+
153
+ def get_rates_from_pos(self) -> Union[pd.DataFrame, None]:
154
+ """
155
+ Retrieves historical data starting from a specific position.
156
+
157
+ Uses the `start_pos` and `count` attributes specified during
158
+ initialization to fetch data.
159
+
160
+ Returns:
161
+ Union[pd.DataFrame, None]: A DataFrame containing historical
162
+ data if successful, otherwise None.
163
+ """
164
+ if self.start_pos is None or self.count is None:
165
+ raise ValueError(
166
+ "Both 'start_pos' and 'count' must be provided "
167
+ "when calling 'get_rates_from_pos'."
168
+ )
169
+ df = self._fetch_data(self.start_pos, self.count)
170
+ return df
171
+
172
+ @property
173
+ def get_open(self):
174
+ return self.data['Open']
175
+
176
+ @property
177
+ def get_high(self):
178
+ return self.data['High']
179
+
180
+ @property
181
+ def get_low(self):
182
+ return self.data['Low']
183
+
184
+ @property
185
+ def get_close(self):
186
+ return self.data['Close']
187
+
188
+ @property
189
+ def get_adj_close(self):
190
+ return self.data['Adj Close']
191
+
192
+ @property
193
+ def get_returns(self):
194
+ data = self.data.copy()
195
+ data['Returns'] = data['Adj Close'].pct_change()
196
+ data = data.dropna()
197
+ return data['Returns']
198
+
199
+ @property
200
+ def get_volume(self):
201
+ return self.data['Volume']
202
+
203
+ def get_historical_data(
204
+ self,
205
+ date_from: datetime,
206
+ date_to: datetime = datetime.now(),
207
+ save_csv: Optional[bool] = False,
208
+ ) -> Union[pd.DataFrame, None]:
209
+ """
210
+ Retrieves historical data within a specified date range.
211
+
212
+ Args:
213
+ date_from (datetime): Starting date for data retrieval.
214
+ date_to (datetime, optional): Ending date for data retrieval.
215
+ Defaults to the current time.
216
+ save_csv (str, optional): File path to save the data as a CSV.
217
+ If None, the data won't be saved.
218
+
219
+ Returns:
220
+ Union[pd.DataFrame, None]: A DataFrame containing historical data
221
+ if successful, otherwise None.
222
+ """
223
+ df = self._fetch_data(date_from, date_to)
224
+ if save_csv and df is not None:
225
+ df.to_csv(f"{self.symbol}.csv")
226
+ return df