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.
- bbstrader/__ini__.py +17 -0
- bbstrader/btengine/__init__.py +50 -0
- bbstrader/btengine/backtest.py +900 -0
- bbstrader/btengine/data.py +374 -0
- bbstrader/btengine/event.py +201 -0
- bbstrader/btengine/execution.py +83 -0
- bbstrader/btengine/performance.py +309 -0
- bbstrader/btengine/portfolio.py +326 -0
- bbstrader/btengine/strategy.py +31 -0
- bbstrader/metatrader/__init__.py +6 -0
- bbstrader/metatrader/account.py +1038 -0
- bbstrader/metatrader/rates.py +226 -0
- bbstrader/metatrader/risk.py +626 -0
- bbstrader/metatrader/trade.py +1296 -0
- bbstrader/metatrader/utils.py +669 -0
- bbstrader/models/__init__.py +6 -0
- bbstrader/models/risk.py +349 -0
- bbstrader/strategies.py +681 -0
- bbstrader/trading/__init__.py +4 -0
- bbstrader/trading/execution.py +965 -0
- bbstrader/trading/run.py +131 -0
- bbstrader/trading/utils.py +153 -0
- bbstrader/tseries.py +592 -0
- bbstrader-0.0.1.dist-info/LICENSE +21 -0
- bbstrader-0.0.1.dist-info/METADATA +132 -0
- bbstrader-0.0.1.dist-info/RECORD +28 -0
- bbstrader-0.0.1.dist-info/WHEEL +5 -0
- bbstrader-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -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
|