bbstrader 2.0.3__cp312-cp312-macosx_11_0_arm64.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.
Files changed (45) hide show
  1. bbstrader/__init__.py +27 -0
  2. bbstrader/__main__.py +92 -0
  3. bbstrader/api/__init__.py +96 -0
  4. bbstrader/api/handlers.py +245 -0
  5. bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
  6. bbstrader/api/metatrader_client.pyi +624 -0
  7. bbstrader/assets/bbs_.png +0 -0
  8. bbstrader/assets/bbstrader.ico +0 -0
  9. bbstrader/assets/bbstrader.png +0 -0
  10. bbstrader/assets/qs_metrics_1.png +0 -0
  11. bbstrader/btengine/__init__.py +54 -0
  12. bbstrader/btengine/backtest.py +358 -0
  13. bbstrader/btengine/data.py +737 -0
  14. bbstrader/btengine/event.py +229 -0
  15. bbstrader/btengine/execution.py +287 -0
  16. bbstrader/btengine/performance.py +408 -0
  17. bbstrader/btengine/portfolio.py +393 -0
  18. bbstrader/btengine/strategy.py +588 -0
  19. bbstrader/compat.py +28 -0
  20. bbstrader/config.py +100 -0
  21. bbstrader/core/__init__.py +27 -0
  22. bbstrader/core/data.py +628 -0
  23. bbstrader/core/strategy.py +466 -0
  24. bbstrader/metatrader/__init__.py +48 -0
  25. bbstrader/metatrader/_copier.py +720 -0
  26. bbstrader/metatrader/account.py +865 -0
  27. bbstrader/metatrader/broker.py +418 -0
  28. bbstrader/metatrader/copier.py +1487 -0
  29. bbstrader/metatrader/rates.py +495 -0
  30. bbstrader/metatrader/risk.py +667 -0
  31. bbstrader/metatrader/trade.py +1692 -0
  32. bbstrader/metatrader/utils.py +402 -0
  33. bbstrader/models/__init__.py +39 -0
  34. bbstrader/models/nlp.py +932 -0
  35. bbstrader/models/optimization.py +182 -0
  36. bbstrader/scripts.py +665 -0
  37. bbstrader/trading/__init__.py +33 -0
  38. bbstrader/trading/execution.py +1159 -0
  39. bbstrader/trading/strategy.py +362 -0
  40. bbstrader/trading/utils.py +69 -0
  41. bbstrader-2.0.3.dist-info/METADATA +396 -0
  42. bbstrader-2.0.3.dist-info/RECORD +45 -0
  43. bbstrader-2.0.3.dist-info/WHEEL +5 -0
  44. bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
  45. bbstrader-2.0.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,362 @@
