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

Files changed (38) hide show
  1. bbstrader/__init__.py +10 -1
  2. bbstrader/__main__.py +5 -0
  3. bbstrader/apps/_copier.py +3 -3
  4. bbstrader/btengine/strategy.py +113 -38
  5. bbstrader/compat.py +18 -10
  6. bbstrader/config.py +0 -16
  7. bbstrader/core/scripts.py +4 -3
  8. bbstrader/metatrader/account.py +51 -26
  9. bbstrader/metatrader/analysis.py +30 -16
  10. bbstrader/metatrader/copier.py +136 -58
  11. bbstrader/metatrader/trade.py +39 -45
  12. bbstrader/metatrader/utils.py +5 -4
  13. bbstrader/models/factors.py +17 -13
  14. bbstrader/models/ml.py +96 -49
  15. bbstrader/models/nlp.py +83 -66
  16. bbstrader/trading/execution.py +39 -22
  17. bbstrader/tseries.py +103 -127
  18. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/METADATA +29 -46
  19. bbstrader-0.3.6.dist-info/RECORD +62 -0
  20. bbstrader-0.3.6.dist-info/top_level.txt +3 -0
  21. docs/conf.py +56 -0
  22. tests/__init__.py +0 -0
  23. tests/engine/__init__.py +1 -0
  24. tests/engine/test_backtest.py +58 -0
  25. tests/engine/test_data.py +536 -0
  26. tests/engine/test_events.py +300 -0
  27. tests/engine/test_execution.py +219 -0
  28. tests/engine/test_portfolio.py +307 -0
  29. tests/metatrader/__init__.py +0 -0
  30. tests/metatrader/test_account.py +1769 -0
  31. tests/metatrader/test_rates.py +292 -0
  32. tests/metatrader/test_risk_management.py +700 -0
  33. tests/metatrader/test_trade.py +439 -0
  34. bbstrader-0.3.4.dist-info/RECORD +0 -49
  35. bbstrader-0.3.4.dist-info/top_level.txt +0 -1
  36. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/WHEEL +0 -0
  37. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/entry_points.txt +0 -0
  38. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/licenses/LICENSE +0 -0
bbstrader/models/ml.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import warnings
3
+ from concurrent.futures import ThreadPoolExecutor, as_completed
3
4
  from itertools import product
4
5
  from time import time
5
6
 
@@ -7,7 +8,6 @@ import lightgbm as lgb
7
8
  import matplotlib.pyplot as plt
8
9
  import numpy as np
9
10
  import pandas as pd
10
- import pandas_ta as ta
11
11
  import seaborn as sns
12
12
  import yfinance as yf
13
13
  from alphalens import performance as perf
@@ -22,15 +22,30 @@ from loguru import logger as log
22
22
  from scipy.stats import spearmanr
23
23
  from sklearn.preprocessing import LabelEncoder, StandardScaler
24
24
 
25
+ try:
26
+ # This is to fix posix import error in pandas_ta
27
+ # On windows systems
28
+ import posix # noqa: F401
29
+ except (ImportError, Exception):
30
+ import bbstrader.compat # noqa: F401
31
+
32
+ import pandas_ta as ta
33
+
25
34
  warnings.filterwarnings("ignore")
26
35
 
27
36
  __all__ = ["OneStepTimeSeriesSplit", "MultipleTimeSeriesCV", "LightGBModel"]
28
37
 
29
38
 
30
39
  class OneStepTimeSeriesSplit:
31
- __author__ = "Stefan Jansen"
32
- """Generates tuples of train_idx, test_idx pairs
33
- Assumes the index contains a level labeled 'date'"""
40
+ """
41
+ Generates tuples of train_idx, test_idx pairs
42
+ Assumes the index contains a level labeled 'date'
43
+
44
+ References
45
+ ----------
46
+ Stefan Jansen (2020). Machine Learning for Algorithmic Trading - Second Edition.
47
+ Chapter 12, Boosting Your Trading Strategy.
48
+ """
34
49
 
35
50
  def __init__(self, n_splits=3, test_period_length=1, shuffle=False):
36
51
  self.n_splits = n_splits
