bbstrader 0.1.93__py3-none-any.whl → 0.1.94__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.

@@ -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__(events, symbol_list, csv_dir)
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 = kwargs.get('time_frame', 'D1')
376
- self.start = kwargs.get('mt5_start', datetime(2000, 1, 1))
377
- self.end = kwargs.get('mt5_end', datetime.now())
378
- self.use_utc = kwargs.get('use_utc', False)
379
- self.filer = kwargs.get('filter', False)
380
- self.fill_na = kwargs.get('fill_na', False)
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 = kwargs.get('data_dir')
393
+ self.data_dir = kwargs.get('data_dir')
383
394
  self.symbol_list = symbol_list
384
- csv_dir = self._download_data(self.data_dir)
385
- super().__init__(events, symbol_list, csv_dir)
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 _download_data(self, cache_dir: str):
388
- data_dir = cache_dir or BBSTRADER_DIR / 'mt5_data' / self.tf
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
- time_frame=self.tf,
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 = kwargs.get('yf_start', '2000-01-01')
436
- self.end_date = kwargs.get('yf_end', datetime.now().strftime('%Y-%m-%d'))
437
- self.cache_dir = kwargs.get('data_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
- super().__init__(events, symbol_list, csv_dir)
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 / 'yf_data' / 'daily'
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) # Cache the data
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
- # TODO # Get data from FMP using Financialtoolkit API
464
- # https://github.com/bbalouki/FinanceToolkit
465
- class FMPDataHandler(BaseCSVDataHandler):
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
- class AlgoseekDataHandler(BaseCSVDataHandler):
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
- class BaseFMPDataHanler(object):
586
+
587
+ class FMPDataHandler(BaseCSVDataHandler):
474
588
  """
475
- This will serve as the base class for all other FMP data
476
- that is not historical data and does not have an OHLC structure.
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
- class FMPFundamentalDataHandler(BaseFMPDataHanler):
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
- # TODO Add other Handlers for FMP
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
+ ...
@@ -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
- init = mt5.initialize()
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
- check_mt5_connection()
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):
@@ -82,6 +82,8 @@ class Rates(object):
82
82
  2. The `open, high, low, close, adjclose, returns,
83
83
  volume` properties returns data in Broker's timezone by default.
84
84
 
85
+ See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
86
+
85
87
  Example:
86
88
  >>> rates = Rates("EURUSD", "1h")
87
89
  >>> df = rates.get_historical_data(
@@ -94,17 +96,18 @@ class Rates(object):
94
96
  def __init__(
95
97
  self,
96
98
  symbol: str,
97
- time_frame: TimeFrame = 'D1',
99
+ timeframe: TimeFrame = 'D1',
98
100
  start_pos: Union[int , str] = 0,
99
101
  count: Optional[int] = MAX_BARS,
100
- session_duration: Optional[float] = None
102
+ session_duration: Optional[float] = None,
103
+ **kwargs
101
104
  ):
102
105
  """
103
106
  Initializes a new Rates instance.
104
107
 
105
108
  Args:
106
109
  symbol (str): Financial instrument symbol (e.g., "EURUSD").
107
- time_frame (str): Timeframe string (e.g., "D1", "1h", "5m").
110
+ timeframe (str): Timeframe string (e.g., "D1", "1h", "5m").
108
111
  start_pos (int, | str): Starting index (int) or date (str) for data retrieval.
109
112
  count (int, optional): Number of bars to retrieve default is
110
113
  the maximum bars availble in the MT5 terminal.
@@ -118,16 +121,17 @@ class Rates(object):
118
121
  For `session_duration` check your broker symbols details
