bbstrader 2.0.3__cp312-cp312-macosx_11_0_arm64.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.
Files changed (45) hide show
  1. bbstrader/__init__.py +27 -0
  2. bbstrader/__main__.py +92 -0
  3. bbstrader/api/__init__.py +96 -0
  4. bbstrader/api/handlers.py +245 -0
  5. bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
  6. bbstrader/api/metatrader_client.pyi +624 -0
  7. bbstrader/assets/bbs_.png +0 -0
  8. bbstrader/assets/bbstrader.ico +0 -0
  9. bbstrader/assets/bbstrader.png +0 -0
  10. bbstrader/assets/qs_metrics_1.png +0 -0
  11. bbstrader/btengine/__init__.py +54 -0
  12. bbstrader/btengine/backtest.py +358 -0
  13. bbstrader/btengine/data.py +737 -0
  14. bbstrader/btengine/event.py +229 -0
  15. bbstrader/btengine/execution.py +287 -0
  16. bbstrader/btengine/performance.py +408 -0
  17. bbstrader/btengine/portfolio.py +393 -0
  18. bbstrader/btengine/strategy.py +588 -0
  19. bbstrader/compat.py +28 -0
  20. bbstrader/config.py +100 -0
  21. bbstrader/core/__init__.py +27 -0
  22. bbstrader/core/data.py +628 -0
  23. bbstrader/core/strategy.py +466 -0
  24. bbstrader/metatrader/__init__.py +48 -0
  25. bbstrader/metatrader/_copier.py +720 -0
  26. bbstrader/metatrader/account.py +865 -0
  27. bbstrader/metatrader/broker.py +418 -0
  28. bbstrader/metatrader/copier.py +1487 -0
  29. bbstrader/metatrader/rates.py +495 -0
  30. bbstrader/metatrader/risk.py +667 -0
  31. bbstrader/metatrader/trade.py +1692 -0
  32. bbstrader/metatrader/utils.py +402 -0
  33. bbstrader/models/__init__.py +39 -0
  34. bbstrader/models/nlp.py +932 -0
  35. bbstrader/models/optimization.py +182 -0
  36. bbstrader/scripts.py +665 -0
  37. bbstrader/trading/__init__.py +33 -0
  38. bbstrader/trading/execution.py +1159 -0
  39. bbstrader/trading/strategy.py +362 -0
  40. bbstrader/trading/utils.py +69 -0
  41. bbstrader-2.0.3.dist-info/METADATA +396 -0
  42. bbstrader-2.0.3.dist-info/RECORD +45 -0
  43. bbstrader-2.0.3.dist-info/WHEEL +5 -0
  44. bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
  45. bbstrader-2.0.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,495 @@
