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