@@ -62,11 +77,15 @@ class OneStepTimeSeriesSplit:
62
77
 
63
78
 
64
79
  class MultipleTimeSeriesCV:
65
- __author__ = "Stefan Jansen"
66
80
  """
67
81
  Generates tuples of train_idx, test_idx pairs
68
82
  Assumes the MultiIndex contains levels 'symbol' and 'date'
69
83
  purges overlapping outcomes
84
+
85
+ References
86
+ ----------
87
+ Stefan Jansen (2020). Machine Learning for Algorithmic Trading - Second Edition.
88
+ Chapter 12, Boosting Your Trading Strategy.
70
89
  """
71
90
 
72
91
  def __init__(
@@ -187,7 +206,7 @@ class LightGBModel(object):
187
206
  # Compute Bollinger Bands using pandas_ta
188
207
  bb = ta.bbands(close, length=20)
189
208
  return pd.DataFrame(
190
- {"bb_high": bb["BBU_20_2.0"], "bb_low": bb["BBL_20_2.0"]}, index=close.index
209
+ {"bb_high": bb["BBU_20_2.0_2.0"], "bb_low": bb["BBL_20_2.0_2.0"]}, index=close.index
191
210
  )
192
211
 
193
212
  def _compute_atr(self, stock_data):
@@ -235,26 +254,29 @@ class LightGBModel(object):
235
254
  return prices
236
255
 
237
256
  def download_boosting_data(self, tickers, start, end=None):
238
- data = []
239
- for ticker in tickers:
240
- try:
241
- prices = yf.download(
242
- ticker,
243
- start=start,
244
- end=end,
245
- progress=False,
246
- multi_level_index=False,
247
- auto_adjust=True,
248
- )
249
- if prices.empty:
250
- continue
251
- prices["symbol"] = ticker
252
- data.append(prices)
253
- except: # noqa: E722
254
- continue
255
- data = pd.concat(data)
257
+ try:
258
+ data = yf.download(
259
+ tickers,
260
+ start=start,
261
+ end=end,
262
+ progress=False,
263
+ auto_adjust=True,
264
+ threads=True,
265
+ )
266
+ if data.empty:
267
+ return pd.DataFrame()
268
+
269
+ data = (
270
+ data.stack(level=1).rename_axis(["Date", "symbol"]).reset_index(level=1)
271
+ )
272
+ except Exception as e:
273
+ self.logger.error(f"An error occurred during data download: {e}")
274
+ return pd.DataFrame()
275
+
276
+ # The rest of the data processing is the same as the original function
256
277
  if "Adj Close" in data.columns:
257
278
  data = data.drop(columns=["Adj Close"])
279
+
258
280
  data = (
259
281
  data.rename(columns={s: s.lower().replace(" ", "_") for s in data.columns})
260
282
  .set_index("symbol", append=True)
@@ -265,17 +287,11 @@ class LightGBModel(object):
265
287
  return data
266
288
 
267
289
  def download_metadata(self, tickers):
268
- def clean_text_column(series: pd.Series) -> pd.Series:
269
- return (
270
- series.str.lower()
271
- # use regex=False for literal string replacements
272
- .str.replace("-", "", regex=False)
273
- .str.replace("&", "and", regex=False)
274
- .str.replace(" ", "_", regex=False)
275
- .str.replace("__", "_", regex=False)
276
- )
290
+ """
291
+ Downloads metadata for multiple tickers concurrently using a thread pool.
292
+ """
277
293
 
278
- metadata = [
294
+ METADATA_KEYS = [
279
295
  "industry",
280
296
  "sector",
281
297
  "exchange",
@@ -296,7 +312,7 @@ class LightGBModel(object):
296
312
  "marketCap",
297
313
  ]
298
314
 
299
- columns = {
315
+ COLUMN_MAP = {
300
316
  "industry": "industry",
301
317
  "sector": "sector",
302
318
  "exchange": "exchange",
@@ -316,22 +332,53 @@ class LightGBModel(object):
316
332
  "askSize": "asksize",
317
333
  "marketCap": "marketcap",
318
334
  }
319
- data = []
320
- for symbol in tickers:
335
+
336
+ def _clean_text_column(series: pd.Series) -> pd.Series:
337
+ """Helper function to clean text columns."""
338
+ return (
339
+ series.str.lower()
340
+ .str.replace("-", "", regex=False)
341
+ .str.replace("&", "and", regex=False)
342
+ .str.replace(" ", "_", regex=False)
343
+ .str.replace("__", "_", regex=False)
344
+ )
345
+
346
+ def _fetch_single_ticker_info(symbol):
347
+ """Worker function to fetch and process info for one ticker."""
321
348
  try:
322
- symbol_info = yf.Ticker(symbol).info
323
- except: # noqa: E722
324
- continue
325
- infos = {}
326
- for info in metadata:
327
- infos[info] = symbol_info.get(info)
328
- data.append(infos)
349
+ ticker_info = yf.Ticker(symbol).info
350
+ return {key: ticker_info.get(key) for key in METADATA_KEYS}
351
+ except Exception:
352
+ return None
353
+
354
+ data = []
355
+ with ThreadPoolExecutor(max_workers=20) as executor:
356
+ future_to_ticker = {
357
+ executor.submit(_fetch_single_ticker_info, ticker): ticker
358
+ for ticker in tickers
359
+ }
360
+
361
+ for future in as_completed(future_to_ticker):
362
+ result = future.result()
363
+ if result:
364
+ data.append(result)
365
+
366
+ if not data:
367
+ return pd.DataFrame()
368
+
329
369
  metadata = pd.DataFrame(data)
330
- metadata = metadata.rename(columns=columns)
331
- metadata.dyield = metadata.dyield.fillna(0)
332
- metadata.sector = clean_text_column(metadata.sector)
333
- metadata.industry = clean_text_column(metadata.industry)
334
- metadata = metadata.set_index("symbol")
370
+ metadata = metadata.rename(columns=COLUMN_MAP)
371
+
372
+ if "dyield" in metadata.columns:
373
+ metadata.dyield = metadata.dyield.fillna(0)
374
+ if "sector" in metadata.columns:
375
+ metadata.sector = _clean_text_column(metadata.sector)
376
+ if "industry" in metadata.columns:
377
+ metadata.industry = _clean_text_column(metadata.industry)
378
+
379
+ if "symbol" in metadata.columns:
380
+ metadata = metadata.set_index("symbol")
381
+
335
382
  return metadata
336
383
 
337
384
  def _select_nlargest_liquidity_stocks(
bbstrader/models/nlp.py CHANGED
@@ -577,44 +577,55 @@ class SentimentAnalyzer(object):
577
577
  **kwargs,
578
578
  ) -> Dict[str, float]:
579
579
  """
580
- Computes sentiment scores for a list of financial tickers based on news and social media data.
581
-
582
- Process:
583
- 1. Collects news articles and posts related to each ticker from various sources:
584
- - Yahoo Finance News
585
- - Google Finance News
586
- - Reddit posts
587
- - Financial Modeling Prep (FMP) news
588
- 2. Analyzes sentiment from each source:
589
- - Uses VADER for Yahoo and Google Finance news.
590
- - Uses TextBlob for Reddit and FMP news.
591
- 3. Computes an overall sentiment score using a weighted average approach.
592
-
593
- Args:
594
- tickers (List[str] | List[Tuple[str, str]]): A list of asset tickers to analyze
595
- - if using tuples, the first element is the ticker and the second is the asset type.
596
- - if using a single string, the asset type must be specified or the default is "stock".
597
- lexicon (dict, optional): A custom sentiment lexicon to update VADER's default lexicon.
598
- asset_type (str, optional): The type of asset, Defaults to "stock",
599
- supported types include:
600
- - "stock": Stock symbols (e.g., AAPL, MSFT)
601
- - "etf": Exchange-traded funds (e.g., SPY, QQQ)
602
- - "future": Futures contracts (e.g., CL=F for crude oil)
603
- - "forex": Forex pairs (e.g., EURUSD=X, USDJPY=X)
604
- - "crypto": Cryptocurrency pairs (e.g., BTC-USD, ETH-USD)
605
- - "index": Stock market indices (e.g., ^GSPC for S&P 500)
606
- top_news (int, optional): Number of news articles/posts to fetch per source. Defaults to 10.
607
- **kwargs: Additional parameters for API authentication and data retrieval, including:
608
- - fmp_api (str): API key for Financial Modeling Prep.
609
- - client_id, client_secret, user_agent (str): Credentials for accessing Reddit API.
610
-
611
- Returns:
612
- Dict[str, float]: A dictionary mapping each ticker to its overall sentiment score.
613
- - Positive values indicate positive sentiment.
614
- - Negative values indicate negative sentiment.
615
- - Zero indicates neutral sentiment.
616
- Notes:
617
- The tickers names must follow yahoo finance conventions.
580
+ Compute sentiment scores for a list of financial tickers based on news and social media data.
581
+
582
+ Process
583
+ -------
584
+ 1. Collect news articles and posts related to each ticker from various sources:
585
+ * Yahoo Finance News
586
+ * Google Finance News
587
+ * Reddit posts
588
+ * Financial Modeling Prep (FMP) news
589
+ 2. Analyze sentiment from each source:
590
+ * Uses VADER for Yahoo and Google Finance news.
591
+ * Uses TextBlob for Reddit and FMP news.
592
+ 3. Compute an overall sentiment score using a weighted average approach.
593
+
594
+ Parameters
595
+ ----------
596
+ tickers : list of str or list of tuple
597
+ A list of asset tickers to analyze.
598
+ * If using tuples, the first element is the ticker and the second is the asset type.
599
+ * If using a single string, the asset type must be specified or defaults to "stock".
600
+ lexicon : dict, optional
601
+ A custom sentiment lexicon to update VADER's default lexicon. Default is None.
602
+ asset_type : str, optional
603
+ The type of asset. Default is "stock".
604
+ Supported types include:
605
+ * "stock": Stock symbols (e.g., AAPL, MSFT)
606
+ * "etf": Exchange-traded funds (e.g., SPY, QQQ)
607
+ * "future": Futures contracts (e.g., CL=F for crude oil)
608
+ * "forex": Forex pairs (e.g., EURUSD=X, USDJPY=X)
609
+ * "crypto": Cryptocurrency pairs (e.g., BTC-USD, ETH-USD)
610
+ * "index": Stock market indices (e.g., ^GSPC for S&P 500)
611
+ top_news : int, optional
612
+ Number of news articles/posts to fetch per source. Default is 10.
613
+ **kwargs : dict
614
+ Additional parameters for API authentication and data retrieval. Must include:
615
+ * fmp_api (str): API key for Financial Modeling Prep.
616
+ * client_id, client_secret, user_agent (str): Credentials for Reddit API.
617
+
618
+ Returns
619
+ -------
620
+ dict of str to float
621
+ A dictionary mapping each ticker to its overall sentiment score.
622
+ * Positive values indicate positive sentiment.
623
+ * Negative values indicate negative sentiment.
624
+ * Zero indicates neutral sentiment.
625
+
626
+ Notes
627
+ -----
628
+ Ticker names must follow Yahoo Finance conventions.
618
629
  """
619
630
 
620
631
  sentiment_results = {}
@@ -761,30 +772,36 @@ class SentimentAnalyzer(object):
761
772
  The dashboard visualizes sentiment scores for given tickers using interactive
762
773
  bar and scatter plots. It fetches new sentiment data at specified intervals.
763
774
 
764
- Args:
765
- tickers (List[str] | List[Tuple[str, str]]):
766
- A list of financial asset tickers to analyze.
767
- - If using tuples, the first element is the ticker and the second is the asset type.
768
- - If using a single string, the asset type must be specified or defaults to "stock".
769
- asset_type (str, optional):
770
- The type of financial asset ("stock", "forex", "crypto"). Defaults to "stock".
771
- lexicon (dict, optional):
772
- A custom sentiment lexicon. Defaults to None.
773
- interval (int, optional):
774
- The refresh interval (in milliseconds) for sentiment data updates. Defaults to 100000.
775
- top_n (int, optional):
776
- The number of top and bottom assets to display in the sentiment bar chart. Defaults to 20.
777
- **kwargs (dict):
778
- Additional arguments required for fetching sentiment data. Must include:
779
- - client_id (str): Reddit API client ID.
780
- - client_secret (str): Reddit API client secret.
781
- - user_agent (str): User agent for Reddit API.
782
- - fmp_api (str): Financial Modeling Prep (FMP) API key.
783
-
784
- Returns:
785
- None: The function does not return anything but starts a real-time interactive dashboard.
775
+ Parameters
776
+ ----------
777
+ tickers : list of str or list of tuple
778
+ A list of financial asset tickers to analyze.
779
+ * If using tuples, the first element is the ticker and the second is the asset type.
780
+ * If using a single string, the asset type must be specified or defaults to "stock".
781
+ asset_type : str, optional
782
+ The type of financial asset ("stock", "forex", "crypto"). Default is "stock".
783
+ lexicon : dict, optional
784
+ A custom sentiment lexicon. Default is None.
785
+ interval : int, optional
786
+ The refresh interval (in milliseconds) for sentiment data updates. Default is 100000.
787
+ top_n : int, optional
788
+ The number of top and bottom assets to display in the sentiment bar chart. Default is 20.
789
+ **kwargs : dict
790
+ Additional arguments required for fetching sentiment data. Must include:
791
+ * client_id (str): Reddit API client ID.
792
+ * client_secret (str): Reddit API client secret.
793
+ * user_agent (str): User agent for Reddit API.
794
+ * fmp_api (str): Financial Modeling Prep (FMP) API key.
795
+
796
+ Returns
797
+ -------
798
+ None
799
+ Starts a real-time interactive dashboard. Does not return any value.
800
+
801
+ Example
802
+ -------
803
+ .. code-block:: python
786
804
 
787
- Example Usage:
788
805
  sa = SentimentAnalyzer()
789
806
  sa.display_sentiment_dashboard(
790
807
  tickers=["AAPL", "TSLA", "GOOGL"],
@@ -799,12 +816,12 @@ class SentimentAnalyzer(object):
799
816
  fmp_api="your_fmp_api_key",
800
817
  )
801
818
 
802
- Notes:
803
- - Sentiment analysis is performed using financial news and social media discussions.
804
- - The dashboard updates in real-time at the specified interval.
805
- - The dashboard will keep running unless manually stopped (Ctrl+C).
819
+ Notes
820
+ -----
821
+ * Sentiment analysis is performed using financial news and social media discussions.
822
+ * The dashboard updates in real-time at the specified interval.
823
+ * The dashboard will keep running unless manually stopped (Ctrl+C).
806
824
  """
807
-
808
825
  app = dash.Dash(__name__)
809
826
 
810
827
  sentiment_history = {ticker: [] for ticker in tickers}
@@ -45,6 +45,7 @@ MT5_ENGINE_TIMEFRAMES = list(_TF_MAPPING.keys())
45
45
  TradingDays = ["monday", "tuesday", "wednesday", "thursday", "friday"]
46
46
  WEEK_DAYS = TradingDays + ["saturday", "sunday"]
47
47
  FRIDAY = "friday"
48
+ WEEK_ENDS = ["friday", "saturday", "sunday"]
48
49
 
49
50
  BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
50
51
  SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
@@ -513,17 +514,12 @@ class Mt5ExecutionEngine:
513
514
  logger.info(sessionmsg)
514
515
 
515
516
  def _check_is_day_ends(self, trade: Trade, symbol, period_type, today, closing):
516
- if trade.days_end():
517
- self._logmsgif("Day", symbol) if today != FRIDAY else self._logmsgif(
517
+ if trade.days_end() or (today in WEEK_ENDS and today != FRIDAY):
518
+ self._logmsgif("Day", symbol) if today not in WEEK_ENDS else self._logmsgif(
518
519
  "Week", symbol
519
520
  )
520
521
  if (
521
- (
522
- period_type == "month"
523
- and today == FRIDAY
524
- and self._is_month_ends()
525
- and closing
526
- )
522
+ (period_type == "month" and self._is_month_ends() and closing)
527
523
  or (period_type == "week" and today == FRIDAY and closing)
528
524
  or (period_type == "day" and closing)
529
525
  or (period_type == "24/7" and closing)
@@ -546,15 +542,17 @@ class Mt5ExecutionEngine:
546
542
  self.strategy.perform_period_end_checks()
547
543
  if self.period_end_action == "break" and closing:
548
544
  sys.exit(0)
549
- elif self.period_end_action == "sleep" and today != FRIDAY or not closing:
545
+ elif (
546
+ self.period_end_action == "sleep" and today not in WEEK_ENDS or not closing
547
+ ):
550
548
  self._sleep_over_night(sessionmsg)
551
- elif self.period_end_action == "sleep" and today == FRIDAY:
549
+ elif self.period_end_action == "sleep" and today in WEEK_ENDS:
552
550
  self._sleep_over_weekend(sessionmsg)
553
551
 
554
552
  def _weekly_end_checks(self, today, closing, sessionmsg):
555
- if today != FRIDAY:
553
+ if today not in WEEK_ENDS:
556
554
  self._sleep_over_night(sessionmsg)
557
- elif today == FRIDAY:
555
+ else:
558
556
  self.strategy.perform_period_end_checks()
559
557
  if self.period_end_action == "break" and closing:
560
558
  sys.exit(0)
@@ -562,9 +560,9 @@ class Mt5ExecutionEngine:
562
560
  self._sleep_over_weekend(sessionmsg)
563
561
 
564
562
  def _monthly_end_cheks(self, today, closing, sessionmsg):
565
- if today != FRIDAY:
563
+ if today not in WEEK_ENDS and not self._is_month_ends():
566
564
  self._sleep_over_night(sessionmsg)
567
- elif today == FRIDAY and self._is_month_ends() and closing:
565
+ elif self._is_month_ends() and closing:
568
566
  self.strategy.perform_period_end_checks()
569
567
  sys.exit(0)
570
568
  else:
@@ -956,7 +954,11 @@ class Mt5ExecutionEngine:
956
954
  def _handle_period_end_actions(self, today):
957
955
  try:
958
956
  check_mt5_connection(**self.kwargs)
959
- day_end = all(trade.days_end() for trade in self.trades_instances.values())
957
+ day_end = (
958
+ all(trade.days_end() for trade in self.trades_instances.values())
959
+ or (today in WEEK_ENDS and today != FRIDAY)
960
+ and self.period != "24/7"
961
+ )
960
962
  closing = self._is_closing()
961
963
  sessionmsg = f"{self.ACCOUNT} STARTING NEW TRADING SESSION ...\n"
962
964
  self._perform_period_end_actions(
@@ -1012,13 +1014,28 @@ class Mt5ExecutionEngine:
1012
1014
 
1013
1015
 
1014
1016
  def RunMt5Engine(account_id: str, **kwargs):
1015
- """Starts an MT5 execution engine for a given account.
1016
- Args:
1017
- account_id: Account ID to run the execution engine on.
1018
- **kwargs: Additional keyword arguments
1019
- _ symbol_list : List of symbols to trade.
1020
- - trades_instances : Dictionary of Trade instances.
1021
- - strategy_cls : Strategy class to use for trading.
1017
+ """
1018
+ Start an MT5 execution engine for a given account.
1019
+
1020
+ Parameters
1021
+ ----------
1022
+ account_id : str
1023
+ Account ID to run the execution engine on.
1024
+
1025
+ **kwargs : dict
1026
+ Additional keyword arguments. Possible keys include:
1027
+
1028
+ * symbol_list : list
1029
+ List of symbols to trade.
1030
+ * trades_instances : dict
1031
+ Dictionary of Trade instances.
1032
+ * strategy_cls : class
1033
+ Strategy class to use for trading.
1034
+
1035
+ Returns
1036
+ -------
1037
+ None
1038
+ Initializes and runs the MT5 execution engine.
1022
1039
  """
1023
1040
  log.info(f"Starting execution engine for {account_id}")
1024
1041