119
122
  """
120
123
  self.symbol = symbol
121
- self.time_frame = self._validate_time_frame(time_frame)
124
+ tf = kwargs.get('time_frame')
125
+ self.time_frame = self._validate_time_frame(timeframe)
122
126
  self.sd = session_duration
123
- self.start_pos = self._get_start_pos(start_pos, time_frame)
127
+ self.start_pos = self._get_start_pos(start_pos, timeframe)
124
128
  self.count = count
125
- self._mt5_initialized()
126
- self.__account = Account()
129
+ self._mt5_initialized(**kwargs)
130
+ self.__account = Account(**kwargs)
127
131
  self.__data = self.get_rates_from_pos()
128
132
 
129
- def _mt5_initialized(self):
130
- check_mt5_connection()
133
+ def _mt5_initialized(self, **kwargs):
134
+ check_mt5_connection(**kwargs)
131
135
 
132
136
  def _get_start_pos(self, index, time_frame):
133
137
  if isinstance(index, int):
@@ -471,13 +475,13 @@ class Rates(object):
471
475
  df.to_csv(f"{self.symbol}.csv")
472
476
  return df
473
477
 
474
- def download_historical_data(symbol, time_frame, date_from,
478
+ def download_historical_data(symbol, timeframe, date_from,
475
479
  date_to=pd.Timestamp.now(),lower_colnames=True,
476
- utc=False, filter=False, fill_na=False, save_csv=False):
480
+ utc=False, filter=False, fill_na=False, save_csv=False, **kwargs):
477
481
  """Download historical data from MetaTrader 5 terminal.
478
482
  See `Rates.get_historical_data` for more details.
479
483
  """
480
- rates = Rates(symbol, time_frame)
484
+ rates = Rates(symbol, timeframe, **kwargs)
481
485
  data = rates.get_historical_data(
482
486
  date_from=date_from,
483
487
  date_to=date_to,
@@ -488,23 +492,23 @@ def download_historical_data(symbol, time_frame, date_from,
488
492
  )
489
493
  return data
490
494
 
491
- def get_data_from_pos(symbol, time_frame, start_pos=0, fill_na=False,
495
+ def get_data_from_pos(symbol, timeframe, start_pos=0, fill_na=False,
492
496
  count=MAX_BARS, lower_colnames=False, utc=False, filter=False,
493
- session_duration=23.0):
497
+ session_duration=23.0, **kwargs):
494
498
  """Get historical data from a specific position.
495
499
  See `Rates.get_rates_from_pos` for more details.
496
500
  """
497
- rates = Rates(symbol, time_frame, start_pos, count, session_duration)
501
+ rates = Rates(symbol, timeframe, start_pos, count, session_duration, **kwargs)
498
502
  data = rates.get_rates_from_pos(filter=filter, fill_na=fill_na,
499
503
  lower_colnames=lower_colnames, utc=utc)
500
504
  return data
501
505
 
502
- def get_data_from_date(symbol, time_frame, date_from, count=MAX_BARS, fill_na=False,
503
- lower_colnames=False, utc=False, filter=False):
506
+ def get_data_from_date(symbol, timeframe, date_from, count=MAX_BARS, fill_na=False,
507
+ lower_colnames=False, utc=False, filter=False, **kwargs):
504
508
  """Get historical data from a specific date.
505
509
  See `Rates.get_rates_from` for more details.
506
510
  """
507
- rates = Rates(symbol, time_frame)
511
+ rates = Rates(symbol, timeframe, **kwargs)
508
512
  data = rates.get_rates_from(date_from, count, filter=filter, fill_na=fill_na,
509
513
  lower_colnames=lower_colnames, utc=utc)
510
514
  return data
@@ -118,8 +118,10 @@ class RiskManagement(Account):
118
118
  tp (int, optional): Take Profit in points, Must be a positive number.
119
119
  be (int, optional): Break Even in points, Must be a positive number.
120
120
  rr (float, optional): Risk reward ratio, Must be a positive number. Defaults to 1.5.
121
+
122
+ See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
121
123
  """
122
- super().__init__()
124
+ super().__init__(**kwargs)
123
125
 
124
126
  # Validation
125
127
  if daily_risk is not None and daily_risk < 0:
@@ -137,6 +139,7 @@ class RiskManagement(Account):
137
139
  if var_time_frame not in TIMEFRAMES:
138
140
  raise ValueError("Unsupported time frame {}".format(var_time_frame))
139
141
 
142
+ self.kwargs = kwargs
140
143
  self.symbol = symbol
141
144
  self.start_time = start_time
142
145
  self.finishing_time = finishing_time
@@ -279,7 +282,7 @@ class RiskManagement(Account):
279
282
  tf_int = self._convert_time_frame(self._tf)
280
283
  interval = round((minutes / tf_int) * 252)
281
284
 
282
- rate = Rates(self.symbol, self._tf, 0, interval)
285
+ rate = Rates(self.symbol, self._tf, 0, interval, **self.kwargs)
283
286
  returns = rate.returns*100
284
287
  std = returns.std()
285
288
  point = self.get_symbol_info(self.symbol).point
@@ -333,7 +336,7 @@ class RiskManagement(Account):
333
336
  tf_int = self._convert_time_frame(tf)
334
337
  interval = round((minutes / tf_int) * 252)
335
338
 
336
- rate = Rates(self.symbol, tf, 0, interval)
339
+ rate = Rates(self.symbol, tf, 0, interval, **self.kwargs)
337
340
  returns = rate.returns*100
338
341
  p = self.get_account_info().margin_free
339
342
  mu = returns.mean()
@@ -138,6 +138,7 @@ class Trade(RiskManagement):
138
138
  - tp
139
139
  - be
140
140
  See the RiskManagement class for more details on these parameters.
141
+ See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
141
142
  """
142
143
  # Call the parent class constructor first
143
144
  super().__init__(
@@ -160,6 +161,7 @@ class Trade(RiskManagement):
160
161
  self.console_log = console_log
161
162
  self.logger = self._get_logger(logger, console_log)
162
163
  self.tf = kwargs.get("time_frame", 'D1')
164
+ self.kwargs = kwargs
163
165
 
164
166
  self.start_time_hour, self.start_time_minutes = self.start.split(":")
165
167
  self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
@@ -174,8 +176,8 @@ class Trade(RiskManagement):
174
176
  self.break_even_points = {}
175
177
  self.trail_after_points = []
176
178
 
177
- self.initialize()
178
- self.select_symbol()
179
+ self.initialize(**kwargs)
180
+ self.select_symbol(**kwargs)
179
181
  self.prepare_symbol()
180
182
 
181
183
  if self.verbose:
@@ -194,7 +196,7 @@ class Trade(RiskManagement):
194
196
  return config_logger(f'{log_path}/{logger}', consol_log)
195
197
  return logger
196
198
 
197
- def initialize(self):
199
+ def initialize(self, **kwargs):
198
200
  """
199
201
  Initializes the MetaTrader 5 (MT5) terminal for trading operations.
200
202
  This method attempts to establish a connection with the MT5 terminal.
@@ -207,7 +209,7 @@ class Trade(RiskManagement):
207
209
  try:
208
210
  if self.verbose:
209
211
  print("\nInitializing the basics.")
210
- check_mt5_connection()
212
+ check_mt5_connection(**kwargs)
211
213
  if self.verbose:
212
214
  print(
213
215
  f"You are running the @{self.expert_name} Expert advisor,"
@@ -216,7 +218,7 @@ class Trade(RiskManagement):
216
218
  except Exception as e:
217
219
  self.logger.error(f"During initialization: {e}")
218
220
 
219
- def select_symbol(self):
221
+ def select_symbol(self, **kwargs):
220
222
  """
221
223
  Selects the trading symbol in the MetaTrader 5 (MT5) terminal.
222
224
  This method ensures that the specified trading
@@ -228,6 +230,7 @@ class Trade(RiskManagement):
228
230
  MT5TerminalError: If symbole selection fails.
229
231
  """
230
232
  try:
233
+ check_mt5_connection(**kwargs)
231
234
  if not Mt5.symbol_select(self.symbol, True):
232
235
  raise_mt5_error(message=INIT_MSG)
233
236
  except Exception as e:
@@ -848,10 +851,11 @@ class Trade(RiskManagement):
848
851
  positions = self.get_positions(symbol=self.symbol)
849
852
  if positions is not None:
850
853
  positions = [position for position in positions if position.magic == id]
851
- profit = 0.0
852
- balance = self.get_account_info().balance
853
- target = round((balance * self.target)/100, 2)
854
- if positions is not None or len(positions) != 0:
854
+
855
+ if positions is not None:
856
+ profit = 0.0
857
+ balance = self.get_account_info().balance
858
+ target = round((balance * self.target)/100, 2)
855
859
  for position in positions:
856
860
  profit += position.profit
857
861
  fees = self.get_stats()[0]["average_fee"] * len(positions)
@@ -1489,6 +1493,7 @@ def create_trade_instance(
1489
1493
  daily_risk: Optional[Dict[str, float]] = None,
1490
1494
  max_risk: Optional[Dict[str, float]] = None,
1491
1495
  pchange_sl: Optional[Dict[str, float] | float] = None,
1496
+ **kwargs
1492
1497
  ) -> Dict[str, Trade]:
1493
1498
  """
1494
1499
  Creates Trade instances for each symbol provided.
bbstrader/models/risk.py CHANGED
@@ -310,7 +310,15 @@ class HMMRiskManager(RiskModel):
310
310
  and data filtered up to the end date if specified.
311
311
  """
312
312
  df = data_frame.copy()
313
- df['Returns'] = df['Adj Close'].pct_change()
313
+ if 'Returns' or 'returns' not in df.columns:
314
+ if 'Close' in df.columns:
315
+ df['Returns'] = df['Close'].pct_change()
316
+ elif 'Adj Close' in df.columns:
317
+ df['Returns'] = df['Adj Close'].pct_change()
318
+ else:
319
+ raise ValueError("No 'Close' or 'Adj Close' columns found.")
320
+ elif 'returns' in df.columns:
321
+ df.rename(columns={'returns': 'Returns'}, inplace=True)
314
322
  if end is not None:
315
323
  df = df[:end.strftime('%Y-%m-%d')]
316
324
  df.dropna(inplace=True)
@@ -104,7 +104,9 @@ def _mt5_execution(
104
104
  telegram=telegram, token=bot_token, chat_id=chat_id)
105
105
 
106
106
  logger = trades_instances[symbols[0]].logger
107
- max_trades = {symbol: mtrades[symbol] for symbol in symbols}
107
+ max_trades = {symbol: mtrades[symbol]
108
+ if mtrades is not None and symbol in mtrades
109
+ else trades_instances[symbol].max_trade() for symbol in symbols}
108
110
  if comment is None:
109
111
  trade = trades_instances[symbols[0]]
110
112
  comment = f"{trade.expert_name}@{trade.version}"
@@ -123,7 +125,7 @@ def _mt5_execution(
123
125
  long_market = {symbol: False for symbol in symbols}
124
126
  short_market = {symbol: False for symbol in symbols}
125
127
  try:
126
- check_mt5_connection()
128
+ check_mt5_connection(**kwargs)
127
129
  strategy: MT5Strategy = strategy_cls(symbol_list=symbols, mode='live', **kwargs)
128
130
  except Exception as e:
129
131
  logger.error(f"Initializing strategy, {e}, STRATEGY={STRATEGY}")
@@ -133,7 +135,7 @@ def _mt5_execution(
133
135
 
134
136
  while True:
135
137
  try:
136
- check_mt5_connection()
138
+ check_mt5_connection(**kwargs)
137
139
  current_date = datetime.now()
138
140
  today = current_date.strftime("%A").lower()
139
141
  time.sleep(0.5)
@@ -167,16 +169,18 @@ def _mt5_execution(
167
169
  continue
168
170
  time.sleep(0.5)
169
171
  try:
170
- check_mt5_connection()
172
+ check_mt5_connection(**kwargs)
171
173
  signals = strategy.calculate_signals()
172
- weights = strategy.apply_risk_management(optimizer)
174
+ weights = None
175
+ if hasattr(strategy, 'apply_risk_management'):
176
+ weights = strategy.apply_risk_management(optimizer)
173
177
  update_risk(weights)
174
178
  except Exception as e:
175
179
  logger.error(f"Calculating signal, {e}, STRATEGY={STRATEGY}")
176
180
  continue
177
181
  for symbol in symbols:
178
182
  try:
179
- check_mt5_connection()
183
+ check_mt5_connection(**kwargs)
180
184
  trade: Trade = trades_instances[symbol]
181
185
  tfmsg = f"Time Frame Not completed !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
182
186
  riskmsg = f"Risk not allowed !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
@@ -269,7 +273,7 @@ def _mt5_execution(
269
273
  )
270
274
  try:
271
275
  FRIDAY = 'friday'
272
- check_mt5_connection()
276
+ check_mt5_connection(**kwargs)
273
277
  day_end = all(trade.days_end() for trade in trades_instances.values())
274
278
  if closing_pnl is not None:
275
279
  closing = all(trade.positive_profit(id=trade.expert_id, th=closing_pnl)
@@ -461,7 +465,7 @@ class MT5ExecutionEngine():
461
465
  trading_days: Optional[List[str]] = TradingDays,
462
466
  comment: Optional[str] = None,
463
467
  **kwargs
464
- ):
468
+ ):
465
469
  """
466
470
  Args:
467
471
  symbol_list : List of symbols to trade
@@ -505,6 +509,7 @@ class MT5ExecutionEngine():
505
509
 
506
510
  4. All strategies must generate signals for backtesting and live trading.
507
511
  See the `bbstrader.trading.strategies` module for more information on how to create custom strategies.
512
+ See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
508
513
  """
509
514
  self.symbol_list = symbol_list
510
515
  self.trades_instances = trades_instances
@@ -525,8 +530,13 @@ class MT5ExecutionEngine():
525
530
  self.comment = comment
526
531
  self.kwargs = kwargs
527
532
 
533
+ def __repr__(self):
534
+ trades = self.trades_instances.keys()
535
+ s = self.strategy_cls.__name__
536
+ return f"MT5ExecutionEngine(Symbols={list(trades)}, Strategy={s})"
537
+
528
538
  def run(self):
529
- check_mt5_connection()
539
+ check_mt5_connection(**self.kwargs)
530
540
  _mt5_execution(
531
541
  self.symbol_list,
532
542
  self.trades_instances,
@@ -80,17 +80,17 @@ class SMAStrategy(Strategy):
80
80
  self.symbol_list = symbol_list or self.bars.symbol_list
81
81
  self.mode = mode
82
82
 
83
+ self.kwargs = kwargs
83
84
  self.short_window = kwargs.get("short_window", 50)
84
85
  self.long_window = kwargs.get("long_window", 200)
85
86
  self.tf = kwargs.get("time_frame", 'D1')
86
87
  self.qty = get_quantities(
87
88
  kwargs.get('quantities', 100), self.symbol_list)
88
89
  self.sd = kwargs.get("session_duration", 23.0)
89
- self.risk_models = build_hmm_models(self.symbol_list, **kwargs)
90
+ self.risk_models = build_hmm_models(self.symbol_list, **self.kwargs)
90
91
  self.risk_window = kwargs.get("risk_window", self.long_window)
91
92
  self.bought = self._calculate_initial_bought()
92
93
 
93
-
94
94
  def _calculate_initial_bought(self):
95
95
  bought = {}
96
96
  for s in self.symbol_list:
@@ -151,13 +151,13 @@ class SMAStrategy(Strategy):
151
151
  signal = SignalEvent(
152
152
  1, s, dt, 'SHORT', quantity=self.qty[s], price=price)
153
153
  self.bought[s] = 'SHORT'
154
- signals[s] = signal
154
+ signals[s] = signal
155
155
  return signals
156
156
 
157
157
  def get_live_data(self):
158
158
  symbol_data = {symbol: None for symbol in self.symbol_list}
159
159
  for symbol in self.symbol_list:
160
- sig_rate = Rates(symbol, self.tf, 0, self.risk_window)
160
+ sig_rate = Rates(symbol, self.tf, 0, self.risk_window+2, **self.kwargs)
161
161
  hmm_data = sig_rate.returns.values
162
162
  prices = sig_rate.close.values
163
163
  current_regime = self.risk_models[symbol].which_trade_allowed(hmm_data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bbstrader
3
- Version: 0.1.93
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/
@@ -23,7 +23,7 @@ Classifier: License :: OSI Approved :: MIT License
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: pandas
26
- Requires-Dist: numpy ==1.26.4
26
+ Requires-Dist: numpy==1.26.4
27
27
  Requires-Dist: yfinance
28
28
  Requires-Dist: scipy
29
29
  Requires-Dist: hmmlearn
@@ -47,8 +47,10 @@ 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
- Requires-Dist: MetaTrader5 ; extra == 'mt5'
53
+ Requires-Dist: MetaTrader5; extra == "mt5"
52
54
 
53
55
  # Simplified Investment & Trading Toolkit
54
56
  ![bbstrader](https://github.com/bbalouki/bbstrader/blob/main/assets/bbstrader_logo.png?raw=true)
@@ -3,30 +3,30 @@ bbstrader/config.py,sha256=_AD_Cd-w5zyabm1CBPNGhzcZuSjThB7jyzTcjbrIlUQ,3618
3
3
  bbstrader/tseries.py,sha256=qJKLxHnPOjB7dXon-ITK7vU1fAuvl8evzET6lSSnijQ,53572
4
4
  bbstrader/btengine/__init__.py,sha256=OaXZTjgDwqWrjPq-CNE4kJkmriKXt9t5pIghW1MDTeo,2911
5
5
  bbstrader/btengine/backtest.py,sha256=A3S84jpGTE_zhguOEGoGu6H_4ws4Iq5sf0n7TZaUYfQ,14615
6
- bbstrader/btengine/data.py,sha256=A6jUqDnjl-w1OSzbLLPfS1WfJ8Se25AqigJs9pbe0wc,17966
6
+ bbstrader/btengine/data.py,sha256=3bdd50hMrszNmcOXd8BYtkjTjg56fiThW2EJsItsCJA,26509
7
7
  bbstrader/btengine/event.py,sha256=zF_ST4tcjV5uJJVV1IbRXQgCLbca2R2fmE7A2MaIno4,8748
8
8
  bbstrader/btengine/execution.py,sha256=Fs6Hk64DxEOEVzAjsQ3CIVvYifWLLgkDjOixSh_Ghsc,10282
9
9
  bbstrader/btengine/performance.py,sha256=WTYzB50lUD5aShPIEebbQPlaC2NVW6VfxdgGHjcIIAw,10707
10
10
  bbstrader/btengine/portfolio.py,sha256=wCRmGxaZvihUPlXIlZp9cQo9fqPP-Tk5oALjknMfnos,16055
11
11
  bbstrader/btengine/strategy.py,sha256=6IN1KQ-a-IQgbCEOflKTtGh-ouztwsVjik6TuMg6CY0,30210
12
12
  bbstrader/metatrader/__init__.py,sha256=OLVOB_EieEb1P72I8V4Vem8kQWJ__D_L3c_wfwqY-9k,211
13
- bbstrader/metatrader/account.py,sha256=3KeGLZ397kctf3EW_y8n9ENswAMU0tBQJuX_L0VXMrI,53909
14
- bbstrader/metatrader/rates.py,sha256=1dJHbVqoT41m3EhF0wRe7dSGe5Kf3o5Maskkw-i5qsQ,20810
15
- bbstrader/metatrader/risk.py,sha256=8NnH1kRvWd_JLieCpVty6hHKz2awrIQV2c8oykxELh0,26596
16
- bbstrader/metatrader/trade.py,sha256=uigDah9n_rVJiwSslTAArLP94sde1dxYyGyRVIPPgb4,70210
13
+ bbstrader/metatrader/account.py,sha256=tkcAEgFIYZwtFRQc07lXQQFWRqglzR4H4LjX2BDRvj8,56371
14
+ bbstrader/metatrader/rates.py,sha256=RLzeq26LJ8dzFccgTcMjSUut4lsnt7xrWPXf5Z8xVuQ,21073
15
+ bbstrader/metatrader/risk.py,sha256=uLarOF-g9-RBdJuKSmIfT5WrPn47bmrvMxP21pQg4xo,26793
16
+ bbstrader/metatrader/trade.py,sha256=xAdwe00qNP0qeDzXGCK1_i4lsa9LpUkhbj82XqwJe6Y,70464
17
17
  bbstrader/metatrader/utils.py,sha256=BTaZun4DKWpCxBBzY0SLQqqz7n_7F_R1F59APfyaa3E,17666
18
18
  bbstrader/models/__init__.py,sha256=mpxtXYEcE8hwNDbzJf8MRqnBIa2T1voraEk0U0ri53c,437
19
19
  bbstrader/models/factors.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  bbstrader/models/ml.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  bbstrader/models/optimization.py,sha256=JlMsda9L-ADSgw4YPE4o3CsL1Yyxfeahf9kUb-EZqqM,6699
22
22
  bbstrader/models/portfolios.py,sha256=dFTZ3maRVY_O3UOIoRlLCbAow3SiLTQYt1q5DNaRUxE,8223
23
- bbstrader/models/risk.py,sha256=Pm_WoGI-vtPW75fwo_7ptF2Br-xQYBwrAAOIgqDQmy8,15120
23
+ bbstrader/models/risk.py,sha256=k1f78_a6oYk24kv-iURecsSfpgCTWi6IpLsoR4LwnWg,15530
24
24
  bbstrader/trading/__init__.py,sha256=3CCzV5rQbH8NthjDJhD0_2FABvpiCmkeC9cVeoW7bi4,438
25
- bbstrader/trading/execution.py,sha256=p_SKUziBBDuGiOmuxBsgBvxWu5nDVGZmtQBKw8OoZgE,25967
25
+ bbstrader/trading/execution.py,sha256=EhrTtDVV1Dz7T7_RglY4he3ZZMzijgl5tcRhQvbfIWA,26545
26
26
  bbstrader/trading/scripts.py,sha256=rQmnG_4F_MuUEc96RXpAQT4kXrC-FkscsgHKgDAR_-Y,1902
27
- bbstrader/trading/strategies.py,sha256=ztKNL4Nmlb-4N8_cq0OJyn3E2cRcdKdKu3FeTbZrHsU,36402
28
- bbstrader-0.1.93.dist-info/LICENSE,sha256=1EudjwwP2oTJy8Vh0e-Kzv8VZZU95y-t6c3DYhR51uc,1115
29
- bbstrader-0.1.93.dist-info/METADATA,sha256=XByGtsQ885U8oZpZOR9DPhQjdfi5eHbScJyGlcbbjxM,9932
30
- bbstrader-0.1.93.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
31
- bbstrader-0.1.93.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
32
- bbstrader-0.1.93.dist-info/RECORD,,
27
+ bbstrader/trading/strategies.py,sha256=6bj-xXIEzhTfWrGHAkjWJg7U1OkARu7YzoaWeSrfuZY,36460
28
+ bbstrader-0.1.94.dist-info/LICENSE,sha256=1EudjwwP2oTJy8Vh0e-Kzv8VZZU95y-t6c3DYhR51uc,1115
29
+ bbstrader-0.1.94.dist-info/METADATA,sha256=JKdBaA3tjT_Deirxlf_2jSQkvlvZh6UxdVqWMph84mI,9983
30
+ bbstrader-0.1.94.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
31
+ bbstrader-0.1.94.dist-info/top_level.txt,sha256=Wwj322jZmxGZ6gD_TdaPiPLjED5ReObm5omerwlmZIg,10
32
+ bbstrader-0.1.94.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.5.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5