1
+ from datetime import datetime
2
+ from typing import Optional, Union
3
+
4
+ import pandas as pd
5
+ from exchange_calendars import get_calendar, get_calendar_names
6
+
7
+ from bbstrader.metatrader.account import Account
8
+ from bbstrader.metatrader.broker import EXCHANGES
9
+ from bbstrader.metatrader.utils import TIMEFRAMES, SymbolType, raise_mt5_error
10
+
11
+ try:
12
+ import MetaTrader5 as Mt5
13
+ except ImportError:
14
+ import bbstrader.compat # noqa: F401
15
+
16
+
17
+ __all__ = [
18
+ "Rates",
19
+ "download_historical_data",
20
+ "get_data_from_pos",
21
+ "get_data_from_date",
22
+ ]
23
+
24
+ MAX_BARS = 10_000_000
25
+
26
+
27
+ IDX_CALENDARS = {
28
+ "CAD": "XTSE",
29
+ "AUD": "XASX",
30
+ "GBP": "XLON",
31
+ "HKD": "XSHG",
32
+ "ZAR": "XJSE",
33
+ "CHF": "XSWX",
34
+ "NOK": "XOSL",
35
+ "EUR": "XETR",
36
+ "SGD": "XSES",
37
+ "USD": "us_futures",
38
+ "JPY": "us_futures",
39
+ }
40
+
41
+
42
+ CALENDARS = {
43
+ SymbolType.FOREX: "us_futures",
44
+ SymbolType.STOCKS: EXCHANGES,
45
+ SymbolType.ETFs: EXCHANGES,
46
+ SymbolType.INDICES: IDX_CALENDARS,
47
+ SymbolType.COMMODITIES: "us_futures",
48
+ SymbolType.CRYPTO: "24/7",
49
+ SymbolType.FUTURES: "us_futures",
50
+ }
51
+
52
+ SESSION_TIMEFRAMES = [
53
+ Mt5.TIMEFRAME_D1,
54
+ Mt5.TIMEFRAME_W1,
55
+ Mt5.TIMEFRAME_H12,
56
+ Mt5.TIMEFRAME_MN1,
57
+ ]
58
+
59
+
60
+ class Rates(object):
61
+ """
62
+ Provides methods to retrieve historical financial data from MetaTrader 5.
63
+
64
+ This class encapsulates interactions with the MetaTrader 5 (MT5) terminal
65
+ to fetch historical price data for a given symbol and timeframe. It offers
66
+ flexibility in retrieving data either by specifying a starting position
67
+ and count of bars or by providing a specific date range .
68
+
69
+ Notes:
70
+ All data is rerturn as pandas.DataFrame
71
+
72
+ 1. Befor using this class, ensure that the `Max bars in chart` in your terminal
73
+ is set to a value that is greater than the number of bars you want to retrieve
74
+ or just set it to Unlimited.
75
+ In your MT5 terminal, go to `Tools` -> `Options` -> `Charts` -> `Max bars in chart`.
76
+
77
+ 2. The `open, high, low, close, adjclose, returns,
78
+ volume` properties returns data in Broker's timezone by default.
79
+
80
+ See `bbstrader.metatrader.broker.check_mt5_connection()` for more details on how to connect to MT5 terminal.
81
+
82
+ Example:
83
+ >>> rates = Rates("EURUSD", "1h")
84
+ >>> df = rates.get_historical_data(
85
+ ... date_from=datetime(2023, 1, 1),
86
+ ... date_to=datetime(2023, 1, 10),
87
+ ... )
88
+ >>> print(df.head())
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ symbol: str,
94
+ timeframe: str = "D1",
95
+ start_pos: int = 0,
96
+ count: Optional[int] = MAX_BARS,
97
+ **kwargs,
98
+ ):
99
+ """
100
+ Initializes a new Rates instance.
101
+
102
+ Args:
103
+ symbol (str): Financial instrument symbol (e.g., "EURUSD").
104
+ timeframe (str): Timeframe string (e.g., "D1", "1h", "5m").
105
+ start_pos (int): Starting index (int) for data retrieval.
106
+ count (int, optional): Number of bars to retrieve default is
107
+ the maximum bars availble in the MT5 terminal.
108
+ Raises:
109
+ ValueError: If the provided timeframe is invalid.
110
+ """
111
+ self.symbol = symbol
112
+ self.start_pos = start_pos
113
+ self.count = count
114
+ self.time_frame = self._validate_time_frame(timeframe)
115
+ self.__account = Account(**kwargs)
116
+ self.__data = self.get_rates_from_pos
117
+
118
+ @property
119
+ def open(self):
120
+ return self.__data()["Open"]
121
+
122
+ @property
123
+ def high(self):
124
+ return self.__data()["High"]
125
+
126
+ @property
127
+ def low(self):
128
+ return self.__data()["Low"]
129
+
130
+ @property
131
+ def close(self):
132
+ return self.__data()["Close"]
133
+
134
+ @property
135
+ def adjclose(self):
136
+ return self.__data()["Adj Close"]
137
+
138
+ @property
139
+ def returns(self):
140
+ """
141
+ Fractional change between the current and a prior element.
142
+
143
+ Computes the fractional change from the immediately previous row by default.
144
+ This is useful in comparing the fraction of change in a time series of elements.
145
+
146
+ Note
147
+ ----
148
+ It calculates fractional change (also known as `per unit change or relative change`)
149
+ and `not percentage change`. If you need the percentage change, multiply these values by 100.
150
+ """
151
+ data = self.__data()
152
+ data["Returns"] = data["Adj Close"].pct_change()
153
+ data = data.dropna()
154
+ return data["Returns"]
155
+
156
+ @property
157
+ def volume(self):
158
+ return self.__data()["Volume"]
159
+
160
+ def _validate_time_frame(self, time_frame: str) -> int:
161
+ """Validates and returns the MT5 timeframe code."""
162
+ if time_frame not in TIMEFRAMES:
163
+ raise ValueError(
164
+ f"Unsupported time frame '{time_frame}'. "
165
+ f"Possible values are: {list(TIMEFRAMES.keys())}"
166
+ )
167
+ return TIMEFRAMES[time_frame]
168
+
169
+ def _fetch_data(
170
+ self,
171
+ start: Union[int, datetime, pd.Timestamp],
172
+ count: Union[int, datetime, pd.Timestamp],
173
+ lower_colnames=False,
174
+ utc=False,
175
+ ) -> Union[pd.DataFrame, None]:
176
+ """Fetches data from MT5 and returns a DataFrame or None."""
177
+ try:
178
+ rates = None
179
+ if isinstance(start, int) and isinstance(count, int):
180
+ rates = Mt5.copy_rates_from_pos(
181
+ self.symbol, self.time_frame, start, count
182
+ )
183
+ elif isinstance(start, (datetime, pd.Timestamp)) and isinstance(count, int):
184
+ rates = Mt5.copy_rates_from(self.symbol, self.time_frame, start, count)
185
+ elif isinstance(start, (datetime, pd.Timestamp)) and isinstance(
186
+ count, (datetime, pd.Timestamp)
187
+ ):
188
+ rates = Mt5.copy_rates_range(self.symbol, self.time_frame, start, count)
189
+ if rates is None:
190
+ return None
191
+
192
+ df = pd.DataFrame(rates)
193
+ return self._format_dataframe(df, lower_colnames=lower_colnames, utc=utc)
194
+ except Exception as e:
195
+ raise_mt5_error(e)
196
+
197
+ def _format_dataframe(
198
+ self, df: pd.DataFrame, lower_colnames=False, utc=False
199
+ ) -> pd.DataFrame:
200
+ """Formats the raw MT5 data into a standardized DataFrame."""
201
+
202
+ df = df.copy()
203
+
204
+ df = df[["time", "open", "high", "low", "close", "tick_volume"]]
205
+ df.columns = ["Date", "Open", "High", "Low", "Close", "Volume"]
206
+
207
+ df["Adj Close"] = df["Close"]
208
+ df = df[["Date", "Open", "High", "Low", "Close", "Adj Close", "Volume"]]
209
+
210
+ df["Date"] = pd.to_datetime(df["Date"], unit="s", utc=utc)
211
+ df.set_index("Date", inplace=True)
212
+
213
+ if lower_colnames:
214
+ df.columns = df.columns.str.lower().str.replace(" ", "_")
215
+ df.index.name = df.index.name.lower().replace(" ", "_")
216
+
217
+ return df
218
+
219
+ def _filter_data(
220
+ self, df: pd.DataFrame, date_from=None, date_to=None, fill_na=False
221
+ ) -> pd.DataFrame:
222
+ df = df.copy()
223
+ symbol_type = self.__account.get_symbol_type(self.symbol)
224
+ currencies = self.__account.get_currency_rates(self.symbol)
225
+ if symbol_type in CALENDARS:
226
+ if symbol_type == SymbolType.STOCKS or symbol_type == SymbolType.ETFs:
227
+ for exchange in CALENDARS[symbol_type]:
228
+ if exchange in get_calendar_names():
229
+ symbols = self.__account.get_stocks_from_exchange(
230
+ exchange_code=exchange
231
+ )
232
+ if self.symbol in symbols:
233
+ calendar = get_calendar(exchange, side="right")
234
+ break
235
+ elif symbol_type == SymbolType.INDICES:
236
+ calendar = get_calendar(
237
+ CALENDARS[symbol_type][currencies["mc"]], side="right"
238
+ )
239
+ else:
240
+ calendar = get_calendar(CALENDARS[symbol_type], side="right")
241
+ date_from = date_from or df.index[0]
242
+ date_to = date_to or df.index[-1]
243
+ if self.time_frame in SESSION_TIMEFRAMES:
244
+ valid_sessions = calendar.sessions_in_range(date_from, date_to)
245
+ else:
246
+ valid_sessions = calendar.minutes_in_range(date_from, date_to)
247
+ if self.time_frame in [Mt5.TIMEFRAME_M1, Mt5.TIMEFRAME_D1]:
248
+ # save the index name of the dataframe
249
+ index_name = df.index.name
250
+ if fill_na:
251
+ if isinstance(fill_na, bool):
252
+ method = "nearest"
253
+ if isinstance(fill_na, str):
254
+ method = fill_na
255
+ df = df.reindex(valid_sessions, method=method)
256
+ else:
257
+ df.reindex(valid_sessions, method=None)
258
+ df.index = df.index.rename(index_name)
259
+ else:
260
+ df = df[df.index.isin(valid_sessions)]
261
+ return df
262
+
263
+ def _check_filter(self, filter, utc):
264
+ if filter and self.time_frame not in SESSION_TIMEFRAMES and not utc:
265
+ utc = True
266
+ elif filter and self.time_frame in SESSION_TIMEFRAMES and utc:
267
+ utc = False
268
+ return utc
269
+
270
+ def get_rates_from_pos(
271
+ self, filter=False, fill_na=False, lower_colnames=False, utc=False
272
+ ) -> Union[pd.DataFrame, None]:
273
+ """
274
+ Retrieves historical data starting from a specific position.
275
+
276
+ Uses the `start_pos` and `count` attributes specified during
277
+ initialization to fetch data.
278
+
279
+ Args:
280
+ filter : See `Rates.get_historical_data` for more details.
281
+ fill_na : See `Rates.get_historical_data` for more details.
282
+ lower_colnames : If True, the column names will be converted to lowercase.
283
+ utc (bool, optional): If True, the data will be in UTC timezone.
284
+ Defaults to False.
285
+
286
+ Returns:
287
+ Union[pd.DataFrame, None]: A DataFrame containing historical
288
+ data if successful, otherwise None.
289
+
290
+ Raises:
291
+ ValueError: If `start_pos` or `count` is not provided during
292
+ initialization.
293
+
294
+ Notes:
295
+ The Datetime for this method is in Broker's timezone.
296
+ """
297
+ if self.start_pos is None or self.count is None:
298
+ raise ValueError(
299
+ "Both 'start_pos' and 'count' must be provided "
300
+ "when calling 'get_rates_from_pos'."
301
+ )
302
+ utc = self._check_filter(filter, utc)
303
+ df = self._fetch_data(
304
+ self.start_pos, self.count, lower_colnames=lower_colnames, utc=utc
305
+ )
306
+ if df is None:
307
+ return None
308
+ if filter:
309
+ return self._filter_data(df, fill_na=fill_na)
310
+ return df
311
+
312
+ def get_rates_from(
313
+ self,
314
+ date_from: datetime | pd.Timestamp,
315
+ count: int = MAX_BARS,
316
+ filter=False,
317
+ fill_na=False,
318
+ lower_colnames=False,
319
+ utc=False,
320
+ ) -> Union[pd.DataFrame, None]:
321
+ """
322
+ Retrieves historical data within a specified date range.
323
+
324
+ Args:
325
+ date_from : Starting date for data retrieval.
326
+ The data will be retrieved from this date going to the past.
327
+
328
+ count : Number of bars to retrieve.
329
+
330
+ filter : See `Rates.get_historical_data` for more details.
331
+ fill_na : See `Rates.get_historical_data` for more details.
332
+ lower_colnames : If True, the column names will be converted to lowercase.
333
+ utc (bool, optional): If True, the data will be in UTC timezone.
334
+ Defaults to False.
335
+
336
+ Returns:
337
+ Union[pd.DataFrame, None]: A DataFrame containing historical
338
+ data if successful, otherwise None.
339
+ """
340
+ utc = self._check_filter(filter, utc)
341
+ df = self._fetch_data(date_from, count, lower_colnames=lower_colnames, utc=utc)
342
+ if df is None:
343
+ return None
344
+ if filter:
345
+ return self._filter_data(df, fill_na=fill_na)
346
+ return df
347
+
348
+ def get_historical_data(
349
+ self,
350
+ date_from: datetime | pd.Timestamp,
351
+ date_to: datetime | pd.Timestamp = pd.Timestamp.now(),
352
+ utc: bool = False,
353
+ filter: Optional[bool] = False,
354
+ fill_na: Optional[bool | str] = False,
355
+ lower_colnames: Optional[bool] = True,
356
+ save_csv: Optional[bool] = False,
357
+ ) -> Union[pd.DataFrame, None]:
358
+ """
359
+ Retrieves historical data within a specified date range.
360
+
361
+ Args:
362
+ date_from : Starting date for data retrieval.
363
+
364
+ date_to : Ending date for data retrieval.
365
+ Defaults to the current time.
366
+
367
+ utc : If True, the data will be in UTC timezone.
368
+ Defaults to False.
369
+
370
+ filter : If True, the data will be filtered based
371
+ on the trading sessions for the symbol.
372
+ This is use when we want to use the data for backtesting using Zipline.
373
+
374
+ fill_na : If True, the data will be filled with the nearest value.
375
+ This is use only when `filter` is True and time frame is "1m" or "D1",
376
+ this is because we use ``calendar.minutes_in_range`` or ``calendar.sessions_in_range``
377
+ where calendar is the ``ExchangeCalendar`` from `exchange_calendars` package.
378
+ So, for "1m" or "D1" time frame, the data will be filled with the nearest value
379
+ because the data from MT5 will have approximately the same number of rows as the
380
+ number of trading days or minute in the exchange calendar, so we can fill the missing
381
+ data with the nearest value.
382
+
383
+ But for other time frames, the data will be reindexed with the exchange calendar
384
+ because the data from MT5 will have more rows than the number of trading days or minute
385
+ in the exchange calendar. So we only take the data that is in the range of the exchange
386
+ calendar sessions or minutes.
387
+
388
+ lower_colnames : If True, the column names will be converted to lowercase.
389
+
390
+ save_csv : File path to save the data as a CSV.
391
+ If None, the data won't be saved.
392
+
393
+ Returns:
394
+ Union[pd.DataFrame, None]: A DataFrame containing historical data
395
+ if successful, otherwise None.
396
+
397
+ Raises:
398
+ ValueError: If the starting date is greater than the ending date.
399
+
400
+ Notes:
401
+ The `filter` for this method can be use only for Admira Markets Group (AMG) symbols.
402
+ The Datetime for this method is in Local timezone by default.
403
+ All STK symbols are filtered based on the the exchange calendar.
404
+ All FX symbols are filtered based on the ``us_futures`` calendar.
405
+ All IDX symbols are filtered based on the exchange calendar of margin currency.
406
+ All COMD symbols are filtered based on the exchange calendar of the commodity.
407
+ """
408
+ utc = self._check_filter(filter, utc)
409
+ df = self._fetch_data(
410
+ date_from, date_to, lower_colnames=lower_colnames, utc=utc
411
+ )
412
+ if df is None:
413
+ return None
414
+ if filter:
415
+ df = self._filter_data(
416
+ df, date_from=date_from, date_to=date_to, fill_na=fill_na
417
+ )
418
+ if save_csv:
419
+ df.to_csv(f"{self.symbol}.csv")
420
+ return df
421
+
422
+
423
+ def download_historical_data(
424
+ symbol,
425
+ timeframe,
426
+ date_from,
427
+ date_to=pd.Timestamp.now(),
428
+ lower_colnames=True,
429
+ utc=False,
430
+ filter=False,
431
+ fill_na=False,
432
+ save_csv=False,
433
+ **kwargs,
434
+ ):
435
+ """Download historical data from MetaTrader 5 terminal.
436
+ See `Rates.get_historical_data` for more details.
437
+ """
438
+ rates = Rates(symbol, timeframe, **kwargs)
439
+ data = rates.get_historical_data(
440
+ date_from=date_from,
441
+ date_to=date_to,
442
+ save_csv=save_csv,
443
+ utc=utc,
444
+ filter=filter,
445
+ lower_colnames=lower_colnames,
446
+ )
447
+ return data
448
+
449
+
450
+ def get_data_from_pos(
451
+ symbol,
452
+ timeframe,
453
+ start_pos=0,
454
+ fill_na=False,
455
+ count=MAX_BARS,
456
+ lower_colnames=False,
457
+ utc=False,
458
+ filter=False,
459
+ session_duration=23.0,
460
+ **kwargs,
461
+ ):
462
+ """Get historical data from a specific position.
463
+ See `Rates.get_rates_from_pos` for more details.
464
+ """
465
+ rates = Rates(symbol, timeframe, start_pos, count, session_duration, **kwargs)
466
+ data = rates.get_rates_from_pos(
467
+ filter=filter, fill_na=fill_na, lower_colnames=lower_colnames, utc=utc
468
+ )
469
+ return data
470
+
471
+
472
+ def get_data_from_date(
473
+ symbol,
474
+ timeframe,
475
+ date_from,
476
+ count=MAX_BARS,
477
+ fill_na=False,
478
+ lower_colnames=False,
479
+ utc=False,
480
+ filter=False,
481
+ **kwargs,
482
+ ):
483
+ """Get historical data from a specific date.
484
+ See `Rates.get_rates_from` for more details.
485
+ """
486
+ rates = Rates(symbol, timeframe, **kwargs)
487
+ data = rates.get_rates_from(
488
+ date_from,
489
+ count,
490
+ filter=filter,
491
+ fill_na=fill_na,
492
+ lower_colnames=lower_colnames,
493
+ utc=utc,
494
+ )
495
+ return data