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

@@ -150,6 +150,7 @@ Orders = Literal[
150
150
  "sell_stop_limits",
151
151
  ]
152
152
 
153
+ EXPERT_ID = 98181105
153
154
 
154
155
  class Trade(RiskManagement):
155
156
  """
@@ -209,7 +210,7 @@ class Trade(RiskManagement):
209
210
  self,
210
211
  symbol: str = "EURUSD",
211
212
  expert_name: str = "bbstrader",
212
- expert_id: int = 9818,
213
+ expert_id: int = EXPERT_ID,
213
214
  version: str = "2.0",
214
215
  target: float = 5.0,
215
216
  start_time: str = "0:00",
@@ -376,21 +377,18 @@ class Trade(RiskManagement):
376
377
 
377
378
  def summary(self):
378
379
  """Show a brief description about the trading program"""
380
+ start = datetime.strptime(self.start, "%H:%M").time()
381
+ finish = datetime.strptime(self.finishing, "%H:%M").time()
382
+ end = datetime.strptime(self.end, "%H:%M").time()
379
383
  summary_data = [
380
384
  ["Expert Advisor Name", f"@{self.expert_name}"],
381
385
  ["Expert Advisor Version", f"@{self.version}"],
382
386
  ["Expert | Strategy ID", self.expert_id],
383
387
  ["Trading Symbol", self.symbol],
384
388
  ["Trading Time Frame", self.tf],
385
- ["Start Trading Time", f"{self.start_time_hour}:{self.start_time_minutes}"],
386
- [
387
- "Finishing Trading Time",
388
- f"{self.finishing_time_hour}:{self.finishing_time_minutes}",
389
- ],
390
- [
391
- "Closing Position After",
392
- f"{self.ending_time_hour}:{self.ending_time_minutes}",
393
- ],
389
+ ["Start Trading Time", f"{start}"],
390
+ ["Finishing Trading Time", f"{finish}"],
391
+ ["Closing Position After", f"{end}"],
394
392
  ]
395
393
  # Custom table format
396
394
  summary_table = tabulate(
@@ -520,7 +518,7 @@ class Trade(RiskManagement):
520
518
  tp: Optional[float] = None,
521
519
  ):
522
520
  """
523
- Open a Buy positin
521
+ Open a Buy position
524
522
 
525
523
  Args:
526
524
  action (str): `BMKT` for Market orders or `BLMT`,
@@ -533,6 +531,9 @@ class Trade(RiskManagement):
533
531
  mm (bool): Weither to put stop loss and tp or not
534
532
  trail (bool): Weither to trail the stop loss or not
535
533
  comment (str): The comment for the opening position
534
+ volume (float): The volume (lot) to trade
535
+ sl (float): The stop loss price
536
+ tp (float): The take profit price
536
537
  """
537
538
  Id = id if id is not None else self.expert_id
538
539
  point = self.get_symbol_info(self.symbol).point
@@ -582,7 +583,7 @@ class Trade(RiskManagement):
582
583
  return False
583
584
 
584
585
  def _order_type(self):
585
- type = {
586
+ return {
586
587
  "BMKT": (Mt5.ORDER_TYPE_BUY, "BUY"),
587
588
  "SMKT": (Mt5.ORDER_TYPE_BUY, "SELL"),
588
589
  "BLMT": (Mt5.ORDER_TYPE_BUY_LIMIT, "BUY_LIMIT"),
@@ -592,7 +593,6 @@ class Trade(RiskManagement):
592
593
  "BSTPLMT": (Mt5.ORDER_TYPE_BUY_STOP_LIMIT, "BUY_STOP_LIMIT"),
593
594
  "SSTPLMT": (Mt5.ORDER_TYPE_SELL_STOP_LIMIT, "SELL_STOP_LIMIT"),
594
595
  }
595
- return type
596
596
 
597
597
  def open_sell_position(
598
598
  self,
@@ -609,7 +609,7 @@ class Trade(RiskManagement):
609
609
  tp: Optional[float] = None,
610
610
  ):
611
611
  """
612
- Open a sell positin
612
+ Open a sell position
613
613
 
614
614
  Args:
615
615
  action (str): `SMKT` for Market orders
@@ -622,10 +622,9 @@ class Trade(RiskManagement):
622
622
  mm (bool): Weither to put stop loss and tp or not