1
+ from datetime import datetime
2
+ from enum import IntEnum
3
+ from queue import Queue
4
+ from typing import Any, Callable, Dict, List, Optional, Union
5
+ from abc import abstractmethod
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ from loguru import logger
10
+
11
+ from bbstrader.api.metatrader_client import TradeOrder # type: ignore
12
+ from bbstrader.btengine.event import FillEvent, SignalEvent
13
+ from bbstrader.config import BBSTRADER_DIR
14
+ from bbstrader.core.strategy import (
15
+ BaseStrategy,
16
+ TradeAction,
17
+ TradeSignal,
18
+ TradingMode,
19
+ generate_signal,
20
+ )
21
+ from bbstrader.metatrader import Account, Rates
22
+
23
+ logger.add(
24
+ f"{BBSTRADER_DIR}/logs/strategy.log",
25
+ enqueue=True,
26
+ level="INFO",
27
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
28
+ )
29
+
30
+ __all__ = [
31
+ "LiveStrategy",
32
+ ]
33
+
34
+
35
+ class SignalType(IntEnum):
36
+ BUY = 0
37
+ SELL = 1
38
+ EXIT_LONG = 2
39
+ EXIT_SHORT = 3
40
+ EXIT_ALL_POSITIONS = 4
41
+ EXIT_ALL_ORDERS = 5
42
+ EXIT_STOP = 6
43
+ EXIT_LIMIT = 7
44
+
45
+
46
+ class LiveStrategy(BaseStrategy):
47
+ """
48
+ Strategy implementation for Live Trading.
49
+ Relies on the `Account` class for state (orders, positions, cash)
50
+ and `Rates` for data.
51
+ """
52
+
53
+ events: "Queue[Union[SignalEvent, FillEvent]]"
54
+
55
+ def __init__(
56
+ self,
57
+ symbol_list: List[str],
58
+ **kwargs: Any,
59
+ ) -> None:
60
+ """
61
+ Initialize the `LiveStrategy` object.
62
+
63
+ Args:
64
+ symbol_list : The list of symbols for the strategy.
65
+ **kwargs : Additional keyword arguments for other classes (e.g, Portfolio, ExecutionHandler).
66
+ - max_trades : The maximum number of trades allowed per symbol.
67
+ - time_frame : The time frame for the strategy.
68
+ - logger : The logger object for the strategy.
69
+ """
70
+ super().__init__(symbol_list, **kwargs)
71
+ self.mode = TradingMode.LIVE
72
+
73
+ @property
74
+ def account(self) -> Account:
75
+ """Create or access the MT5 Account."""
76
+ return Account(**self.kwargs)
77
+
78
+ @property
79
+ def cash(self) -> float:
80
+ return self.account.equity
81
+
82
+ @property
83
+ def orders(self) -> List[TradeOrder]:
84
+ """Returns active orders from the Broker."""
85
+ return self.account.get_orders() or []
86
+
87
+ @property
88
+ def positions(self) -> List[Any]:
89
+ """Returns open positions from the Broker."""
90
+ return self.account.get_positions() or []
91
+
92
+ def get_asset_values(
93
+ self,
94
+ symbol_list: List[str],
95
+ window: int,
96
+ value_type: str = "returns",
97
+ array: bool = True,
98
+ **kwargs,
99
+ ) -> Optional[Dict[str, Union[np.ndarray, pd.Series]]]:
100
+ asset_values: Dict[str, Union[np.ndarray, pd.Series]] = {}
101
+ for asset in symbol_list:
102
+ rates = Rates(asset, timeframe=self.tf, count=window + 1, **self.kwargs)
103
+ if array:
104
+ values = getattr(rates, value_type).to_numpy()
105
+ asset_values[asset] = values[~np.isnan(values)]
106
+ else:
107
+ values = getattr(rates, value_type)
108
+ asset_values[asset] = values
109
+
110
+ if all(len(values) >= window for values in asset_values.values()):
111
+ return {a: v[-window:] for a, v in asset_values.items()}
112
+
113
+ if kwargs.get("error") == "raise":
114
+ raise ValueError("Not enough data to calculate the values.")
115
+ elif kwargs.get("error") == "ignore":
116
+ return asset_values
117
+ return None
118
+
119
+ def signal(
120
+ self, signal: int, symbol: str, sl: float = None, tp: float = None
121
+ ) -> TradeSignal:
122
+ """
123
+ Generate a ``TradeSignal`` object based on the signal value.
124
+
125
+ Parameters
126
+ ----------
127
+ signal : int
128
+ An integer value representing the signal type:
129
+ * 0: BUY
130
+ * 1: SELL
131
+ * 2: EXIT_LONG
132
+ * 3: EXIT_SHORT
133
+ * 4: EXIT_ALL_POSITIONS
134
+ * 5: EXIT_ALL_ORDERS
135
+ * 6: EXIT_STOP
136
+ * 7: EXIT_LIMIT
137
+ symbol : str
138
+ The symbol for the trade.
139
+
140
+ Returns
141
+ -------
142
+ TradeSignal
143
+ A ``TradeSignal`` object representing the trade signal.
144
+
145
+ Raises
146
+ ------
147
+ ValueError
148
+ If the signal value is not between 0 and 7.
149
+
150
+ Notes
151
+ -----
152
+ This generates only common signals. For more complex signals, use
153
+ ``generate_signal`` directly.
154
+ """
155
+ signal_id = getattr(self, "id", getattr(self, "ID", None))
156
+ if signal_id is None:
157
+ raise ValueError("Strategy ID not set")
158
+
159
+ action_map = {
160
+ SignalType.BUY: TradeAction.BUY,
161
+ SignalType.SELL: TradeAction.SELL,
162
+ SignalType.EXIT_LONG: TradeAction.EXIT_LONG,
163
+ SignalType.EXIT_SHORT: TradeAction.EXIT_SHORT,
164
+ SignalType.EXIT_ALL_POSITIONS: TradeAction.EXIT_ALL_POSITIONS,
165
+ SignalType.EXIT_ALL_ORDERS: TradeAction.EXIT_ALL_ORDERS,
166
+ SignalType.EXIT_STOP: TradeAction.EXIT_STOP,
167
+ SignalType.EXIT_LIMIT: TradeAction.EXIT_LIMIT,
168
+ }
169
+
170
+ try:
171
+ action = action_map[SignalType(signal)]
172
+ except (ValueError, KeyError):
173
+ raise ValueError(f"Invalid signal value: {signal}")
174
+ kwargs = (
175
+ {"sl": sl, "tp": tp}
176
+ if action in (TradeAction.BUY, TradeAction.SELL)
177
+ else {}
178
+ )
179
+
180
+ return generate_signal(signal_id, symbol, action, **kwargs)
181
+
182
+ @abstractmethod
183
+ def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal]: ...
184
+
185
+ def ispositions(
186
+ self,
187
+ symbol: str,
188
+ strategy_id: int,
189
+ position: int,
190
+ max_trades: int,
191
+ one_true: bool = False,
192
+ ) -> bool:
193
+ """
194
+ This function is use for live trading to check if there are open positions
195
+ for a given symbol and strategy. It is used to prevent opening more trades
196
+ than the maximum allowed trades per symbol.
197
+
198
+ Args:
199
+ symbol : The symbol for the trade.
200
+ strategy_id : The unique identifier for the strategy.
201
+ position : The position type (1: short, 0: long).
202
+ max_trades : The maximum number of trades allowed per symbol.
203
+ one_true : If True, return True if there is at least one open position.
204
+ account : The `bbstrader.metatrader.Account` object for the strategy.
205
+
206
+ Returns:
207
+ bool : True if there are open positions, False otherwise
208
+ """
209
+ positions = self.account.get_positions(symbol=symbol)
210
+ if positions is not None:
211
+ open_positions = [
212
+ pos.ticket
213
+ for pos in positions
214
+ if pos.type == position and pos.magic == strategy_id
215
+ ]
216
+ if one_true:
217
+ return len(open_positions) in range(1, max_trades + 1)
218
+ return len(open_positions) >= max_trades
219
+ return False
220
+
221
+ def get_positions_prices(
222
+ self,
223
+ symbol: str,
224
+ strategy_id: int,
225
+ position: int,
226
+ ) -> np.ndarray:
227
+ """
228
+ Get the buy or sell prices for open positions of a given symbol and strategy.
229
+
230
+ Args:
231
+ symbol : The symbol for the trade.
232
+ strategy_id : The unique identifier for the strategy.
233
+ position : The position type (1: short, 0: long).
234
+ account : The `bbstrader.metatrader.Account` object for the strategy.
235
+
236
+ Returns:
237
+ prices : numpy array of buy or sell prices for open positions if any or an empty array.
238
+ """
239
+ positions = self.account.get_positions(symbol=symbol)
240
+ if positions is not None:
241
+ prices = np.array(
242
+ [
243
+ pos.price_open
244
+ for pos in positions
245
+ if pos.type == position and pos.magic == strategy_id
246
+ ]
247
+ )
248
+ return prices
249
+ return np.array([])
250
+
251
+ def get_active_orders(
252
+ self, symbol: str, strategy_id: int, order_type: Optional[int] = None
253
+ ) -> List[TradeOrder]:
254
+ """
255
+ Get the active orders for a given symbol and strategy.
256
+
257
+ Args:
258
+ symbol : The symbol for the trade.
259
+ strategy_id : The unique identifier for the strategy.
260
+ order_type : The type of order to filter by (optional):
261
+ "BUY_LIMIT": 2
262
+ "SELL_LIMIT": 3
263
+ "BUY_STOP": 4
264
+ "SELL_STOP": 5
265
+ "BUY_STOP_LIMIT": 6
266
+ "SELL_STOP_LIMIT": 7
267
+
268
+ Returns:
269
+ List[TradeOrder] : A list of active orders for the given symbol and strategy.
270
+ """
271
+ all_orders = self.orders
272
+ orders = [
273
+ o
274
+ for o in all_orders
275
+ if isinstance(o, TradeOrder)
276
+ and o.symbol == symbol
277
+ and o.magic == strategy_id
278
+ ]
279
+ if order_type is not None and len(orders) > 0:
280
+ orders = [o for o in orders if o.type == order_type]
281
+ return orders
282
+
283
+ def exit_positions(
284
+ self, position: int, prices: np.ndarray, asset: str, th: float = 0.01
285
+ ) -> bool:
286
+ """Logic to determine if positions should be exited based on threshold."""
287
+ if len(prices) == 0:
288
+ return False
289
+ tick_info = self.account.get_tick_info(asset)
290
+ if tick_info is None:
291
+ return False
292
+ bid, ask = tick_info.bid, tick_info.ask
293
+ price = None
294
+ if len(prices) == 1:
295
+ price = prices[0]
296
+ elif len(prices) in range(2, self.max_trades[asset] + 1):
297
+ price = np.mean(prices)
298
+
299
+ if price is not None:
300
+ if position == 0: # Long exit check
301
+ return self.calculate_pct_change(ask, price) >= th
302
+ elif position == 1: # Short exit check
303
+ return self.calculate_pct_change(bid, price) <= -th
304
+ return False
305
+
306
+ def send_trade_report(self, perf_analyzer: Callable, **kwargs: Any) -> None:
307
+ """
308
+ Generates and sends a trade report message containing performance metrics for the current strategy.
309
+ This method retrieves the trade history for the current account, filters it by the strategy's ID,
310
+ computes performance metrics using the provided `perf_analyzer` callable, and formats the results
311
+ into a message. The message includes account information, strategy details, a timestamp, and
312
+ performance metrics. The message is then sent via Telegram using the specified bot token and chat ID.
313
+
314
+ Args:
315
+ perf_analyzer (Callable): A function or callable object that takes the filtered trade history
316
+ (as a DataFrame) and additional keyword arguments, and returns a DataFrame of performance metrics.
317
+ **kwargs: Additional keyword arguments, which may include
318
+ - Any other param requires by ``perf_analyzer``
319
+ """
320
+ from bbstrader.trading.utils import send_message
321
+
322
+ history = self.account.get_trades_history()
323
+ if history is None or history.empty:
324
+ self.logger.warning("No trades found on this account.")
325
+ return
326
+
327
+ ID = getattr(self, "id", None) or getattr(self, "ID")
328
+ history = history[history["magic"] == ID]
329
+ performance = perf_analyzer(history, **kwargs)
330
+ if performance.empty:
331
+ self.logger.warning("No trades found for the current strategy.")
332
+ return
333
+
334
+ account_name = self.kwargs.get("account", "MT5 Account")
335
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
336
+
337
+ header = (
338
+ f"TRADE REPORT\n\n"
339
+ f"ACCOUNT: {account_name}\n"
340
+ f"STRATEGY: {self.NAME}\n"
341
+ f"ID: {ID}\n"
342
+ f"DESCRIPTION: {self.DESCRIPTION}\n"
343
+ f"TIMESTAMP: {timestamp}\n\n"
344
+ f"📊 PERFORMANCE:\n"
345
+ )
346
+ metrics = performance.iloc[0].to_dict()
347
+
348
+ lines = []
349
+ for key, value in metrics.items():
350
+ if isinstance(value, float):
351
+ value = round(value, 4)
352
+ lines.append(f"{key:<15}: {value}")
353
+
354
+ performance_str = "\n".join(lines)
355
+ message = f"{header}{performance_str}"
356
+
357
+ send_message(
358
+ message=message,
359
+ telegram=True,
360
+ token=self.kwargs.get("bot_token"),
361
+ chat_id=self.kwargs.get("chat_id"),
362
+ )
@@ -0,0 +1,69 @@
1
+ import asyncio
2
+
3
+ from notifypy import Notify
4
+ from telegram import Bot
5
+ from telegram.error import TelegramError
6
+
7
+
8
+ __all__ = ["send_telegram_message", "send_notification", "send_message"]
9
+
10
+
11
+ async def send_telegram_message(token, chat_id, text=""):
12
+ """
13
+ Send a message to a telegram chat
14
+
15
+ Args:
16
+ token: str: Telegram bot token
17
+ chat_id: int or str or list: Chat id or list of chat ids
18
+ text: str: Message to send
19
+ """
20
+ try:
21
+ bot = Bot(token=token)
22
+ if isinstance(chat_id, (int, str)):
23
+ chat_id = [chat_id]
24
+ for id in chat_id:
25
+ await bot.send_message(chat_id=id, text=text)
26
+ except TelegramError as e:
27
+ print(f"Error sending message: {e}")
28
+
29
+
30
+ def send_notification(title, message=""):
31
+ """
32
+ Send a desktop notification
33
+
34
+ Args:
35
+ title: str: Title of the notification
36
+ message: str: Message of the notification
37
+ """
38
+ notification = Notify(default_notification_application_name="bbstrading")
39
+ notification.title = title
40
+ notification.message = message
41
+ notification.send()
42
+
43
+
44
+ def send_message(
45
+ title="SIGNAL",
46
+ message="New signal",
47
+ notify_me=False,
48
+ telegram=False,
49
+ token=None,
50
+ chat_id=None,
51
+ ):
52
+ """
53
+ Send a message to the user
54
+
55
+ Args:
56
+ title: str: Title of the message
57
+ message: str: Message of the message
58
+ notify_me: bool: Send a desktop notification
59
+ telegram: bool: Send a telegram message
60
+ token: str: Telegram bot token
61
+ chat_id: int or str or list: Chat id or list of chat ids
62
+ """
63
+ if notify_me:
64
+ send_notification(title, message=message)
65
+ if telegram:
66
+ if token is None or chat_id is None:
67
+ raise ValueError("Token and chat_id must be provided")
68
+ asyncio.run(send_telegram_message(token, chat_id, text=message))
69
+