bbstrader 0.1.92__tar.gz → 0.1.94__tar.gz
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-0.1.92/bbstrader.egg-info → bbstrader-0.1.94}/PKG-INFO +3 -1
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/btengine/backtest.py +20 -14
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/btengine/data.py +230 -39
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/btengine/execution.py +3 -2
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/btengine/performance.py +2 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/btengine/portfolio.py +12 -16
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/btengine/strategy.py +140 -45
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/metatrader/account.py +67 -13
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/metatrader/rates.py +22 -18
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/metatrader/risk.py +34 -6
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/metatrader/trade.py +14 -9
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/models/__init__.py +5 -1
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/models/optimization.py +12 -5
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/models/portfolios.py +3 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/models/risk.py +9 -1
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/trading/execution.py +38 -15
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/trading/strategies.py +4 -4
- {bbstrader-0.1.92 → bbstrader-0.1.94/bbstrader.egg-info}/PKG-INFO +3 -1
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader.egg-info/requires.txt +2 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/requirements.txt +3 -1
- {bbstrader-0.1.92 → bbstrader-0.1.94}/setup.py +1 -1
- {bbstrader-0.1.92 → bbstrader-0.1.94}/LICENSE +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/MANIFEST.in +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/README.md +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/__ini__.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/btengine/__init__.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/btengine/event.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/config.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/metatrader/__init__.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/metatrader/utils.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/models/factors.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/models/ml.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/trading/__init__.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/trading/scripts.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader/tseries.py +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader.egg-info/SOURCES.txt +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader.egg-info/dependency_links.txt +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/bbstrader.egg-info/top_level.txt +0 -0
- {bbstrader-0.1.92 → bbstrader-0.1.94}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bbstrader
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.94
|
|
4
4
|
Summary: Simplified Investment & Trading Toolkit
|
|
5
5
|
Home-page: https://github.com/bbalouki/bbstrader
|
|
6
6
|
Download-URL: https://pypi.org/project/bbstrader/
|
|
@@ -47,6 +47,8 @@ Requires-Dist: scikit-learn
|
|
|
47
47
|
Requires-Dist: notify-py
|
|
48
48
|
Requires-Dist: python-telegram-bot
|
|
49
49
|
Requires-Dist: pyportfolioopt
|
|
50
|
+
Requires-Dist: eodhd
|
|
51
|
+
Requires-Dist: financetoolkit
|
|
50
52
|
Provides-Extra: mt5
|
|
51
53
|
Requires-Dist: MetaTrader5; extra == "mt5"
|
|
52
54
|
|
|
@@ -85,6 +85,8 @@ class BacktestEngine(Backtest):
|
|
|
85
85
|
strategy (Strategy): Generates signals based on market data.
|
|
86
86
|
kwargs : Additional parameters based on the `ExecutionHandler`,
|
|
87
87
|
the `DataHandler`, the `Strategy` used and the `Portfolio`.
|
|
88
|
+
- show_equity (bool): Show the equity curve of the portfolio.
|
|
89
|
+
- stats_file (str): File to save the summary stats.
|
|
88
90
|
"""
|
|
89
91
|
self.symbol_list = symbol_list
|
|
90
92
|
self.initial_capital = initial_capital
|
|
@@ -104,6 +106,7 @@ class BacktestEngine(Backtest):
|
|
|
104
106
|
|
|
105
107
|
self._generate_trading_instances()
|
|
106
108
|
self.show_equity = kwargs.get("show_equity", False)
|
|
109
|
+
self.stats_file = kwargs.get("stats_file", None)
|
|
107
110
|
|
|
108
111
|
def _generate_trading_instances(self):
|
|
109
112
|
"""
|
|
@@ -137,14 +140,21 @@ class BacktestEngine(Backtest):
|
|
|
137
140
|
i = 0
|
|
138
141
|
while True:
|
|
139
142
|
i += 1
|
|
140
|
-
|
|
143
|
+
value = self.portfolio.all_holdings[-1]['Total']
|
|
141
144
|
if self.data_handler.continue_backtest == True:
|
|
145
|
+
# Update the market bars
|
|
142
146
|
self.data_handler.update_bars()
|
|
143
147
|
self.strategy.check_pending_orders()
|
|
148
|
+
self.strategy.get_update_from_portfolio(
|
|
149
|
+
self.portfolio.current_positions,
|
|
150
|
+
self.portfolio.current_holdings
|
|
151
|
+
)
|
|
152
|
+
self.strategy.cash = value
|
|
144
153
|
else:
|
|
145
154
|
print("\n[======= BACKTEST COMPLETED =======]")
|
|
146
|
-
print(f"END DATE: {self.data_handler.get_latest_bar_datetime()}")
|
|
155
|
+
print(f"END DATE: {self.data_handler.get_latest_bar_datetime(self.symbol_list[0])}")
|
|
147
156
|
print(f"TOTAL BARS: {i} ")
|
|
157
|
+
print(f"PORFOLIO VALUE: {round(value, 2)}")
|
|
148
158
|
break
|
|
149
159
|
|
|
150
160
|
# Handle the events
|
|
@@ -171,10 +181,6 @@ class BacktestEngine(Backtest):
|
|
|
171
181
|
self.fills += 1
|
|
172
182
|
self.portfolio.update_fill(event)
|
|
173
183
|
self.strategy.update_trades_from_fill(event)
|
|
174
|
-
self.strategy.get_update_from_portfolio(
|
|
175
|
-
self.portfolio.current_positions,
|
|
176
|
-
self.portfolio.current_holdings
|
|
177
|
-
)
|
|
178
184
|
|
|
179
185
|
time.sleep(self.heartbeat)
|
|
180
186
|
|
|
@@ -192,13 +198,13 @@ class BacktestEngine(Backtest):
|
|
|
192
198
|
stat2['Orders'] = self.orders
|
|
193
199
|
stat2['Fills'] = self.fills
|
|
194
200
|
stats.extend(stat2.items())
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
tab_stats = tabulate(stats, headers=["Metric", "Value"], tablefmt="outline")
|
|
202
|
+
print(tab_stats, "\n")
|
|
203
|
+
if self.stats_file:
|
|
204
|
+
with open(self.stats_file, 'a') as f:
|
|
205
|
+
f.write("\n[======= Summary Stats =======]\n")
|
|
206
|
+
f.write(tab_stats)
|
|
207
|
+
f.write("\n")
|
|
202
208
|
|
|
203
209
|
if self.show_equity:
|
|
204
210
|
print("\nCreating equity curve...")
|
|
@@ -336,7 +342,7 @@ def run_backtest_with(engine: Literal["bbstrader", "cerebro", "zipline"], **kwar
|
|
|
336
342
|
data_handler=kwargs.get("data_handler"),
|
|
337
343
|
strategy=kwargs.get("strategy"),
|
|
338
344
|
exc_handler=kwargs.get("exc_handler"),
|
|
339
|
-
initial_capital=kwargs.get("initial_capital"),
|
|
345
|
+
initial_capital=kwargs.get("initial_capital", 100000.0),
|
|
340
346
|
heartbeat=kwargs.get("heartbeat", 0.0),
|
|
341
347
|
**kwargs
|
|
342
348
|
)
|
|
@@ -10,13 +10,18 @@ from bbstrader.metatrader.rates import download_historical_data
|
|
|
10
10
|
from bbstrader.btengine.event import MarketEvent
|
|
11
11
|
from bbstrader.config import BBSTRADER_DIR
|
|
12
12
|
from datetime import datetime
|
|
13
|
+
from eodhd import APIClient
|
|
14
|
+
from financetoolkit import Toolkit
|
|
15
|
+
from pytz import timezone
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
__all__ = [
|
|
16
19
|
"DataHandler",
|
|
17
20
|
"CSVDataHandler",
|
|
18
21
|
"MT5DataHandler",
|
|
19
|
-
"YFDataHandler"
|
|
22
|
+
"YFDataHandler",
|
|
23
|
+
"EODHDataHandler",
|
|
24
|
+
"FMPDataHandler",
|
|
20
25
|
]
|
|
21
26
|
|
|
22
27
|
|
|
@@ -167,7 +172,7 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
167
172
|
os.path.join(self.csv_dir, f'{self.symbol_list[0]}.csv')
|
|
168
173
|
).columns.to_list()
|
|
169
174
|
new_names = self.columns or default_names
|
|
170
|
-
new_names = [name.lower().replace(' ', '_') for name in new_names]
|
|
175
|
+
new_names = [name.strip().lower().replace(' ', '_') for name in new_names]
|
|
171
176
|
self.columns = new_names
|
|
172
177
|
assert 'adj_close' in new_names or 'close' in new_names, \
|
|
173
178
|
"Column names must contain 'Adj Close' and 'Close' or adj_close and close"
|
|
@@ -340,7 +345,13 @@ class CSVDataHandler(BaseCSVDataHandler):
|
|
|
340
345
|
"""
|
|
341
346
|
csv_dir = kwargs.get("csv_dir")
|
|
342
347
|
csv_dir = csv_dir or BBSTRADER_DIR / 'csv_data'
|
|
343
|
-
super().__init__(
|
|
348
|
+
super().__init__(
|
|
349
|
+
events,
|
|
350
|
+
symbol_list,
|
|
351
|
+
csv_dir,
|
|
352
|
+
columns =kwargs.get('columns'),
|
|
353
|
+
index_col=kwargs.get('index_col', 0)
|
|
354
|
+
)
|
|
344
355
|
|
|
345
356
|
|
|
346
357
|
class MT5DataHandler(BaseCSVDataHandler):
|
|
@@ -372,32 +383,41 @@ class MT5DataHandler(BaseCSVDataHandler):
|
|
|
372
383
|
See `bbstrader.metatrader.rates.Rates` for other arguments.
|
|
373
384
|
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
374
385
|
"""
|
|
375
|
-
self.tf
|
|
376
|
-
self.start
|
|
377
|
-
self.end
|
|
378
|
-
self.use_utc
|
|
379
|
-
self.filer
|
|
380
|
-
self.fill_na
|
|
386
|
+
self.tf = kwargs.get('time_frame', 'D1')
|
|
387
|
+
self.start = kwargs.get('mt5_start', datetime(2000, 1, 1))
|
|
388
|
+
self.end = kwargs.get('mt5_end', datetime.now())
|
|
389
|
+
self.use_utc = kwargs.get('use_utc', False)
|
|
390
|
+
self.filer = kwargs.get('filter', False)
|
|
391
|
+
self.fill_na = kwargs.get('fill_na', False)
|
|
381
392
|
self.lower_cols = kwargs.get('lower_cols', True)
|
|
382
|
-
self.data_dir
|
|
393
|
+
self.data_dir = kwargs.get('data_dir')
|
|
383
394
|
self.symbol_list = symbol_list
|
|
384
|
-
|
|
385
|
-
|
|
395
|
+
self.kwargs = kwargs
|
|
396
|
+
|
|
397
|
+
csv_dir = self._download_and_cache_data(self.data_dir)
|
|
398
|
+
super().__init__(
|
|
399
|
+
events,
|
|
400
|
+
symbol_list,
|
|
401
|
+
csv_dir,
|
|
402
|
+
columns =kwargs.get('columns'),
|
|
403
|
+
index_col=kwargs.get('index_col', 0)
|
|
404
|
+
)
|
|
386
405
|
|
|
387
|
-
def
|
|
388
|
-
data_dir = cache_dir or BBSTRADER_DIR / '
|
|
406
|
+
def _download_and_cache_data(self, cache_dir: str):
|
|
407
|
+
data_dir = cache_dir or BBSTRADER_DIR / 'mt5' / self.tf
|
|
389
408
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
390
409
|
for symbol in self.symbol_list:
|
|
391
410
|
try:
|
|
392
411
|
data = download_historical_data(
|
|
393
412
|
symbol=symbol,
|
|
394
|
-
|
|
413
|
+
timeframe=self.tf,
|
|
395
414
|
date_from=self.start,
|
|
396
|
-
date_to=self.end,
|
|
415
|
+
date_to=self.end,
|
|
397
416
|
utc=self.use_utc,
|
|
398
417
|
filter=self.filer,
|
|
399
418
|
fill_na=self.fill_na,
|
|
400
|
-
lower_colnames=self.lower_cols
|
|
419
|
+
lower_colnames=self.lower_cols,
|
|
420
|
+
**self.kwargs
|
|
401
421
|
)
|
|
402
422
|
if data is None:
|
|
403
423
|
raise ValueError(f"No data found for {symbol}")
|
|
@@ -432,15 +452,23 @@ class YFDataHandler(BaseCSVDataHandler):
|
|
|
432
452
|
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
433
453
|
"""
|
|
434
454
|
self.symbol_list = symbol_list
|
|
435
|
-
self.start_date
|
|
436
|
-
self.end_date
|
|
437
|
-
self.cache_dir
|
|
455
|
+
self.start_date = kwargs.get('yf_start')
|
|
456
|
+
self.end_date = kwargs.get('yf_end', datetime.now())
|
|
457
|
+
self.cache_dir = kwargs.get('data_dir')
|
|
458
|
+
|
|
438
459
|
csv_dir = self._download_and_cache_data(self.cache_dir)
|
|
439
|
-
|
|
460
|
+
|
|
461
|
+
super().__init__(
|
|
462
|
+
events,
|
|
463
|
+
symbol_list,
|
|
464
|
+
csv_dir,
|
|
465
|
+
columns =kwargs.get('columns'),
|
|
466
|
+
index_col=kwargs.get('index_col', 0)
|
|
467
|
+
)
|
|
440
468
|
|
|
441
469
|
def _download_and_cache_data(self, cache_dir: str):
|
|
442
470
|
"""Downloads and caches historical data as CSV files."""
|
|
443
|
-
cache_dir = cache_dir or BBSTRADER_DIR / '
|
|
471
|
+
cache_dir = cache_dir or BBSTRADER_DIR / 'yfinance' / 'daily'
|
|
444
472
|
os.makedirs(cache_dir, exist_ok=True)
|
|
445
473
|
for symbol in self.symbol_list:
|
|
446
474
|
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
@@ -449,39 +477,202 @@ class YFDataHandler(BaseCSVDataHandler):
|
|
|
449
477
|
symbol, start=self.start_date, end=self.end_date, multi_level_index=False)
|
|
450
478
|
if data.empty:
|
|
451
479
|
raise ValueError(f"No data found for {symbol}")
|
|
452
|
-
data.to_csv(filepath)
|
|
480
|
+
data.to_csv(filepath)
|
|
453
481
|
except Exception as e:
|
|
454
482
|
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
455
483
|
return cache_dir
|
|
456
484
|
|
|
457
485
|
|
|
458
|
-
# TODO # Get data from EODHD
|
|
459
|
-
# https://eodhd.com/
|
|
460
486
|
class EODHDataHandler(BaseCSVDataHandler):
|
|
461
|
-
|
|
487
|
+
"""
|
|
488
|
+
Downloads historical data from EOD Historical Data.
|
|
489
|
+
Data is fetched using the `eodhd` library.
|
|
462
490
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
491
|
+
To use this class, you need to sign up for an API key at
|
|
492
|
+
https://eodhistoricaldata.com/ and provide the key as an argument.
|
|
493
|
+
"""
|
|
494
|
+
def __init__(self, events: Queue, symbol_list: List[str], **kwargs):
|
|
495
|
+
"""
|
|
496
|
+
Args:
|
|
497
|
+
events (Queue): The Event Queue for passing market events.
|
|
498
|
+
symbol_list (list[str]): List of symbols to download data for.
|
|
499
|
+
eodhd_start (str): Start date for historical data (YYYY-MM-DD).
|
|
500
|
+
eodhd_end (str): End date for historical data (YYYY-MM-DD).
|
|
501
|
+
data_dir (str, optional): Directory for caching data .
|
|
502
|
+
eodhd_period (str, optional): Time period for historical data (e.g., 'd', 'w', 'm', '1m', '5m', '1h').
|
|
503
|
+
eodhd_api_key (str, optional): API key for EOD Historical Data.
|
|
467
504
|
|
|
505
|
+
Note:
|
|
506
|
+
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
507
|
+
"""
|
|
508
|
+
self.symbol_list = symbol_list
|
|
509
|
+
self.start_date = kwargs.get('eodhd_start')
|
|
510
|
+
self.end_date = kwargs.get('eodhd_end', datetime.now().strftime('%Y-%m-%d'))
|
|
511
|
+
self.period = kwargs.get('eodhd_period', 'd')
|
|
512
|
+
self.cache_dir = kwargs.get('data_dir')
|
|
513
|
+
self.__api_key = kwargs.get('eodhd_api_key', 'demo')
|
|
468
514
|
|
|
469
|
-
|
|
470
|
-
|
|
515
|
+
csv_dir = self._download_and_cache_data(self.cache_dir)
|
|
516
|
+
|
|
517
|
+
super().__init__(
|
|
518
|
+
events,
|
|
519
|
+
symbol_list,
|
|
520
|
+
csv_dir,
|
|
521
|
+
columns =kwargs.get('columns'),
|
|
522
|
+
index_col=kwargs.get('index_col', 0)
|
|
523
|
+
)
|
|
471
524
|
|
|
525
|
+
def _get_data(self, symbol: str, period) -> pd.DataFrame | List[Dict]:
|
|
526
|
+
if not self.__api_key:
|
|
527
|
+
raise ValueError("API key is required for EODHD data.")
|
|
528
|
+
client = APIClient(api_key=self.__api_key)
|
|
529
|
+
if period in ['d', 'w', 'm']:
|
|
530
|
+
return client.get_historical_data(
|
|
531
|
+
symbol=symbol,
|
|
532
|
+
interval=period,
|
|
533
|
+
iso8601_start=self.start_date,
|
|
534
|
+
iso8601_end=self.end_date,
|
|
535
|
+
)
|
|
536
|
+
elif period in ['1m', '5m', '1h']:
|
|
537
|
+
hms = ' 00:00:00'
|
|
538
|
+
fmt = '%Y-%m-%d %H:%M:%S'
|
|
539
|
+
startdt = datetime.strptime(self.start_date + hms, fmt)
|
|
540
|
+
enddt = datetime.strptime(self.end_date + hms, fmt)
|
|
541
|
+
startdt = startdt.replace(tzinfo=timezone('UTC'))
|
|
542
|
+
enddt = enddt.replace(tzinfo=timezone('UTC'))
|
|
543
|
+
unix_start = int(startdt.timestamp())
|
|
544
|
+
unix_end = int(enddt.timestamp())
|
|
545
|
+
return client.get_intraday_historical_data(
|
|
546
|
+
symbol=symbol,
|
|
547
|
+
interval=period,
|
|
548
|
+
from_unix_time=unix_start,
|
|
549
|
+
to_unix_time=unix_end,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def _forma_data(self, data: List[Dict] | pd.DataFrame) -> pd.DataFrame:
|
|
553
|
+
if isinstance(data, pd.DataFrame):
|
|
554
|
+
if data.empty or len(data) == 0:
|
|
555
|
+
raise ValueError("No data found.")
|
|
556
|
+
df = data.drop(labels=['symbol', 'interval'], axis=1)
|
|
557
|
+
df = df.rename(columns={'adjusted_close': 'adj_close'})
|
|
558
|
+
return df
|
|
559
|
+
|
|
560
|
+
elif isinstance(data, list):
|
|
561
|
+
if not data or len(data) == 0:
|
|
562
|
+
raise ValueError("No data found.")
|
|
563
|
+
df = pd.DataFrame(data)
|
|
564
|
+
df = df.drop(columns=['timestamp', 'gmtoffset'], axis=1)
|
|
565
|
+
df = df.rename(columns={'datetime': 'date'})
|
|
566
|
+
df['adj_close'] = df['close']
|
|
567
|
+
df = df[['date', 'open', 'high', 'low', 'close', 'adj_close', 'volume']]
|
|
568
|
+
df.date = pd.to_datetime(df.date)
|
|
569
|
+
df = df.set_index('date')
|
|
570
|
+
return df
|
|
571
|
+
|
|
572
|
+
def _download_and_cache_data(self, cache_dir: str):
|
|
573
|
+
"""Downloads and caches historical data as CSV files."""
|
|
574
|
+
cache_dir = cache_dir or BBSTRADER_DIR / 'eodhd' / self.period
|
|
575
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
576
|
+
for symbol in self.symbol_list:
|
|
577
|
+
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
578
|
+
try:
|
|
579
|
+
data = self._get_data(symbol, self.period)
|
|
580
|
+
data = self._forma_data(data)
|
|
581
|
+
data.to_csv(filepath)
|
|
582
|
+
except Exception as e:
|
|
583
|
+
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
584
|
+
return cache_dir
|
|
472
585
|
|
|
473
|
-
|
|
586
|
+
|
|
587
|
+
class FMPDataHandler(BaseCSVDataHandler):
|
|
474
588
|
"""
|
|
475
|
-
|
|
476
|
-
|
|
589
|
+
Downloads historical data from Financial Modeling Prep (FMP).
|
|
590
|
+
Data is fetched using the `financetoolkit` library.
|
|
591
|
+
|
|
592
|
+
To use this class, you need to sign up for an API key at
|
|
593
|
+
https://financialmodelingprep.com/developer/docs/pricing and
|
|
594
|
+
provide the key as an argument.
|
|
595
|
+
|
|
477
596
|
"""
|
|
478
|
-
|
|
597
|
+
def __init__(self, events: Queue, symbol_list: List[str], **kwargs):
|
|
598
|
+
"""
|
|
599
|
+
Args:
|
|
600
|
+
events (Queue): The Event Queue for passing market events.
|
|
601
|
+
symbol_list (list[str]): List of symbols to download data for.
|
|
602
|
+
fmp_start (str): Start date for historical data (YYYY-MM-DD).
|
|
603
|
+
fmp_end (str): End date for historical data (YYYY-MM-DD).
|
|
604
|
+
data_dir (str, optional): Directory for caching data .
|
|
605
|
+
fmp_period (str, optional): Time period for historical data
|
|
606
|
+
(e.g. daily, weekly, monthly, quarterly, yearly, "1min", "5min", "15min", "30min", "1hour").
|
|
607
|
+
fmp_api_key (str): API key for Financial Modeling Prep.
|
|
479
608
|
|
|
609
|
+
Note:
|
|
610
|
+
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
611
|
+
"""
|
|
612
|
+
self.symbol_list = symbol_list
|
|
613
|
+
self.start_date = kwargs.get('fmp_start')
|
|
614
|
+
self.end_date = kwargs.get('fmp_end', datetime.now().strftime('%Y-%m-%d'))
|
|
615
|
+
self.period = kwargs.get('fmp_period', 'daily')
|
|
616
|
+
self.cache_dir = kwargs.get('data_dir')
|
|
617
|
+
self.__api_key = kwargs.get('fmp_api_key')
|
|
480
618
|
|
|
481
|
-
|
|
482
|
-
|
|
619
|
+
csv_dir = self._download_and_cache_data(self.cache_dir)
|
|
620
|
+
|
|
621
|
+
super().__init__(
|
|
622
|
+
events,
|
|
623
|
+
symbol_list,
|
|
624
|
+
csv_dir,
|
|
625
|
+
columns =kwargs.get('columns'),
|
|
626
|
+
index_col=kwargs.get('index_col', 0)
|
|
627
|
+
)
|
|
483
628
|
|
|
484
|
-
|
|
629
|
+
def _get_data(self, symbol: str, period: str) -> pd.DataFrame:
|
|
630
|
+
if not self.__api_key:
|
|
631
|
+
raise ValueError("API key is required for FMP data.")
|
|
632
|
+
toolkit = Toolkit(
|
|
633
|
+
symbol,
|
|
634
|
+
api_key=self.__api_key,
|
|
635
|
+
start_date=self.start_date,
|
|
636
|
+
end_date=self.end_date,
|
|
637
|
+
benchmark_ticker=None
|
|
638
|
+
)
|
|
639
|
+
if period in ['daily', 'weekly', 'monthly', 'quarterly', 'yearly']:
|
|
640
|
+
return toolkit.get_historical_data(period=period)
|
|
641
|
+
elif period in ['1min', '5min', '15min', '30min', '1hour']:
|
|
642
|
+
return toolkit.get_intraday_data(period=period)
|
|
643
|
+
|
|
644
|
+
def _format_data(self, data: pd.DataFrame, period: str) -> pd.DataFrame:
|
|
645
|
+
if data.empty or len(data) == 0:
|
|
646
|
+
raise ValueError("No data found.")
|
|
647
|
+
if period[0].isnumeric():
|
|
648
|
+
data = data.drop(columns=['Return', 'Volatility', 'Cumulative Return'], axis=1)
|
|
649
|
+
else:
|
|
650
|
+
data = data.drop(columns=['Dividends', 'Return', 'Volatility',
|
|
651
|
+
'Excess Return', 'Excess Volatility',
|
|
652
|
+
'Cumulative Return'], axis=1)
|
|
653
|
+
data = data.reset_index()
|
|
654
|
+
if 'Adj Close' not in data.columns:
|
|
655
|
+
data['Adj Close'] = data['Close']
|
|
656
|
+
data['date'] = data['date'].dt.to_timestamp()
|
|
657
|
+
data['date'] = pd.to_datetime(data['date'])
|
|
658
|
+
data.set_index('date', inplace=True)
|
|
659
|
+
return data
|
|
660
|
+
|
|
661
|
+
def _download_and_cache_data(self, cache_dir: str):
|
|
662
|
+
"""Downloads and caches historical data as CSV files."""
|
|
663
|
+
cache_dir = cache_dir or BBSTRADER_DIR / 'fmp' / self.period
|
|
664
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
665
|
+
for symbol in self.symbol_list:
|
|
666
|
+
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
667
|
+
try:
|
|
668
|
+
data = self._get_data(symbol, self.period)
|
|
669
|
+
data = self._format_data(data, self.period)
|
|
670
|
+
data.to_csv(filepath)
|
|
671
|
+
except Exception as e:
|
|
672
|
+
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
673
|
+
return cache_dir
|
|
485
674
|
|
|
486
675
|
|
|
487
676
|
# TODO Add data Handlers for Interactive Brokers
|
|
677
|
+
class TWSDataHandler(BaseCSVDataHandler):
|
|
678
|
+
...
|
|
@@ -77,8 +77,9 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
77
77
|
if event.type == 'ORDER':
|
|
78
78
|
dtime = self.bardata.get_latest_bar_datetime(event.symbol)
|
|
79
79
|
fill_event = FillEvent(
|
|
80
|
-
dtime, event.symbol,
|
|
81
|
-
'ARCA', event.quantity, event.direction,
|
|
80
|
+
timeindex=dtime, symbol=event.symbol,
|
|
81
|
+
exchange='ARCA', quantity=event.quantity, direction=event.direction,
|
|
82
|
+
fill_cost=None, commission=None, order=event.signal
|
|
82
83
|
)
|
|
83
84
|
self.events.put(fill_event)
|
|
84
85
|
self.logger.info(
|
|
@@ -93,16 +93,20 @@ class Portfolio(object):
|
|
|
93
93
|
initial_capital (float): The starting capital in USD.
|
|
94
94
|
|
|
95
95
|
kwargs (dict): Additional arguments
|
|
96
|
+
- `leverage`: The leverage to apply to the portfolio.
|
|
96
97
|
- `time_frame`: The time frame of the bars.
|
|
97
|
-
- `
|
|
98
|
+
- `session_duration`: The number of trading hours in a day.
|
|
98
99
|
- `benchmark`: The benchmark symbol to compare the portfolio.
|
|
99
|
-
- `
|
|
100
|
+
- `output_dir`: The directory to save the backtest results.
|
|
101
|
+
- `strategy_name`: The name of the strategy (the name must not include 'Strategy' in it).
|
|
102
|
+
- `print_stats`: Whether to print the backtest statistics.
|
|
100
103
|
"""
|
|
101
104
|
self.bars = bars
|
|
102
105
|
self.events = events
|
|
103
106
|
self.symbol_list = self.bars.symbol_list
|
|
104
107
|
self.start_date = start_date
|
|
105
108
|
self.initial_capital = initial_capital
|
|
109
|
+
self._leverage = kwargs.get('leverage', 1)
|
|
106
110
|
|
|
107
111
|
self.timeframe = kwargs.get("time_frame", "D1")
|
|
108
112
|
self.trading_hours = kwargs.get("session_duration", 23)
|
|
@@ -277,8 +281,7 @@ class Portfolio(object):
|
|
|
277
281
|
|
|
278
282
|
def generate_order(self, signal: SignalEvent):
|
|
279
283
|
"""
|
|
280
|
-
|
|
281
|
-
and generate an OrderEvent, else return None.
|
|
284
|
+
Turns a SignalEvent into an OrderEvent.
|
|
282
285
|
|
|
283
286
|
Args:
|
|
284
287
|
signal (SignalEvent): The tuple containing Signal information.
|
|
@@ -294,25 +297,17 @@ class Portfolio(object):
|
|
|
294
297
|
strength = signal.strength
|
|
295
298
|
price = signal.price or self._get_price(symbol)
|
|
296
299
|
cur_quantity = self.current_positions[symbol]
|
|
297
|
-
|
|
300
|
+
mkt_quantity = round(quantity * strength, 2)
|
|
301
|
+
new_quantity = mkt_quantity * self._leverage
|
|
298
302
|
|
|
299
303
|
if direction in ['LONG', 'SHORT', 'EXIT']:
|
|
300
304
|
order_type = 'MKT'
|
|
301
305
|
else:
|
|
302
306
|
order_type = direction
|
|
303
|
-
mkt_quantity = round(quantity * strength)
|
|
304
|
-
cost = mkt_quantity * price
|
|
305
307
|
|
|
306
|
-
if
|
|
307
|
-
new_quantity = mkt_quantity
|
|
308
|
-
elif cash < cost and cash > 0:
|
|
309
|
-
new_quantity = round(cash // price)
|
|
310
|
-
else:
|
|
311
|
-
new_quantity = 0
|
|
312
|
-
|
|
313
|
-
if new_quantity > 0 and direction == 'LONG':
|
|
308
|
+
if direction == 'LONG' and new_quantity > 0:
|
|
314
309
|
order = OrderEvent(symbol, order_type, new_quantity, 'BUY', price, direction)
|
|
315
|
-
if
|
|
310
|
+
if direction == 'SHORT' and new_quantity > 0:
|
|
316
311
|
order = OrderEvent(symbol, order_type, new_quantity, 'SELL', price, direction)
|
|
317
312
|
|
|
318
313
|
if direction == 'EXIT' and cur_quantity > 0:
|
|
@@ -322,6 +317,7 @@ class Portfolio(object):
|
|
|
322
317
|
|
|
323
318
|
return order
|
|
324
319
|
|
|
320
|
+
|
|
325
321
|
def update_signal(self, event: SignalEvent):
|
|
326
322
|
"""
|
|
327
323
|
Acts on a SignalEvent to generate new orders
|