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,737 @@
1
+ import os.path
2
+ from abc import ABCMeta, abstractmethod
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from queue import Queue
6
+ from typing import Any, Dict, Generator, List, Optional, Tuple, Union
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import yfinance as yf
11
+ from eodhd import APIClient
12
+ from financetoolkit import Toolkit
13
+ from numpy.typing import NDArray
14
+ from pytz import timezone
15
+
16
+ from bbstrader.btengine.event import MarketEvent
17
+ from bbstrader.config import BBSTRADER_DIR
18
+ from bbstrader.metatrader.rates import download_historical_data
19
+
20
+ __all__ = [
21
+ "DataHandler",
22
+ "CSVDataHandler",
23
+ "MT5DataHandler",
24
+ "YFDataHandler",
25
+ "EODHDataHandler",
26
+ "FMPDataHandler",
27
+ ]
28
+
29
+
30
+ class DataHandler(metaclass=ABCMeta):
31
+ """
32
+ One of the goals of an event-driven trading system is to minimise
33
+ duplication of code between the backtesting element and the live execution
34
+ element. Ideally it would be optimal to utilise the same signal generation
35
+ methodology and portfolio management components for both historical testing
36
+ and live trading. In order for this to work the Strategy object which generates
37
+ the Signals, and the `Portfolio` object which provides Orders based on them,
38
+ must utilise an identical interface to a market feed for both historic and live
39
+ running.
40
+
41
+ This motivates the concept of a class hierarchy based on a `DataHandler` object,
42
+ which givesall subclasses an interface for providing market data to the remaining
43
+ components within thesystem. In this way any subclass data handler can be "swapped out",
44
+ without affecting strategy or portfolio calculation.
45
+
46
+ Specific example subclasses could include `HistoricCSVDataHandler`,
47
+ `YFinanceDataHandler`, `FMPDataHandler`, `IBMarketFeedDataHandler` etc.
48
+ """
49
+
50
+ @property
51
+ def symbols(self) -> List[str]:
52
+ pass
53
+
54
+ @property
55
+ def data(self) -> Dict[str, pd.DataFrame]:
56
+ pass
57
+
58
+ @property
59
+ def labels(self) -> List[str]:
60
+ pass
61
+
62
+ @property
63
+ def index(self) -> Union[str, List[str]]:
64
+ pass
65
+
66
+ @abstractmethod
67
+ def get_latest_bar(self, symbol: str) -> pd.Series:
68
+ """
69
+ Returns the last bar updated.
70
+ """
71
+ raise NotImplementedError("Should implement get_latest_bar()")
72
+
73
+ @abstractmethod
74
+ def get_latest_bars(
75
+ self, symbol: str, N: int = 1, df: bool = True
76
+ ) -> Union[pd.DataFrame, List[pd.Series]]:
77
+ """
78
+ Returns the last N bars updated.
79
+ """
80
+ raise NotImplementedError("Should implement get_latest_bars()")
81
+
82
+ @abstractmethod
83
+ def get_latest_bar_datetime(self, symbol: str) -> Union[datetime, pd.Timestamp]:
84
+ """
85
+ Returns a Python datetime object for the last bar.
86
+ """
87
+ raise NotImplementedError("Should implement get_latest_bar_datetime()")
88
+
89
+ @abstractmethod
90
+ def get_latest_bar_value(self, symbol: str, val_type: str) -> float:
91
+ """
92
+ Returns one of the Open, High, Low, Close, Adj Close, Volume or Returns
93
+ from the last bar.
94
+ """
95
+ raise NotImplementedError("Should implement get_latest_bar_value()")
96
+
97
+ @abstractmethod
98
+ def get_latest_bars_values(self, symbol: str, val_type: str, N: int = 1) -> NDArray:
99
+ """
100
+ Returns the last N bar values from the
101
+ latest_symbol list, or N-k if less available.
102
+ """
103
+ raise NotImplementedError("Should implement get_latest_bars_values()")
104
+
105
+ @abstractmethod
106
+ def update_bars(self) -> None:
107
+ """
108
+ Pushes the latest bars to the bars_queue for each symbol
109
+ in a tuple OHLCVI format: (datetime, Open, High, Low,
110
+ Close, Adj Close, Volume, Retruns).
111
+ """
112
+ raise NotImplementedError("Should implement update_bars()")
113
+
114
+
115
+ class BaseCSVDataHandler(DataHandler):
116
+ """
117
+ Base class for handling data loaded from CSV files.
118
+
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ events: "Queue[MarketEvent]",
124
+ symbol_list: List[str],
125
+ csv_dir: str,
126
+ columns: Optional[List[str]] = None,
127
+ index_col: Union[str, int, List[str], List[int]] = 0,
128
+ ) -> None:
129
+ """
130
+ Initialises the data handler by requesting the location of the CSV files
131
+ and a list of symbols.
132
+
133
+ Args:
134
+ events : The Event Queue.
135
+ symbol_list : A list of symbol strings.
136
+ csv_dir : Absolute directory path to the CSV files.
137
+ columns : List of column names to use for the data.
138
+ index_col : Column to use as the index.
139
+ """
140
+ self.events = events
141
+ self.symbol_list = symbol_list
142
+ self.csv_dir = csv_dir
143
+ self.columns = columns
144
+ self.index_col = index_col
145
+ self.symbol_data: Dict[str, Union[pd.DataFrame, Generator]] = {}
146
+ self.latest_symbol_data: Dict[str, List[Any]] = {}
147
+ self.continue_backtest = True
148
+ self._index: Optional[Union[str, List[str]]] = None
149
+ self._load_and_process_data()
150
+
151
+ @property
152
+ def symbols(self) -> List[str]:
153
+ return self.symbol_list
154
+
155
+ @property
156
+ def data(self) -> Dict[str, pd.DataFrame]:
157
+ return self.symbol_data # type: ignore
158
+
159
+ @property
160
+ def datadir(self) -> str:
161
+ return self.csv_dir
162
+
163
+ @property
164
+ def labels(self) -> List[str]:
165
+ return self.columns # type: ignore
166
+
167
+ @property
168
+ def index(self) -> Union[str, List[str]]:
169
+ return self._index # type: ignore
170
+
171
+ def _load_and_process_data(self) -> None:
172
+ """
173
+ Opens the CSV files from the data directory, converting
174
+ them into pandas DataFrames within a symbol dictionary.
175
+ """
176
+ default_names = pd.read_csv(
177
+ os.path.join(self.csv_dir, f"{self.symbol_list[0]}.csv")
178
+ ).columns.to_list()
179
+ new_names = self.columns or default_names
180
+ new_names = [name.strip().lower().replace(" ", "_") for name in new_names]
181
+ self.columns = new_names
182
+ assert "adj_close" in new_names or "close" in new_names, (
183
+ "Column names must contain 'Adj Close' and 'Close' or adj_close and close"
184
+ )
185
+ comb_index = None
186
+ for s in self.symbol_list:
187
+ # Load the CSV file with no header information,
188
+ # indexed on date
189
+ self.symbol_data[s] = pd.read_csv(
190
+ os.path.join(self.csv_dir, f"{s}.csv"),
191
+ header=0,
192
+ index_col=self.index_col,
193
+ parse_dates=True,
194
+ names=new_names,
195
+ )
196
+ self.symbol_data[s].sort_index(inplace=True)
197
+ # Combine the index to pad forward values
198
+ if comb_index is None:
199
+ comb_index = self.symbol_data[s].index
200
+ elif len(self.symbol_data[s].index) > len(comb_index):
201
+ comb_index = self.symbol_data[s].index
202
+ # Set the latest symbol_data to None
203
+ self.latest_symbol_data[s] = []
204
+
205
+ # Reindex the dataframes
206
+ for s in self.symbol_list:
207
+ self.symbol_data[s] = self.symbol_data[s].reindex( # type: ignore
208
+ index=comb_index, method="pad"
209
+ )
210
+ if "adj_close" not in new_names:
211
+ self.columns.append("adj_close")
212
+ self.symbol_data[s]["adj_close"] = self.symbol_data[s]["close"] # type: ignore
213
+ self.symbol_data[s]["returns"] = ( # type: ignore
214
+ self.symbol_data[s][ # type: ignore
215
+ "adj_close" if "adj_close" in new_names else "close"
216
+ ]
217
+ .pct_change()
218
+ .dropna()
219
+ )
220
+ self._index = self.symbol_data[s].index.name # type: ignore
221
+ self.symbol_data[s].to_csv(os.path.join(self.csv_dir, f"{s}.csv")) # type: ignore
222
+ if self.events is not None:
223
+ self.symbol_data[s] = self.symbol_data[s].iterrows() # type: ignore
224
+
225
+ def _get_new_bar(self, symbol: str) -> Generator[Tuple[Any, Any], Any, None]:
226
+ """
227
+ Returns the latest bar from the data feed.
228
+ """
229
+ for b in self.symbol_data[symbol]: # type: ignore
230
+ yield b
231
+
232
+ def get_latest_bar(self, symbol: str) -> pd.Series:
233
+ """
234
+ Returns the last bar from the latest_symbol list.
235
+ """
236
+ try:
237
+ bars_list = self.latest_symbol_data[symbol]
238
+ except KeyError:
239
+ print(f"{symbol} not available in the historical data set.")
240
+ raise
241
+ else:
242
+ return bars_list[-1]
243
+
244
+ def get_latest_bars(
245
+ self, symbol: str, N: int = 1, df: bool = True
246
+ ) -> Union[pd.DataFrame, List[pd.Series]]:
247
+ """
248
+ Returns the last N bars from the latest_symbol list,
249
+ or N-k if less available.
250
+ """
251
+ try:
252
+ bars_list = self.latest_symbol_data[symbol]
253
+ except KeyError:
254
+ print(f"{symbol} not available in the historical data set.")
255
+ raise
256
+ else:
257
+ if df:
258
+ df_ = pd.DataFrame([bar[1] for bar in bars_list[-N:]])
259
+ df_.index.name = self._index # type: ignore
260
+ return df_
261
+ return bars_list[-N:]
262
+
263
+ def get_latest_bar_datetime(self, symbol: str) -> Union[datetime, pd.Timestamp]:
264
+ """
265
+ Returns a Python datetime object for the last bar.
266
+ """
267
+ try:
268
+ bars_list = self.latest_symbol_data[symbol]
269
+ except KeyError:
270
+ print(f"{symbol} not available in the historical data set.")
271
+ raise
272
+ else:
273
+ return bars_list[-1][0]
274
+
275
+ def get_latest_bars_datetime(
276
+ self, symbol: str, N: int = 1
277
+ ) -> List[Union[datetime, pd.Timestamp]]:
278
+ """
279
+ Returns a list of Python datetime objects for the last N bars.
280
+ """
281
+ try:
282
+ bars_list = self.get_latest_bars(symbol, N) # type: ignore
283
+ except KeyError:
284
+ print(f"{symbol} not available in the historical data set for .")
285
+ raise
286
+ else:
287
+ return [b[0] for b in bars_list] # type: ignore
288
+
289
+ def get_latest_bar_value(self, symbol: str, val_type: str) -> float:
290
+ """
291
+ Returns one of the Open, High, Low, Close, Volume or OI
292
+ values from the pandas Bar series object.
293
+ """
294
+ try:
295
+ bars_list = self.latest_symbol_data[symbol]
296
+ except KeyError:
297
+ print(f"{symbol} not available in the historical data set.")
298
+ raise
299
+ else:
300
+ try:
301
+ return getattr(bars_list[-1][1], val_type)
302
+ except AttributeError:
303
+ print(
304
+ f"Value type {val_type} not available in the historical data set for {symbol}."
305
+ )
306
+ raise
307
+
308
+ def get_latest_bars_values(self, symbol: str, val_type: str, N: int = 1) -> NDArray:
309
+ """
310
+ Returns the last N bar values from the
311
+ latest_symbol list, or N-k if less available.
312
+ """
313
+ try:
314
+ bars_list = self.get_latest_bars(symbol, N, df=False)
315
+ except KeyError:
316
+ print(f"{symbol} not available in the historical data set.")
317
+ raise
318
+ else:
319
+ try:
320
+ return np.array([getattr(b[1], val_type) for b in bars_list])
321
+ except AttributeError:
322
+ print(
323
+ f"Value type {val_type} not available in the historical data set."
324
+ )
325
+ raise
326
+
327
+ def update_bars(self) -> None:
328
+ """
329
+ Pushes the latest bar to the latest_symbol_data structure
330
+ for all symbols in the symbol list.
331
+ """
332
+ for s in self.symbol_list:
333
+ try:
334
+ bar = next(self._get_new_bar(s))
335
+ except StopIteration:
336
+ self.continue_backtest = False
337
+ else:
338
+ if bar is not None:
339
+ self.latest_symbol_data[s].append(bar)
340
+ self.events.put(MarketEvent())
341
+
342
+
343
+ class CSVDataHandler(BaseCSVDataHandler):
344
+ """
345
+ `CSVDataHandler` is designed to read CSV files for
346
+ each requested symbol from disk and provide an interface
347
+ to obtain the "latest" bar in a manner identical to a live
348
+ trading interface.
349
+
350
+ This class is useful when you have your own data or you want
351
+ to cutomize specific data in some form based on your `Strategy()` .
352
+ """
353
+
354
+ def __init__(
355
+ self, events: "Queue[MarketEvent]", symbol_list: List[str], **kwargs: Any
356
+ ) -> None:
357
+ """
358
+ Initialises the historic data handler by requesting
359
+ the location of the CSV files and a list of symbols.
360
+ It will be assumed that all files are of the form
361
+ `symbol.csv`, where `symbol` is a string in the list.
362
+
363
+ Args:
364
+ events (Queue): The Event Queue.
365
+ symbol_list (List[str]): A list of symbol strings.
366
+ csv_dir (str): Absolute directory path to the CSV files.
367
+
368
+ NOTE:
369
+ All csv fille can be stored in 'Home/.bbstrader/data/csv_data'
370
+
371
+ """
372
+ csv_dir = kwargs.get("csv_dir")
373
+ csv_dir = csv_dir or BBSTRADER_DIR / "data" / "csv_data"
374
+ super().__init__(
375
+ events,
376
+ symbol_list,
377
+ str(csv_dir),
378
+ columns=kwargs.get("columns"),
379
+ index_col=kwargs.get("index_col", 0),
380
+ )
381
+
382
+
383
+ class MT5DataHandler(BaseCSVDataHandler):
384
+ """
385
+ Downloads historical data from MetaTrader 5 (MT5) and provides
386
+ an interface for accessing this data bar-by-bar, simulating
387
+ a live market feed for backtesting.
388
+
389
+ Data is downloaded from MT5, saved as CSV files, and then loaded
390
+ using the functionality inherited from `BaseCSVDataHandler`.
391
+
392
+ This class is useful when you need to get data from specific broker
393
+ for different time frames.
394
+ """
395
+
396
+ def __init__(
397
+ self, events: "Queue[MarketEvent]", symbol_list: List[str], **kwargs: Any
398
+ ) -> None:
399
+ """
400
+ Args:
401
+ events (Queue): The Event Queue for passing market events.
402
+ symbol_list (List[str]): A list of symbol strings to download data for.
403
+ **kwargs: Keyword arguments for data retrieval:
404
+ time_frame (str): MT5 time frame (e.g., 'D1' for daily).
405
+ mt5_start (datetime): Start date for historical data.
406
+ mt5_end (datetime): End date for historical data.
407
+ data_dir (str): Directory for storing data .
408
+
409
+ Note:
410
+ Requires a working connection to an MT5 terminal.
411
+ See `bbstrader.metatrader.rates.Rates` for other arguments.
412
+ See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
413
+ """
414
+ self.tf = kwargs.get("time_frame", "D1")
415
+ self.start = kwargs.get("mt5_start", datetime(2000, 1, 1))
416
+ self.end = kwargs.get("mt5_end", datetime.now())
417
+ self.use_utc = kwargs.get("use_utc", False)
418
+ self.filer = kwargs.get("filter", False)
419
+ self.fill_na = kwargs.get("fill_na", False)
420
+ self.lower_cols = kwargs.get("lower_cols", True)
421
+ self.data_dir = kwargs.get("data_dir")
422
+ self.symbol_list = symbol_list
423
+ self.kwargs = kwargs
424
+ self.kwargs["backtest"] = (
425
+ True # Ensure backtest mode is set to avoid InvalidBroker errors
426
+ )
427
+
428
+ csv_dir = self._download_and_cache_data(self.data_dir)
429
+ super().__init__(
430
+ events,
431
+ symbol_list,
432
+ str(csv_dir),
433
+ columns=kwargs.get("columns"),
434
+ index_col=kwargs.get("index_col", 0),
435
+ )
436
+
437
+ def _download_and_cache_data(self, cache_dir: Optional[str]) -> Path:
438
+ data_dir = (
439
+ Path(cache_dir) if cache_dir else BBSTRADER_DIR / "data" / "mt5" / self.tf
440
+ )
441
+ data_dir.mkdir(parents=True, exist_ok=True)
442
+ for symbol in self.symbol_list:
443
+ try:
444
+ data = download_historical_data(
445
+ symbol=symbol,
446
+ timeframe=self.tf,
447
+ date_from=self.start,
448
+ date_to=self.end,
449
+ utc=self.use_utc,
450
+ filter=self.filer,
451
+ fill_na=self.fill_na,
452
+ lower_colnames=self.lower_cols,
453
+ **self.kwargs,
454
+ )
455
+ if data is None:
456
+ raise ValueError(f"No data found for {symbol}")
457
+ data.to_csv(data_dir / f"{symbol}.csv")
458
+ except Exception as e:
459
+ raise ValueError(f"Error downloading {symbol}: {e}")
460
+ return data_dir
461
+
462
+
463
+ class YFDataHandler(BaseCSVDataHandler):
464
+ """
465
+ Downloads historical data from Yahoo Finance and provides
466
+ an interface for accessing this data bar-by-bar, simulating
467
+ a live market feed for backtesting.
468
+
469
+ Data is fetched using the `yfinance` library and optionally cached
470
+ to disk to speed up subsequent runs.
471
+
472
+ This class is useful when working with historical daily prices.
473
+ """
474
+
475
+ def __init__(
476
+ self, events: "Queue[MarketEvent]", symbol_list: List[str], **kwargs: Any
477
+ ) -> None:
478
+ """
479
+ Args:
480
+ events (Queue): The Event Queue for passing market events.
481
+ symbol_list (list[str]): List of symbols to download data for.
482
+ yf_start (str): Start date for historical data (YYYY-MM-DD).
483
+ yf_end (str): End date for historical data (YYYY-MM-DD).
484
+ data_dir (str, optional): Directory for caching data .
485
+
486
+ Note:
487
+ See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
488
+ """
489
+ self.symbol_list = symbol_list
490
+ self.start_date = kwargs.get("yf_start")
491
+ self.end_date = kwargs.get("yf_end", datetime.now())
492
+ self.cache_dir = kwargs.get("data_dir")
493
+
494
+ csv_dir = self._download_and_cache_data(self.cache_dir)
495
+
496
+ super().__init__(
497
+ events,
498
+ symbol_list,
499
+ str(csv_dir),
500
+ columns=kwargs.get("columns"),
501
+ index_col=kwargs.get("index_col", 0),
502
+ )
503
+
504
+ def _download_and_cache_data(self, cache_dir: Optional[str]) -> str:
505
+ """Downloads and caches historical data as CSV files."""
506
+ _cache_dir = cache_dir or BBSTRADER_DIR / "data" / "yfinance" / "daily"
507
+ os.makedirs(_cache_dir, exist_ok=True)
508
+ for symbol in self.symbol_list:
509
+ filepath = os.path.join(_cache_dir, f"{symbol}.csv")
510
+ try:
511
+ data = yf.download(
512
+ symbol,
513
+ start=self.start_date,
514
+ end=self.end_date,
515
+ multi_level_index=False,
516
+ auto_adjust=True,
517
+ progress=False,
518
+ )
519
+ if "Adj Close" not in data.columns:
520
+ data["Adj Close"] = data["Close"]
521
+ if data.empty:
522
+ raise ValueError(f"No data found for {symbol}")
523
+ data.to_csv(filepath)
524
+ except Exception as e:
525
+ raise ValueError(f"Error downloading {symbol}: {e}")
526
+ return str(_cache_dir)
527
+
528
+
529
+ class EODHDataHandler(BaseCSVDataHandler):
530
+ """
531
+ Downloads historical data from EOD Historical Data.
532
+ Data is fetched using the `eodhd` library.
533
+
534
+ To use this class, you need to sign up for an API key at
535
+ https://eodhistoricaldata.com/ and provide the key as an argument.
536
+ """
537
+
538
+ def __init__(
539
+ self, events: "Queue[MarketEvent]", symbol_list: List[str], **kwargs: Any
540
+ ) -> None:
541
+ """
542
+ Args:
543
+ events (Queue): The Event Queue for passing market events.
544
+ symbol_list (list[str]): List of symbols to download data for.
545
+ eodhd_start (str): Start date for historical data (YYYY-MM-DD).
546
+ eodhd_end (str): End date for historical data (YYYY-MM-DD).
547
+ data_dir (str, optional): Directory for caching data .
548
+ eodhd_period (str, optional): Time period for historical data (e.g., 'd', 'w', 'm', '1m', '5m', '1h').
549
+ eodhd_api_key (str, optional): API key for EOD Historical Data.
550
+
551
+ Note:
552
+ See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
553
+ """
554
+ self.symbol_list = symbol_list
555
+ self.start_date = kwargs.get("eodhd_start")
556
+ self.end_date = kwargs.get("eodhd_end", datetime.now().strftime("%Y-%m-%d"))
557
+ self.period = kwargs.get("eodhd_period", "d")
558
+ self.cache_dir = kwargs.get("data_dir")
559
+ self.__api_key = kwargs.get("eodhd_api_key", "demo")
560
+
561
+ csv_dir = self._download_and_cache_data(self.cache_dir)
562
+
563
+ super().__init__(
564
+ events,
565
+ symbol_list,
566
+ str(csv_dir),
567
+ columns=kwargs.get("columns"),
568
+ index_col=kwargs.get("index_col", 0),
569
+ )
570
+
571
+ def _get_data(
572
+ self, symbol: str, period: str
573
+ ) -> Union[pd.DataFrame, List[Dict[str, Any]]]:
574
+ if not self.__api_key:
575
+ raise ValueError("API key is required for EODHD data.")
576
+ client = APIClient(api_key=self.__api_key)
577
+ if period in ["d", "w", "m"]:
578
+ return client.get_historical_data(
579
+ symbol=symbol,
580
+ interval=period,
581
+ iso8601_start=self.start_date,
582
+ iso8601_end=self.end_date,
583
+ )
584
+ elif period in ["1m", "5m", "1h"]:
585
+ hms = " 00:00:00"
586
+ fmt = "%Y-%m-%d %H:%M:%S"
587
+ startdt = datetime.strptime(str(self.start_date) + hms, fmt)
588
+ enddt = datetime.strptime(str(self.end_date) + hms, fmt)
589
+ startdt = startdt.replace(tzinfo=timezone("UTC"))
590
+ enddt = enddt.replace(tzinfo=timezone("UTC"))
591
+ unix_start = int(startdt.timestamp())
592
+ unix_end = int(enddt.timestamp())
593
+ return client.get_intraday_historical_data(
594
+ symbol=symbol,
595
+ interval=period,
596
+ from_unix_time=unix_start,
597
+ to_unix_time=unix_end,
598
+ )
599
+ raise ValueError(f"Unsupported period: {period}")
600
+
601
+ def _format_data(
602
+ self, data: Union[List[Dict[str, Any]], pd.DataFrame]
603
+ ) -> pd.DataFrame:
604
+ if isinstance(data, pd.DataFrame):
605
+ if data.empty or len(data) == 0:
606
+ raise ValueError("No data found.")
607
+ df = data.drop(labels=["symbol", "interval"], axis=1)
608
+ df = df.rename(columns={"adjusted_close": "adj_close"})
609
+ return df
610
+
611
+ elif isinstance(data, list):
612
+ if not data or len(data) == 0:
613
+ raise ValueError("No data found.")
614
+ df = pd.DataFrame(data)
615
+ df = df.drop(columns=["timestamp", "gmtoffset"], axis=1)
616
+ df = df.rename(columns={"datetime": "date"})
617
+ df["adj_close"] = df["close"]
618
+ df = df[["date", "open", "high", "low", "close", "adj_close", "volume"]]
619
+ df.date = pd.to_datetime(df.date)
620
+ df = df.set_index("date")
621
+ return df
622
+ raise TypeError(f"Unsupported data type: {type(data)}")
623
+
624
+ def _download_and_cache_data(self, cache_dir: Optional[str]) -> str:
625
+ """Downloads and caches historical data as CSV files."""
626
+ _cache_dir = cache_dir or BBSTRADER_DIR / "data" / "eodhd" / self.period
627
+ os.makedirs(_cache_dir, exist_ok=True)
628
+ for symbol in self.symbol_list:
629
+ filepath = os.path.join(_cache_dir, f"{symbol}.csv")
630
+ try:
631
+ data = self._get_data(symbol, self.period)
632
+ data = self._format_data(data)
633
+ data.to_csv(filepath)
634
+ except Exception as e:
635
+ raise ValueError(f"Error downloading {symbol}: {e}")
636
+ return str(_cache_dir)
637
+
638
+
639
+ class FMPDataHandler(BaseCSVDataHandler):
640
+ """
641
+ Downloads historical data from Financial Modeling Prep (FMP).
642
+ Data is fetched using the `financetoolkit` library.
643
+
644
+ To use this class, you need to sign up for an API key at
645
+ https://financialmodelingprep.com/developer/docs/pricing and
646
+ provide the key as an argument.
647
+
648
+ """
649
+
650
+ def __init__(
651
+ self, events: "Queue[MarketEvent]", symbol_list: List[str], **kwargs: Any
652
+ ) -> None:
653
+ """
654
+ Args:
655
+ events (Queue): The Event Queue for passing market events.
656
+ symbol_list (list[str]): List of symbols to download data for.
657
+ fmp_start (str): Start date for historical data (YYYY-MM-DD).
658
+ fmp_end (str): End date for historical data (YYYY-MM-DD).
659
+ data_dir (str, optional): Directory for caching data .
660
+ fmp_period (str, optional): Time period for historical data
661
+ (e.g. daily, weekly, monthly, quarterly, yearly, "1min", "5min", "15min", "30min", "1hour").
662
+ fmp_api_key (str): API key for Financial Modeling Prep.
663
+
664
+ Note:
665
+ See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
666
+ """
667
+ self.symbol_list = symbol_list
668
+ self.start_date = kwargs.get("fmp_start")
669
+ self.end_date = kwargs.get("fmp_end", datetime.now().strftime("%Y-%m-%d"))
670
+ self.period = kwargs.get("fmp_period", "daily")
671
+ self.cache_dir = kwargs.get("data_dir")
672
+ self.__api_key = kwargs.get("fmp_api_key")
673
+
674
+ csv_dir = self._download_and_cache_data(self.cache_dir)
675
+
676
+ super().__init__(
677
+ events,
678
+ symbol_list,
679
+ str(csv_dir),
680
+ columns=kwargs.get("columns"),
681
+ index_col=kwargs.get("index_col", 0),
682
+ )
683
+
684
+ def _get_data(self, symbol: str, period: str) -> pd.DataFrame:
685
+ if not self.__api_key:
686
+ raise ValueError("API key is required for FMP data.")
687
+ toolkit = Toolkit(
688
+ symbol,
689
+ api_key=self.__api_key,
690
+ start_date=self.start_date,
691
+ end_date=self.end_date,
692
+ benchmark_ticker=None,
693
+ progress_bar=False,
694
+ )
695
+ if period in ["daily", "weekly", "monthly", "quarterly", "yearly"]:
696
+ return toolkit.get_historical_data(period=period, progress_bar=False)
697
+ elif period in ["1min", "5min", "15min", "30min", "1hour"]:
698
+ return toolkit.get_intraday_data(period=period, progress_bar=False)
699
+ raise ValueError(f"Unsupported period: {period}")
700
+
701
+ def _format_data(self, data: pd.DataFrame, period: str) -> pd.DataFrame:
702
+ if data.empty or len(data) == 0:
703
+ raise ValueError("No data found.")
704
+ if period[0].isnumeric():
705
+ data = data.drop(columns=["Return", "Volatility", "Cumulative Return"])
706
+ else:
707
+ data = data.drop(
708
+ columns=[
709
+ "Dividends",
710
+ "Return",
711
+ "Volatility",
712
+ "Excess Return",
713
+ "Excess Volatility",
714
+ "Cumulative Return",
715
+ ]
716
+ )
717
+ data = data.reset_index()
718
+ if "Adj Close" not in data.columns:
719
+ data["Adj Close"] = data["Close"]
720
+ data["date"] = data["date"].dt.to_timestamp() # type: ignore
721
+ data["date"] = pd.to_datetime(data["date"])
722
+ data.set_index("date", inplace=True)
723
+ return data
724
+
725
+ def _download_and_cache_data(self, cache_dir: Optional[str]) -> str:
726
+ """Downloads and caches historical data as CSV files."""
727
+ _cache_dir = cache_dir or BBSTRADER_DIR / "data" / "fmp" / self.period
728
+ os.makedirs(_cache_dir, exist_ok=True)
729
+ for symbol in self.symbol_list:
730
+ filepath = os.path.join(_cache_dir, f"{symbol}.csv")
731
+ try:
732
+ data = self._get_data(symbol, self.period)
733
+ data = self._format_data(data, self.period)
734
+ data.to_csv(filepath)
735
+ except Exception as e:
736
+ raise ValueError(f"Error downloading {symbol}: {e}")
737
+ return str(_cache_dir)