623
623
  trail (bool): Weither to trail the stop loss or not
624
624
  comment (str): The comment for the closing position
625
- symbol (str): The symbol to trade
626
625
  volume (float): The volume (lot) to trade
627
- sl (float): The stop loss in points
628
- tp (float): The take profit in points
626
+ sl (float): The stop loss price
627
+ tp (float): The take profit price
629
628
  """
630
629
  Id = id if id is not None else self.expert_id
631
630
  point = self.get_symbol_info(self.symbol).point
@@ -691,11 +690,11 @@ class Trade(RiskManagement):
691
690
  LOGGER.info(f"Not Trading time, SYMBOL={self.symbol}")
692
691
  return False
693
692
  elif not self.is_risk_ok():
694
- LOGGER.error(f"Account Risk not allowed, SYMBOL={self.symbol}")
693
+ LOGGER.warning(f"Account Risk not allowed, SYMBOL={self.symbol}")
695
694
  self._check(comment)
696
695
  return False
697
696
  elif self.is_max_trades_reached():
698
- LOGGER.error(f"Maximum trades reached for Today, SYMBOL={self.symbol}")
697
+ LOGGER.warning(f"Maximum trades reached for Today, SYMBOL={self.symbol}")
699
698
  return False
700
699
  elif self.profit_target():
701
700
  self._check(f"Profit target Reached !!! SYMBOL={self.symbol}")
@@ -835,7 +834,8 @@ class Trade(RiskManagement):
835
834
  action (str): (`'BMKT'`, `'SMKT'`) for Market orders
836
835
  or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
837
836
  price (float): The price at which to open an order
838
- stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
837
+ stoplimit (float): A price a pending Limit order is set at
838
+ when the price reaches the 'price' value (this condition is mandatory).
839
839
  The pending order is not passed to the trading system until that moment
840
840
  id (int): The strategy id or expert Id
841
841
  mm (bool): Weither to put stop loss and tp or not
@@ -843,43 +843,32 @@ class Trade(RiskManagement):
843
843
  comment (str): The comment for the closing position
844
844
  symbol (str): The symbol to trade
845
845
  volume (float): The volume (lot) to trade
846
- sl (float): The stop loss in points
847
- tp (float): The take profit in points
846
+ sl (float): The stop loss price
847
+ tp (float): The take profit price
848
848
  """
849
849
  BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
850
850
  SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
851
851
  if action in BUYS:
852
- return self.open_buy_position(
853
- action=action,
854
- price=price,
855
- stoplimit=stoplimit,
856
- id=id,
857
- mm=mm,
858
- trail=trail,
859
- comment=comment,
860
- symbol=symbol,
861
- volume=volume,
862
- sl=sl,
863
- tp=tp,
864
- )
852
+ open_position = self.open_buy_position
865
853
  elif action in SELLS:
866
- return self.open_sell_position(
867
- action=action,
868
- price=price,
869
- stoplimit=stoplimit,
870
- id=id,
871
- mm=mm,
872
- trail=trail,
873
- comment=comment,
874
- symbol=symbol,
875
- volume=volume,
876
- sl=sl,
877
- tp=tp,
878
- )
854
+ open_position = self.open_sell_position
879
855
  else:
880
856
  raise ValueError(
881
857
  f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}"
882
858
  )
859
+ return open_position(
860
+ action=action,
861
+ price=price,
862
+ stoplimit=stoplimit,
863
+ id=id,
864
+ mm=mm,
865
+ trail=trail,
866
+ comment=comment,
867
+ symbol=symbol,
868
+ volume=volume,
869
+ sl=sl,
870
+ tp=tp,
871
+ )
883
872
 
884
873
  @property
885
874
  def orders(self):
@@ -1134,9 +1123,9 @@ class Trade(RiskManagement):
1134
1123
  be = self.get_break_even()
1135
1124
  if trail_after_points is not None:
1136
1125
  if isinstance(trail_after_points, int):
1137
- assert trail_after_points > be, (
1138
- "trail_after_points must be greater than break even or set to None"
1139
- )
1126
+ assert (
1127
+ trail_after_points > be
1128
+ ), "trail_after_points must be greater than break even or set to None"
1140
1129
  trail_after_points = self._get_trail_after_points(trail_after_points)
