bbstrader 0.1.93__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bbstrader might be problematic. Click here for more details.
- bbstrader/__ini__.py +2 -2
- bbstrader/btengine/data.py +241 -40
- bbstrader/btengine/strategy.py +12 -8
- bbstrader/config.py +4 -0
- bbstrader/core/__init__.py +0 -0
- bbstrader/core/data.py +23 -0
- bbstrader/core/utils.py +0 -0
- bbstrader/ibkr/__init__.py +0 -0
- bbstrader/metatrader/account.py +66 -12
- bbstrader/metatrader/rates.py +24 -20
- bbstrader/metatrader/risk.py +6 -3
- bbstrader/metatrader/trade.py +31 -13
- bbstrader/models/__init__.py +1 -1
- bbstrader/models/factors.py +275 -0
- bbstrader/models/ml.py +1026 -0
- bbstrader/models/optimization.py +17 -16
- bbstrader/models/{portfolios.py → portfolio.py} +20 -11
- bbstrader/models/risk.py +10 -2
- bbstrader/trading/execution.py +67 -35
- bbstrader/trading/strategies.py +5 -5
- bbstrader/tseries.py +412 -63
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/METADATA +9 -3
- bbstrader-0.2.0.dist-info/RECORD +36 -0
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/WHEEL +1 -1
- bbstrader-0.1.93.dist-info/RECORD +0 -32
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.93.dist-info → bbstrader-0.2.0.dist-info}/top_level.txt +0 -0
bbstrader/__ini__.py
CHANGED
|
@@ -5,9 +5,9 @@ Simplified Investment & Trading Toolkit
|
|
|
5
5
|
"""
|
|
6
6
|
__author__ = "Bertin Balouki SIMYELI"
|
|
7
7
|
__copyright__ = "2023-2024 Bertin Balouki SIMYELI"
|
|
8
|
-
__email__ = "
|
|
8
|
+
__email__ = "bertin@bbstrader.com"
|
|
9
9
|
__license__ = "MIT"
|
|
10
|
-
__version__ = '0.
|
|
10
|
+
__version__ = '0.2.0'
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
from bbstrader import btengine
|
bbstrader/btengine/data.py
CHANGED
|
@@ -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
|
|
|
@@ -148,12 +153,19 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
148
153
|
@property
|
|
149
154
|
def symbols(self)-> List[str]:
|
|
150
155
|
return self.symbol_list
|
|
156
|
+
|
|
151
157
|
@property
|
|
152
158
|
def data(self)-> Dict[str, pd.DataFrame]:
|
|
153
159
|
return self.symbol_data
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def datadir(self)-> str:
|
|
163
|
+
return self.csv_dir
|
|
164
|
+
|
|
154
165
|
@property
|
|
155
166
|
def labels(self)-> List[str]:
|
|
156
167
|
return self.columns
|
|
168
|
+
|
|
157
169
|
@property
|
|
158
170
|
def index(self)-> str | List[str]:
|
|
159
171
|
return self._index
|
|
@@ -167,7 +179,7 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
167
179
|
os.path.join(self.csv_dir, f'{self.symbol_list[0]}.csv')
|
|
168
180
|
).columns.to_list()
|
|
169
181
|
new_names = self.columns or default_names
|
|
170
|
-
new_names = [name.lower().replace(' ', '_') for name in new_names]
|
|
182
|
+
new_names = [name.strip().lower().replace(' ', '_') for name in new_names]
|
|
171
183
|
self.columns = new_names
|
|
172
184
|
assert 'adj_close' in new_names or 'close' in new_names, \
|
|
173
185
|
"Column names must contain 'Adj Close' and 'Close' or adj_close and close"
|
|
@@ -198,6 +210,7 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
198
210
|
'adj_close' if 'adj_close' in new_names else 'close'
|
|
199
211
|
].pct_change().dropna()
|
|
200
212
|
self._index = self.symbol_data[s].index.name
|
|
213
|
+
self.symbol_data[s].to_csv(os.path.join(self.csv_dir, f'{s}.csv'))
|
|
201
214
|
if self.events is not None:
|
|
202
215
|
self.symbol_data[s] = self.symbol_data[s].iterrows()
|
|
203
216
|
|
|
@@ -340,7 +353,13 @@ class CSVDataHandler(BaseCSVDataHandler):
|
|
|
340
353
|
"""
|
|
341
354
|
csv_dir = kwargs.get("csv_dir")
|
|
342
355
|
csv_dir = csv_dir or BBSTRADER_DIR / 'csv_data'
|
|
343
|
-
super().__init__(
|
|
356
|
+
super().__init__(
|
|
357
|
+
events,
|
|
358
|
+
symbol_list,
|
|
359
|
+
csv_dir,
|
|
360
|
+
columns =kwargs.get('columns'),
|
|
361
|
+
index_col=kwargs.get('index_col', 0)
|
|
362
|
+
)
|
|
344
363
|
|
|
345
364
|
|
|
346
365
|
class MT5DataHandler(BaseCSVDataHandler):
|
|
@@ -372,32 +391,41 @@ class MT5DataHandler(BaseCSVDataHandler):
|
|
|
372
391
|
See `bbstrader.metatrader.rates.Rates` for other arguments.
|
|
373
392
|
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
374
393
|
"""
|
|
375
|
-
self.tf
|
|
376
|
-
self.start
|
|
377
|
-
self.end
|
|
378
|
-
self.use_utc
|
|
379
|
-
self.filer
|
|
380
|
-
self.fill_na
|
|
394
|
+
self.tf = kwargs.get('time_frame', 'D1')
|
|
395
|
+
self.start = kwargs.get('mt5_start', datetime(2000, 1, 1))
|
|
396
|
+
self.end = kwargs.get('mt5_end', datetime.now())
|
|
397
|
+
self.use_utc = kwargs.get('use_utc', False)
|
|
398
|
+
self.filer = kwargs.get('filter', False)
|
|
399
|
+
self.fill_na = kwargs.get('fill_na', False)
|
|
381
400
|
self.lower_cols = kwargs.get('lower_cols', True)
|
|
382
|
-
self.data_dir
|
|
401
|
+
self.data_dir = kwargs.get('data_dir')
|
|
383
402
|
self.symbol_list = symbol_list
|
|
384
|
-
|
|
385
|
-
|
|
403
|
+
self.kwargs = kwargs
|
|
404
|
+
|
|
405
|
+
csv_dir = self._download_and_cache_data(self.data_dir)
|
|
406
|
+
super().__init__(
|
|
407
|
+
events,
|
|
408
|
+
symbol_list,
|
|
409
|
+
csv_dir,
|
|
410
|
+
columns =kwargs.get('columns'),
|
|
411
|
+
index_col=kwargs.get('index_col', 0)
|
|
412
|
+
)
|
|
386
413
|
|
|
387
|
-
def
|
|
388
|
-
data_dir = cache_dir or BBSTRADER_DIR / '
|
|
414
|
+
def _download_and_cache_data(self, cache_dir: str):
|
|
415
|
+
data_dir = cache_dir or BBSTRADER_DIR / 'mt5' / self.tf
|
|
389
416
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
390
417
|
for symbol in self.symbol_list:
|
|
391
418
|
try:
|
|
392
419
|
data = download_historical_data(
|
|
393
420
|
symbol=symbol,
|
|
394
|
-
|
|
421
|
+
timeframe=self.tf,
|
|
395
422
|
date_from=self.start,
|
|
396
|
-
date_to=self.end,
|
|
423
|
+
date_to=self.end,
|
|
397
424
|
utc=self.use_utc,
|
|
398
425
|
filter=self.filer,
|
|
399
426
|
fill_na=self.fill_na,
|
|
400
|
-
lower_colnames=self.lower_cols
|
|
427
|
+
lower_colnames=self.lower_cols,
|
|
428
|
+
**self.kwargs
|
|
401
429
|
)
|
|
402
430
|
if data is None:
|
|
403
431
|
raise ValueError(f"No data found for {symbol}")
|
|
@@ -432,56 +460,229 @@ class YFDataHandler(BaseCSVDataHandler):
|
|
|
432
460
|
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
433
461
|
"""
|
|
434
462
|
self.symbol_list = symbol_list
|
|
435
|
-
self.start_date
|
|
436
|
-
self.end_date
|
|
437
|
-
self.cache_dir
|
|
463
|
+
self.start_date = kwargs.get('yf_start')
|
|
464
|
+
self.end_date = kwargs.get('yf_end', datetime.now())
|
|
465
|
+
self.cache_dir = kwargs.get('data_dir')
|
|
466
|
+
|
|
438
467
|
csv_dir = self._download_and_cache_data(self.cache_dir)
|
|
439
|
-
|
|
468
|
+
|
|
469
|
+
super().__init__(
|
|
470
|
+
events,
|
|
471
|
+
symbol_list,
|
|
472
|
+
csv_dir,
|
|
473
|
+
columns =kwargs.get('columns'),
|
|
474
|
+
index_col=kwargs.get('index_col', 0)
|
|
475
|
+
)
|
|
440
476
|
|
|
441
477
|
def _download_and_cache_data(self, cache_dir: str):
|
|
442
478
|
"""Downloads and caches historical data as CSV files."""
|
|
443
|
-
cache_dir = cache_dir or BBSTRADER_DIR / '
|
|
479
|
+
cache_dir = cache_dir or BBSTRADER_DIR / 'yfinance' / 'daily'
|
|
444
480
|
os.makedirs(cache_dir, exist_ok=True)
|
|
445
481
|
for symbol in self.symbol_list:
|
|
446
482
|
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
447
483
|
try:
|
|
448
484
|
data = yf.download(
|
|
449
|
-
symbol, start=self.start_date, end=self.end_date,
|
|
485
|
+
symbol, start=self.start_date, end=self.end_date,
|
|
486
|
+
multi_level_index=False, progress=False)
|
|
450
487
|
if data.empty:
|
|
451
488
|
raise ValueError(f"No data found for {symbol}")
|
|
452
|
-
data.to_csv(filepath)
|
|
489
|
+
data.to_csv(filepath)
|
|
453
490
|
except Exception as e:
|
|
454
491
|
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
455
492
|
return cache_dir
|
|
456
493
|
|
|
457
494
|
|
|
458
|
-
# TODO # Get data from EODHD
|
|
459
|
-
# https://eodhd.com/
|
|
460
495
|
class EODHDataHandler(BaseCSVDataHandler):
|
|
461
|
-
|
|
496
|
+
"""
|
|
497
|
+
Downloads historical data from EOD Historical Data.
|
|
498
|
+
Data is fetched using the `eodhd` library.
|
|
462
499
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
500
|
+
To use this class, you need to sign up for an API key at
|
|
501
|
+
https://eodhistoricaldata.com/ and provide the key as an argument.
|
|
502
|
+
"""
|
|
503
|
+
def __init__(self, events: Queue, symbol_list: List[str], **kwargs):
|
|
504
|
+
"""
|
|
505
|
+
Args:
|
|
506
|
+
events (Queue): The Event Queue for passing market events.
|
|
507
|
+
symbol_list (list[str]): List of symbols to download data for.
|
|
508
|
+
eodhd_start (str): Start date for historical data (YYYY-MM-DD).
|
|
509
|
+
eodhd_end (str): End date for historical data (YYYY-MM-DD).
|
|
510
|
+
data_dir (str, optional): Directory for caching data .
|
|
511
|
+
eodhd_period (str, optional): Time period for historical data (e.g., 'd', 'w', 'm', '1m', '5m', '1h').
|
|
512
|
+
eodhd_api_key (str, optional): API key for EOD Historical Data.
|
|
467
513
|
|
|
514
|
+
Note:
|
|
515
|
+
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
516
|
+
"""
|
|
517
|
+
self.symbol_list = symbol_list
|
|
518
|
+
self.start_date = kwargs.get('eodhd_start')
|
|
519
|
+
self.end_date = kwargs.get('eodhd_end', datetime.now().strftime('%Y-%m-%d'))
|
|
520
|
+
self.period = kwargs.get('eodhd_period', 'd')
|
|
521
|
+
self.cache_dir = kwargs.get('data_dir')
|
|
522
|
+
self.__api_key = kwargs.get('eodhd_api_key', 'demo')
|
|
523
|
+
|
|
524
|
+
csv_dir = self._download_and_cache_data(self.cache_dir)
|
|
525
|
+
|
|
526
|
+
super().__init__(
|
|
527
|
+
events,
|
|
528
|
+
symbol_list,
|
|
529
|
+
csv_dir,
|
|
530
|
+
columns =kwargs.get('columns'),
|
|
531
|
+
index_col=kwargs.get('index_col', 0)
|
|
532
|
+
)
|
|
468
533
|
|
|
469
|
-
|
|
470
|
-
|
|
534
|
+
def _get_data(self, symbol: str, period) -> pd.DataFrame | List[Dict]:
|
|
535
|
+
if not self.__api_key:
|
|
536
|
+
raise ValueError("API key is required for EODHD data.")
|
|
537
|
+
client = APIClient(api_key=self.__api_key)
|
|
538
|
+
if period in ['d', 'w', 'm']:
|
|
539
|
+
return client.get_historical_data(
|
|
540
|
+
symbol=symbol,
|
|
541
|
+
interval=period,
|
|
542
|
+
iso8601_start=self.start_date,
|
|
543
|
+
iso8601_end=self.end_date,
|
|
544
|
+
)
|
|
545
|
+
elif period in ['1m', '5m', '1h']:
|
|
546
|
+
hms = ' 00:00:00'
|
|
547
|
+
fmt = '%Y-%m-%d %H:%M:%S'
|
|
548
|
+
startdt = datetime.strptime(self.start_date + hms, fmt)
|
|
549
|
+
enddt = datetime.strptime(self.end_date + hms, fmt)
|
|
550
|
+
startdt = startdt.replace(tzinfo=timezone('UTC'))
|
|
551
|
+
enddt = enddt.replace(tzinfo=timezone('UTC'))
|
|
552
|
+
unix_start = int(startdt.timestamp())
|
|
553
|
+
unix_end = int(enddt.timestamp())
|
|
554
|
+
return client.get_intraday_historical_data(
|
|
555
|
+
symbol=symbol,
|
|
556
|
+
interval=period,
|
|
557
|
+
from_unix_time=unix_start,
|
|
558
|
+
to_unix_time=unix_end,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
def _forma_data(self, data: List[Dict] | pd.DataFrame) -> pd.DataFrame:
|
|
562
|
+
if isinstance(data, pd.DataFrame):
|
|
563
|
+
if data.empty or len(data) == 0:
|
|
564
|
+
raise ValueError("No data found.")
|
|
565
|
+
df = data.drop(labels=['symbol', 'interval'], axis=1)
|
|
566
|
+
df = df.rename(columns={'adjusted_close': 'adj_close'})
|
|
567
|
+
return df
|
|
568
|
+
|
|
569
|
+
elif isinstance(data, list):
|
|
570
|
+
if not data or len(data) == 0:
|
|
571
|
+
raise ValueError("No data found.")
|
|
572
|
+
df = pd.DataFrame(data)
|
|
573
|
+
df = df.drop(columns=['timestamp', 'gmtoffset'], axis=1)
|
|
574
|
+
df = df.rename(columns={'datetime': 'date'})
|
|
575
|
+
df['adj_close'] = df['close']
|
|
576
|
+
df = df[['date', 'open', 'high', 'low', 'close', 'adj_close', 'volume']]
|
|
577
|
+
df.date = pd.to_datetime(df.date)
|
|
578
|
+
df = df.set_index('date')
|
|
579
|
+
return df
|
|
580
|
+
|
|
581
|
+
def _download_and_cache_data(self, cache_dir: str):
|
|
582
|
+
"""Downloads and caches historical data as CSV files."""
|
|
583
|
+
cache_dir = cache_dir or BBSTRADER_DIR / 'eodhd' / self.period
|
|
584
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
585
|
+
for symbol in self.symbol_list:
|
|
586
|
+
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
587
|
+
try:
|
|
588
|
+
data = self._get_data(symbol, self.period)
|
|
589
|
+
data = self._forma_data(data)
|
|
590
|
+
data.to_csv(filepath)
|
|
591
|
+
except Exception as e:
|
|
592
|
+
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
593
|
+
return cache_dir
|
|
471
594
|
|
|
472
595
|
|
|
473
|
-
class
|
|
596
|
+
class FMPDataHandler(BaseCSVDataHandler):
|
|
474
597
|
"""
|
|
475
|
-
|
|
476
|
-
|
|
598
|
+
Downloads historical data from Financial Modeling Prep (FMP).
|
|
599
|
+
Data is fetched using the `financetoolkit` library.
|
|
600
|
+
|
|
601
|
+
To use this class, you need to sign up for an API key at
|
|
602
|
+
https://financialmodelingprep.com/developer/docs/pricing and
|
|
603
|
+
provide the key as an argument.
|
|
604
|
+
|
|
477
605
|
"""
|
|
478
|
-
|
|
606
|
+
def __init__(self, events: Queue, symbol_list: List[str], **kwargs):
|
|
607
|
+
"""
|
|
608
|
+
Args:
|
|
609
|
+
events (Queue): The Event Queue for passing market events.
|
|
610
|
+
symbol_list (list[str]): List of symbols to download data for.
|
|
611
|
+
fmp_start (str): Start date for historical data (YYYY-MM-DD).
|
|
612
|
+
fmp_end (str): End date for historical data (YYYY-MM-DD).
|
|
613
|
+
data_dir (str, optional): Directory for caching data .
|
|
614
|
+
fmp_period (str, optional): Time period for historical data
|
|
615
|
+
(e.g. daily, weekly, monthly, quarterly, yearly, "1min", "5min", "15min", "30min", "1hour").
|
|
616
|
+
fmp_api_key (str): API key for Financial Modeling Prep.
|
|
479
617
|
|
|
618
|
+
Note:
|
|
619
|
+
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
620
|
+
"""
|
|
621
|
+
self.symbol_list = symbol_list
|
|
622
|
+
self.start_date = kwargs.get('fmp_start')
|
|
623
|
+
self.end_date = kwargs.get('fmp_end', datetime.now().strftime('%Y-%m-%d'))
|
|
624
|
+
self.period = kwargs.get('fmp_period', 'daily')
|
|
625
|
+
self.cache_dir = kwargs.get('data_dir')
|
|
626
|
+
self.__api_key = kwargs.get('fmp_api_key')
|
|
480
627
|
|
|
481
|
-
|
|
482
|
-
|
|
628
|
+
csv_dir = self._download_and_cache_data(self.cache_dir)
|
|
629
|
+
|
|
630
|
+
super().__init__(
|
|
631
|
+
events,
|
|
632
|
+
symbol_list,
|
|
633
|
+
csv_dir,
|
|
634
|
+
columns =kwargs.get('columns'),
|
|
635
|
+
index_col=kwargs.get('index_col', 0)
|
|
636
|
+
)
|
|
483
637
|
|
|
484
|
-
|
|
638
|
+
def _get_data(self, symbol: str, period: str) -> pd.DataFrame:
|
|
639
|
+
if not self.__api_key:
|
|
640
|
+
raise ValueError("API key is required for FMP data.")
|
|
641
|
+
toolkit = Toolkit(
|
|
642
|
+
symbol,
|
|
643
|
+
api_key=self.__api_key,
|
|
644
|
+
start_date=self.start_date,
|
|
645
|
+
end_date=self.end_date,
|
|
646
|
+
benchmark_ticker=None,
|
|
647
|
+
progress_bar=False
|
|
648
|
+
)
|
|
649
|
+
if period in ['daily', 'weekly', 'monthly', 'quarterly', 'yearly']:
|
|
650
|
+
return toolkit.get_historical_data(period=period, progress_bar=False)
|
|
651
|
+
elif period in ['1min', '5min', '15min', '30min', '1hour']:
|
|
652
|
+
return toolkit.get_intraday_data(period=period, progress_bar=False)
|
|
653
|
+
|
|
654
|
+
def _format_data(self, data: pd.DataFrame, period: str) -> pd.DataFrame:
|
|
655
|
+
if data.empty or len(data) == 0:
|
|
656
|
+
raise ValueError("No data found.")
|
|
657
|
+
if period[0].isnumeric():
|
|
658
|
+
data = data.drop(columns=['Return', 'Volatility', 'Cumulative Return'], axis=1)
|
|
659
|
+
else:
|
|
660
|
+
data = data.drop(columns=['Dividends', 'Return', 'Volatility',
|
|
661
|
+
'Excess Return', 'Excess Volatility',
|
|
662
|
+
'Cumulative Return'], axis=1)
|
|
663
|
+
data = data.reset_index()
|
|
664
|
+
if 'Adj Close' not in data.columns:
|
|
665
|
+
data['Adj Close'] = data['Close']
|
|
666
|
+
data['date'] = data['date'].dt.to_timestamp()
|
|
667
|
+
data['date'] = pd.to_datetime(data['date'])
|
|
668
|
+
data.set_index('date', inplace=True)
|
|
669
|
+
return data
|
|
670
|
+
|
|
671
|
+
def _download_and_cache_data(self, cache_dir: str):
|
|
672
|
+
"""Downloads and caches historical data as CSV files."""
|
|
673
|
+
cache_dir = cache_dir or BBSTRADER_DIR / 'fmp' / self.period
|
|
674
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
675
|
+
for symbol in self.symbol_list:
|
|
676
|
+
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
677
|
+
try:
|
|
678
|
+
data = self._get_data(symbol, self.period)
|
|
679
|
+
data = self._format_data(data, self.period)
|
|
680
|
+
data.to_csv(filepath)
|
|
681
|
+
except Exception as e:
|
|
682
|
+
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
683
|
+
return cache_dir
|
|
485
684
|
|
|
486
685
|
|
|
487
686
|
# TODO Add data Handlers for Interactive Brokers
|
|
687
|
+
class TWSDataHandler(BaseCSVDataHandler):
|
|
688
|
+
...
|
bbstrader/btengine/strategy.py
CHANGED
|
@@ -86,6 +86,7 @@ class MT5Strategy(Strategy):
|
|
|
86
86
|
self.tf = kwargs.get("time_frame", 'D1')
|
|
87
87
|
self.logger = kwargs.get("logger")
|
|
88
88
|
self._initialize_portfolio()
|
|
89
|
+
self.kwargs = kwargs
|
|
89
90
|
|
|
90
91
|
@property
|
|
91
92
|
def cash(self) -> float:
|
|
@@ -201,19 +202,22 @@ class MT5Strategy(Strategy):
|
|
|
201
202
|
- ``action``: The action to take for the symbol (LONG, SHORT, EXIT, etc.)
|
|
202
203
|
- ``price``: The price at which to execute the action.
|
|
203
204
|
- ``stoplimit``: The stop-limit price for STOP-LIMIT orders.
|
|
205
|
+
- ``id``: The unique identifier for the strategy or order.
|
|
204
206
|
|
|
205
|
-
The dictionary can be use for pending orders (limit, stop, stop-limit) where the price is required
|
|
207
|
+
The dictionary can be use for pending orders (limit, stop, stop-limit) where the price is required
|
|
208
|
+
or for executing orders where the each order has a unique identifier.
|
|
206
209
|
"""
|
|
207
210
|
pass
|
|
208
211
|
|
|
209
|
-
def apply_risk_management(self, optimer, freq=252) -> Dict[str, float] | None:
|
|
212
|
+
def apply_risk_management(self, optimer, symbols=None, freq=252) -> Dict[str, float] | None:
|
|
210
213
|
"""
|
|
211
214
|
Apply risk management rules to the strategy.
|
|
212
215
|
"""
|
|
213
216
|
if optimer is None:
|
|
214
217
|
return None
|
|
218
|
+
symbols = symbols or self.symbols
|
|
215
219
|
prices = self.get_asset_values(
|
|
216
|
-
symbol_list=
|
|
220
|
+
symbol_list=symbols, bars=self.data, mode=self.mode,
|
|
217
221
|
window=freq, value_type='close', array=False, tf=self.tf
|
|
218
222
|
)
|
|
219
223
|
prices = pd.DataFrame(prices)
|
|
@@ -222,7 +226,7 @@ class MT5Strategy(Strategy):
|
|
|
222
226
|
weights = optimized_weights(prices=prices, freq=freq, method=optimer)
|
|
223
227
|
return {symbol: weight for symbol, weight in weights.items()}
|
|
224
228
|
except Exception:
|
|
225
|
-
return {symbol: 0.0 for symbol in
|
|
229
|
+
return {symbol: 0.0 for symbol in symbols}
|
|
226
230
|
|
|
227
231
|
def get_quantity(self, symbol, weight, price=None, volume=None, maxqty=None) -> int:
|
|
228
232
|
"""
|
|
@@ -530,7 +534,7 @@ class MT5Strategy(Strategy):
|
|
|
530
534
|
asset_values[asset] = getattr(values, value_type)
|
|
531
535
|
elif mode == 'live':
|
|
532
536
|
for asset in symbol_list:
|
|
533
|
-
rates = Rates(
|
|
537
|
+
rates = Rates(asset, timeframe=tf, count=window + 1, **self.kwargs)
|
|
534
538
|
if array:
|
|
535
539
|
values = getattr(rates, value_type).values
|
|
536
540
|
asset_values[asset] = values[~np.isnan(values)]
|
|
@@ -575,11 +579,11 @@ class MT5Strategy(Strategy):
|
|
|
575
579
|
Returns:
|
|
576
580
|
bool : True if there are open positions, False otherwise
|
|
577
581
|
"""
|
|
578
|
-
account = account or Account()
|
|
582
|
+
account = account or Account(**self.kwargs)
|
|
579
583
|
positions = account.get_positions(symbol=symbol)
|
|
580
584
|
if positions is not None:
|
|
581
585
|
open_positions = [
|
|
582
|
-
pos for pos in positions if pos.type == position
|
|
586
|
+
pos.ticket for pos in positions if pos.type == position
|
|
583
587
|
and pos.magic == strategy_id
|
|
584
588
|
]
|
|
585
589
|
if one_true:
|
|
@@ -600,7 +604,7 @@ class MT5Strategy(Strategy):
|
|
|
600
604
|
Returns:
|
|
601
605
|
prices : numpy array of buy or sell prices for open positions if any or an empty array.
|
|
602
606
|
"""
|
|
603
|
-
account = account or Account()
|
|
607
|
+
account = account or Account(**self.kwargs)
|
|
604
608
|
positions = account.get_positions(symbol=symbol)
|
|
605
609
|
if positions is not None:
|
|
606
610
|
prices = np.array([
|
bbstrader/config.py
CHANGED
|
@@ -3,6 +3,10 @@ from typing import List
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
|
|
6
|
+
|
|
7
|
+
ADMIRAL_PATH = "C:\\Program Files\\Admirals Group MT5 Terminal\\terminal64.exe"
|
|
8
|
+
FTMO_PATH = "C:\\Program Files\\FTMO MetaTrader 5\\terminal64.exe"
|
|
9
|
+
|
|
6
10
|
def get_config_dir(name: str=".bbstrader") -> Path:
|
|
7
11
|
"""
|
|
8
12
|
Get the path to the configuration directory.
|
|
File without changes
|
bbstrader/core/data.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
from financetoolkit import Toolkit
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'FMP',
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
class FMP(Toolkit):
|
|
11
|
+
"""
|
|
12
|
+
FMPData class for fetching data from Financial Modeling Prep API
|
|
13
|
+
using the Toolkit class from financetoolkit package.
|
|
14
|
+
|
|
15
|
+
See `financetoolkit` for more details.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, api_key: str ='', symbols: str | list = 'AAPL'):
|
|
19
|
+
super().__init__(tickers=symbols, api_key=api_key)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataBendo:
|
|
23
|
+
...
|
bbstrader/core/utils.py
ADDED
|
File without changes
|
|
File without changes
|
bbstrader/metatrader/account.py
CHANGED
|
@@ -125,9 +125,57 @@ AMG_EXCHANGES = {
|
|
|
125
125
|
'XSWX': r"Switzerland.*\(SWX\)"
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
def check_mt5_connection():
|
|
128
|
+
def check_mt5_connection(**kwargs):
|
|
129
|
+
"""
|
|
130
|
+
Initialize the connection to the MetaTrader 5 terminal.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
path (str, optional): The path to the MetaTrader 5 terminal executable file.
|
|
134
|
+
Defaults to None (e.g., "C:\\Program Files\\MetaTrader 5\\terminal64.exe").
|
|
135
|
+
login (int, optional): The login ID of the trading account. Defaults to None.
|
|
136
|
+
password (str, optional): The password of the trading account. Defaults to None.
|
|
137
|
+
server (str, optional): The name of the trade server to which the client terminal is connected.
|
|
138
|
+
Defaults to None.
|
|
139
|
+
timeout (int, optional): Connection timeout in milliseconds. Defaults to 60_000.
|
|
140
|
+
portable (bool, optional): If True, the portable mode of the terminal is used.
|
|
141
|
+
Defaults to False (See https://www.metatrader5.com/en/terminal/help/start_advanced/start#portable).
|
|
142
|
+
|
|
143
|
+
Notes:
|
|
144
|
+
If you want to lunch multiple terminal instances:
|
|
145
|
+
- Follow these instructions to lunch each terminal in portable mode first:
|
|
146
|
+
https://www.metatrader5.com/en/terminal/help/start_advanced/start#configuration_file
|
|
147
|
+
"""
|
|
148
|
+
path = kwargs.get('path', None)
|
|
149
|
+
login = kwargs.get('login', None)
|
|
150
|
+
password = kwargs.get('password', None)
|
|
151
|
+
server = kwargs.get('server', None)
|
|
152
|
+
timeout = kwargs.get('timeout', 60_000)
|
|
153
|
+
portable = kwargs.get('portable', False)
|
|
154
|
+
|
|
155
|
+
if path is None and (login or password or server):
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"You must provide a path to the terminal executable file"
|
|
158
|
+
f"when providing login, password or server"
|
|
159
|
+
)
|
|
129
160
|
try:
|
|
130
|
-
|
|
161
|
+
if path is not None:
|
|
162
|
+
if (
|
|
163
|
+
login is not None and
|
|
164
|
+
password is not None and
|
|
165
|
+
server is not None
|
|
166
|
+
):
|
|
167
|
+
init = mt5.initialize(
|
|
168
|
+
path=path,
|
|
169
|
+
login=login,
|
|
170
|
+
password=password,
|
|
171
|
+
server=server,
|
|
172
|
+
timeout=timeout,
|
|
173
|
+
portable=portable
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
init = mt5.initialize(path=path)
|
|
177
|
+
else:
|
|
178
|
+
init = mt5.initialize()
|
|
131
179
|
if not init:
|
|
132
180
|
raise_mt5_error(INIT_MSG)
|
|
133
181
|
except Exception:
|
|
@@ -135,9 +183,9 @@ def check_mt5_connection():
|
|
|
135
183
|
|
|
136
184
|
|
|
137
185
|
class Broker(object):
|
|
138
|
-
def __init__(self, name: str=None):
|
|
186
|
+
def __init__(self, name: str=None, **kwargs):
|
|
139
187
|
if name is None:
|
|
140
|
-
check_mt5_connection()
|
|
188
|
+
check_mt5_connection(**kwargs)
|
|
141
189
|
self._name = mt5.account_info().company
|
|
142
190
|
else:
|
|
143
191
|
self._name = name
|
|
@@ -157,8 +205,8 @@ class Broker(object):
|
|
|
157
205
|
|
|
158
206
|
|
|
159
207
|
class AdmiralMarktsGroup(Broker):
|
|
160
|
-
def __init__(self):
|
|
161
|
-
super().__init__("Admirals Group AS")
|
|
208
|
+
def __init__(self, **kwargs):
|
|
209
|
+
super().__init__("Admirals Group AS", **kwargs)
|
|
162
210
|
|
|
163
211
|
@property
|
|
164
212
|
def timezone(self) -> str:
|
|
@@ -166,8 +214,8 @@ class AdmiralMarktsGroup(Broker):
|
|
|
166
214
|
|
|
167
215
|
|
|
168
216
|
class JustGlobalMarkets(Broker):
|
|
169
|
-
def __init__(self):
|
|
170
|
-
super().__init__("Just Global Markets Ltd.")
|
|
217
|
+
def __init__(self, **kwargs):
|
|
218
|
+
super().__init__("Just Global Markets Ltd.", **kwargs)
|
|
171
219
|
|
|
172
220
|
@property
|
|
173
221
|
def timezone(self) -> str:
|
|
@@ -175,8 +223,8 @@ class JustGlobalMarkets(Broker):
|
|
|
175
223
|
|
|
176
224
|
|
|
177
225
|
class FTMO(Broker):
|
|
178
|
-
def __init__(self):
|
|
179
|
-
super().__init__("FTMO S.R.O.")
|
|
226
|
+
def __init__(self, **kwargs):
|
|
227
|
+
super().__init__("FTMO S.R.O.", **kwargs)
|
|
180
228
|
|
|
181
229
|
@property
|
|
182
230
|
def timezone(self) -> str:
|
|
@@ -230,8 +278,14 @@ class Account(object):
|
|
|
230
278
|
>>> trade_history = account.get_trade_history(from_date, to_date)
|
|
231
279
|
"""
|
|
232
280
|
|
|
233
|
-
def __init__(self):
|
|
234
|
-
|
|
281
|
+
def __init__(self, **kwargs):
|
|
282
|
+
"""
|
|
283
|
+
Initialize the Account class.
|
|
284
|
+
|
|
285
|
+
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
check_mt5_connection(**kwargs)
|
|
235
289
|
self._check_brokers()
|
|
236
290
|
|
|
237
291
|
def _check_brokers(self):
|