1141
1130
  if positions is not None:
1142
1131
  for position in positions:
@@ -1225,7 +1214,8 @@ class Trade(RiskManagement):
1225
1214
  Sets the break-even level for a given trading position.
1226
1215
 
1227
1216
  Args:
1228
- position (TradePosition): The trading position for which the break-even is to be set. This is the value return by `mt5.positions_get()`.
1217
+ position (TradePosition): The trading position for which the break-even is to be set.
1218
+ This is the value return by `mt5.positions_get()`.
1229
1219
  be (int): The break-even level in points.
1230
1220
  level (float): The break-even level in price, if set to None , it will be calated automaticaly.
1231
1221
  price (float): The break-even price, if set to None , it will be calated automaticaly.
@@ -1456,7 +1446,8 @@ class Trade(RiskManagement):
1456
1446
  Args:
1457
1447
  ticket (int): Order ticket to modify (e.g TradeOrder.ticket)
1458
1448
  price (float): The price at which to modify the order
1459
- stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
1449
+ stoplimit (float): A price a pending Limit order is set at
1450
+ when the price reaches the 'price' value (this condition is mandatory).
1460
1451
  The pending order is not passed to the trading system until that moment
1461
1452
  sl (float): The stop loss in points
1462
1453
  tp (float): The take profit in points
@@ -1606,7 +1597,8 @@ class Trade(RiskManagement):
1606
1597
  ):
1607
1598
  """
1608
1599
  Args:
1609
- order_type (str): Type of orders to close ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
1600
+ order_type (str): Type of orders to close
1601
+ ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
1610
1602
  id (int): The unique ID of the Expert or Strategy
1611
1603
  comment (str): Comment for the closing position
1612
1604
  """
@@ -1915,7 +1907,7 @@ def create_trade_instance(
1915
1907
  if ids is not None and isinstance(ids, (int, float))
1916
1908
  else params["expert_id"]
1917
1909
  if "expert_id" in params
1918
- else None
1910
+ else EXPERT_ID
1919
1911
  )
1920
1912
  params["pchange_sl"] = (
1921
1913
  pchange_sl[symbol]
@@ -16,6 +16,101 @@ __all__ = [
16
16
  "search_coint_candidate_pairs",
17
17
  ]
18
18
 
19
+ def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
20
+ """Download and process data for a list of tickers from the specified source."""
21
+ data_list = []
22
+ for ticker in tickers:
23
+ try:
24
+ if source == "yf":
25
+ data = yf.download(
26
+ ticker,
27
+ start=start,
28
+ end=end,
29
+ progress=False,
30
+ multi_level_index=False,
31
+ )
32
+ data = data.drop(columns=["Adj Close"], axis=1)
33
+ elif source == "mt5":
34
+ start, end = pd.Timestamp(start), pd.Timestamp(end)
35
+ data = download_historical_data(
36
+ symbol=ticker,
37
+ timeframe=tf,
38
+ date_from=start,
39
+ date_to=end,
40
+ **{"path": path},
41
+ )
42
+ data = data.drop(columns=["adj_close"], axis=1)
43
+ elif source in ["fmp", "eodhd"]:
44
+ handler_class = (
45
+ FMPDataHandler if source == "fmp" else EODHDataHandler
46
+ )
47
+ handler = handler_class(events=None, symbol_list=[ticker], **kwargs)
48
+ data = handler.data[ticker]
49
+ else:
50
+ raise ValueError(f"Invalid source: {source}")
51
+
52
+ data = data.reset_index()
53
+ data = data.rename(columns=str.lower)
54
+ data["ticker"] = ticker
55
+ data_list.append(data)
56
+
57
+ except Exception as e:
58
+ print(f"No Data found for {ticker}: {e}")
59
+ continue
60
+
61
+ return pd.concat(data_list)
62
+
63
+ def _handle_date_range(start, end, window):
64
+ """Handle start and end date generation."""
65
+ if start is None or end is None:
66
+ end = pd.Timestamp(datetime.now()).strftime("%Y-%m-%d")
67
+ start = (
68
+ pd.Timestamp(datetime.now())
69
+ - pd.DateOffset(years=window)
70
+ + pd.DateOffset(days=1)
71
+ ).strftime("%Y-%m-%d")
72
+ return start, end
73
+
74
+ def _period_search(start, end, securities, candidates, window, npairs):
75
+ if window < 3 or (pd.Timestamp(end) - pd.Timestamp(start)).days / 365 < 3:
76
+ raise ValueError(
77
+ "The date range must be at least two (2) years for period search."
78
+ )
79
+ top_pairs = []
80
+ p_start = pd.Timestamp(end) - pd.DateOffset(years=1)
81
+ periods = pd.date_range(start=p_start, end=pd.Timestamp(end), freq="BQE")
82
+ npairs = max(round(npairs / 2), 1)
83
+ for period in periods:
84
+ s_start = period - pd.DateOffset(years=2) + pd.DateOffset(days=1)
85
+ print(f"Searching for pairs in period: {s_start} - {period}")
86
+ pairs = find_cointegrated_pairs(
87
+ securities,
88
+ candidates,
89
+ n=npairs,
90
+ start=str(s_start),
91
+ stop=str(period),
92
+ coint=True,
93
+ )
94
+ pairs["period"] = period
95
+ top_pairs.append(pairs)
96
+ top_pairs = pd.concat(top_pairs)
97
+ if len(top_pairs.columns) <= 1:
98
+ raise ValueError(
99
+ "No pairs found in the specified period."
100
+ "Please adjust the date range or increase the number of pairs."
101
+ )
102
+ return top_pairs.head(npairs * 2)
103
+
104
+ def _process_asset_data(securities, candidates, universe, rolling_window):
105
+ """Process and select assets from the data."""
106
+ securities = select_assets(
107
+ securities, n=universe, rolling_window=rolling_window
108
+ )
109
+ candidates = select_assets(
110
+ candidates, n=universe, rolling_window=rolling_window
111
+ )
112
+ return securities, candidates
113
+
19
114
 
20
115
  def search_coint_candidate_pairs(
21
116
  securities: pd.DataFrame | List[str] = None,
@@ -145,101 +240,6 @@ def search_coint_candidate_pairs(
145
240
 
146
241
  """
147
242
 
148
- def _download_and_process_data(source, tickers, start, end, tf, path, **kwargs):
149
- """Download and process data for a list of tickers from the specified source."""
150
- data_list = []
151
- for ticker in tickers:
152
- try:
153
- if source == "yf":
154
- data = yf.download(
155
- ticker,
156
- start=start,
157
- end=end,
158
- progress=False,
159
- multi_level_index=False,
160
- )
161
- data = data.drop(columns=["Adj Close"], axis=1)
162
- elif source == "mt5":
163
- start, end = pd.Timestamp(start), pd.Timestamp(end)
164
- data = download_historical_data(
165
- symbol=ticker,
166
- timeframe=tf,
167
- date_from=start,
168
- date_to=end,
169
- **{"path": path},
170
- )
171
- data = data.drop(columns=["adj_close"], axis=1)
172
- elif source in ["fmp", "eodhd"]:
173
- handler_class = (
174
- FMPDataHandler if source == "fmp" else EODHDataHandler
175
- )
176
- handler = handler_class(events=None, symbol_list=[ticker], **kwargs)
177
- data = handler.data[ticker]
178
- else:
179
- raise ValueError(f"Invalid source: {source}")
180
-
181
- data = data.reset_index()
182
- data = data.rename(columns=str.lower)
183
- data["ticker"] = ticker
184
- data_list.append(data)
185
-
186
- except Exception as e:
187
- print(f"No Data found for {ticker}: {e}")
188
- continue
189
-
190
- return pd.concat(data_list)
191
-
192
- def _handle_date_range(start, end, window):
193
- """Handle start and end date generation."""
194
- if start is None or end is None:
195
- end = pd.Timestamp(datetime.now()).strftime("%Y-%m-%d")
196
- start = (
197
- pd.Timestamp(datetime.now())
198
- - pd.DateOffset(years=window)
199
- + pd.DateOffset(days=1)
200
- ).strftime("%Y-%m-%d")
201
- return start, end
202
-
203
- def _period_search(start, end, securities, candidates, npairs=npairs):
204
- if window < 3 or (pd.Timestamp(end) - pd.Timestamp(start)).days / 365 < 3:
205
- raise ValueError(
206
- "The date range must be at least two (2) years for period search."
207
- )
208
- top_pairs = []
209
- p_start = pd.Timestamp(end) - pd.DateOffset(years=1)
210
- periods = pd.date_range(start=p_start, end=pd.Timestamp(end), freq="BQE")
211
- npairs = max(round(npairs / 2), 1)
212
- for period in periods:
213
- s_start = period - pd.DateOffset(years=2) + pd.DateOffset(days=1)
214
- print(f"Searching for pairs in period: {s_start} - {period}")
215
- pairs = find_cointegrated_pairs(
216
- securities,
217
- candidates,
218
- n=npairs,
219
- start=str(s_start),
220
- stop=str(period),
221
- coint=True,
222
- )
223
- pairs["period"] = period
224
- top_pairs.append(pairs)
225
- top_pairs = pd.concat(top_pairs)
226
- if len(top_pairs.columns) <= 1:
227
- raise ValueError(
228
- "No pairs found in the specified period."
229
- "Please adjust the date range or increase the number of pairs."
230
- )
231
- return top_pairs.head(npairs * 2)
232
-
233
- def _process_asset_data(securities, candidates, universe, rolling_window):
234
- """Process and select assets from the data."""
235
- securities = select_assets(
236
- securities, n=universe, rolling_window=rolling_window
237
- )
238
- candidates = select_assets(
239
- candidates, n=universe, rolling_window=rolling_window
240
- )
241
- return securities, candidates
242
-
243
243
  if (
244
244
  securities is not None
245
245
  and candidates is not None
@@ -255,7 +255,7 @@ def search_coint_candidate_pairs(
255
255
  if period_search:
256
256
  start = securities.index.get_level_values("date").min()
257
257
  end = securities.index.get_level_values("date").max()
258
- top_pairs = _period_search(start, end, securities, candidates)
258
+ top_pairs = _period_search(start, end, securities, candidates, window, npairs)
259
259
  else:
260
260
  top_pairs = find_cointegrated_pairs(
261
261
  securities, candidates, n=npairs, coint=True
@@ -291,7 +291,7 @@ def search_coint_candidate_pairs(
291
291
  )
292
292
  if period_search:
293
293
  top_pairs = _period_search(
294
- start, end, securities_data, candidates_data
294
+ start, end, securities_data, candidates_data, window, npairs
295
295
  ).head(npairs)
296
296
  else:
297
297
  top_pairs = find_cointegrated_pairs(
bbstrader/models/nlp.py CHANGED
@@ -506,6 +506,7 @@ class SentimentAnalyzer(object):
506
506
  reddit_posts = news.get_reddit_posts(
507
507
  ticker, n_posts=top_news, **{k: kwargs.get(k) for k in rd_params}
508
508
  )
509
+ coindesk_news = news.get_coindesk_news(query=ticker, list_of_str=True)
509
510
  fmp_source_news = []
510
511
  fmp_news = news.get_fmp_news(kwargs.get("fmp_api"))
511
512
  for source in ["articles"]: # , "releases", asset_type]:
@@ -518,7 +519,7 @@ class SentimentAnalyzer(object):
518
519
  source_news = []
519
520
  if any([len(s) > 0 for s in [yahoo_news, google_news]]):
520
521
  sources += 1
521
- for source in [reddit_posts, fmp_source_news]:
522
+ for source in [reddit_posts, fmp_source_news, coindesk_news]:
522
523
  if len(source) > 0:
523
524
  sources += 1
524
525
  # Compute sentiment
@@ -531,11 +532,17 @@ class SentimentAnalyzer(object):
531
532
  fmp_sentiment = self.analyze_sentiment(
532
533
  fmp_source_news, lexicon=lexicon, textblob=True
533
534
  )
535
+ coindesk_sentiment = self.analyze_sentiment(
536
+ coindesk_news, lexicon=lexicon, textblob=True
537
+ )
534
538
 
535
539
  # Weighted average sentiment score
536
540
  if sources != 0:
537
541
  overall_sentiment = (
538
- news_sentiment + reddit_sentiment + fmp_sentiment
542
+ news_sentiment
543
+ + reddit_sentiment
544
+ + fmp_sentiment
545
+ + coindesk_sentiment
539
546
  ) / sources
540
547
  else:
541
548
  overall_sentiment = 0.0