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,466 @@
1
+ from abc import ABCMeta, abstractmethod
2
+ from dataclasses import dataclass
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, Dict, List, Optional, Union
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import pytz
10
+ from loguru import logger
11
+
12
+ from bbstrader.config import BBSTRADER_DIR
13
+ from bbstrader.models.optimization import optimized_weights
14
+
15
+ logger.add(
16
+ f"{BBSTRADER_DIR}/logs/strategy.log",
17
+ enqueue=True,
18
+ level="INFO",
19
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
20
+ )
21
+
22
+
23
+ __all__ = [
24
+ "TradeAction",
25
+ "TradeSignal",
26
+ "TradingMode",
27
+ "generate_signal",
28
+ "Strategy",
29
+ ]
30
+
31
+
32
+ class TradeAction(Enum):
33
+ """
34
+ An enumeration class for trade actions.
35
+ """
36
+
37
+ BUY = "LONG"
38
+ SELL = "SHORT"
39
+ LONG = "LONG"
40
+ SHORT = "SHORT"
41
+ BMKT = "BMKT"
42
+ SMKT = "SMKT"
43
+ BLMT = "BLMT"
44
+ SLMT = "SLMT"
45
+ BSTP = "BSTP"
46
+ SSTP = "SSTP"
47
+ BSTPLMT = "BSTPLMT"
48
+ SSTPLMT = "SSTPLMT"
49
+ EXIT = "EXIT"
50
+ EXIT_LONG = "EXIT_LONG"
51
+ EXIT_SHORT = "EXIT_SHORT"
52
+ EXIT_STOP = "EXIT_STOP"
53
+ EXIT_LIMIT = "EXIT_LIMIT"
54
+ EXIT_LONG_STOP = "EXIT_LONG_STOP"
55
+ EXIT_LONG_LIMIT = "EXIT_LONG_LIMIT"
56
+ EXIT_SHORT_STOP = "EXIT_SHORT_STOP"
57
+ EXIT_SHORT_LIMIT = "EXIT_SHORT_LIMIT"
58
+ EXIT_LONG_STOP_LIMIT = "EXIT_LONG_STOP_LIMIT"
59
+ EXIT_SHORT_STOP_LIMIT = "EXIT_SHORT_STOP_LIMIT"
60
+ EXIT_PROFITABLES = "EXIT_PROFITABLES"
61
+ EXIT_LOSINGS = "EXIT_LOSINGS"
62
+ EXIT_ALL_POSITIONS = "EXIT_ALL_POSITIONS"
63
+ EXIT_ALL_ORDERS = "EXIT_ALL_ORDERS"
64
+
65
+ def __str__(self):
66
+ return self.value
67
+
68
+
69
+ @dataclass()
70
+ class TradeSignal:
71
+ """
72
+ Represents a trading signal generated by a trading system or strategy.
73
+
74
+ Notes
75
+ -----
76
+ Attributes:
77
+
78
+ - id (int): A unique identifier for the trade signal or the strategy.
79
+ - symbol (str): The trading symbol (e.g., stock ticker, forex pair, crypto asset).
80
+ - action (TradeAction): The trading action to perform. Must be an instance of the ``TradeAction`` enum (e.g., BUY, SELL).
81
+ - price (float, optional): The price at which the trade should be executed.
82
+ - stoplimit (float, optional): A stop-limit price for the trade. Must not be set without specifying a price.
83
+ - sl (float, optional): A stop loss price for the trade.
84
+ - tp (float, optional): A take profit price for the trade.
85
+ - comment (str, optional): An optional comment or description related to the trade signal.
86
+ """
87
+
88
+ id: int
89
+ symbol: str
90
+ action: TradeAction
91
+ price: float = None
92
+ stoplimit: float = None
93
+ sl: float = None
94
+ tp: float = None
95
+ comment: str = None
96
+
97
+ def __post_init__(self):
98
+ if not isinstance(self.action, TradeAction):
99
+ raise TypeError(
100
+ f"action must be of type TradeAction, not {type(self.action)}"
101
+ )
102
+ if self.stoplimit is not None and self.price is None:
103
+ raise ValueError("stoplimit cannot be set without price")
104
+
105
+ def __repr__(self):
106
+ return (
107
+ f"TradeSignal(id={self.id}, symbol='{self.symbol}', action='{self.action.value}', "
108
+ f"price={self.price}, stoplimit={self.stoplimit}, sl={self.sl}, tp={self.tp}, comment='{self.comment or ''}')"
109
+ )
110
+
111
+
112
+ def generate_signal(
113
+ id: int,
114
+ symbol: str,
115
+ action: TradeAction,
116
+ price: float = None,
117
+ stoplimit: float = None,
118
+ sl: float = None,
119
+ tp: float = None,
120
+ comment: str = None,
121
+ ) -> TradeSignal:
122
+ """
123
+ Generates a trade signal for MetaTrader 5.
124
+
125
+ Args:
126
+ id (int): Unique identifier for the trade signal.
127
+ symbol (str): The symbol for which the trade signal is generated.
128
+ action (TradeAction): The action to be taken (e.g., BUY, SELL).
129
+ price (float, optional): The price at which to execute the trade.
130
+ stoplimit (float, optional): The stop limit price for the trade.
131
+ sl (float, optional): The stop loss price for the trade.
132
+ tp (float, optional): The take profit price for the trade.
133
+ comment (str, optional): Additional comments for the trade.
134
+
135
+ Returns:
136
+ TradeSignal: A TradeSignal object containing the details of the trade signal.
137
+ """
138
+ return TradeSignal(
139
+ id=id,
140
+ symbol=symbol,
141
+ action=action,
142
+ price=price,
143
+ stoplimit=stoplimit,
144
+ sl=sl,
145
+ tp=tp,
146
+ comment=comment,
147
+ )
148
+
149
+
150
+ class TradingMode(Enum):
151
+ BACKTEST = "BACKTEST"
152
+ LIVE = "LIVE"
153
+
154
+ def isbacktest(self) -> bool:
155
+ return self == TradingMode.BACKTEST
156
+
157
+ def islive(self) -> bool:
158
+ return self == TradingMode.LIVE
159
+
160
+
161
+ class Strategy(metaclass=ABCMeta):
162
+ """
163
+ A `Strategy()` object encapsulates all calculation on market data
164
+ that generate advisory signals to a `Portfolio` object. Thus all of
165
+ the "strategy logic" resides within this class. We opted to separate
166
+ out the `Strategy` and `Portfolio` objects for this backtester,
167
+ since we believe this is more amenable to the situation of multiple
168
+ strategies feeding "ideas" to a larger `Portfolio`, which then can handle
169
+ its own risk (such as sector allocation, leverage). In higher frequency trading,
170
+ the strategy and portfolio concepts will be tightly coupled and extremely
171
+ hardware dependent.
172
+
173
+ At this stage in the event-driven backtester development there is no concept of
174
+ an indicator or filter, such as those found in technical trading. These are also
175
+ good candidates for creating a class hierarchy.
176
+
177
+ The strategy hierarchy is relatively simple as it consists of an abstract
178
+ base class with a single pure virtual method for generating `SignalEvent` objects.
179
+ Other methods are provided to check for pending orders, update trades from fills,
180
+ and get updates from the portfolio.
181
+ """
182
+
183
+ @abstractmethod
184
+ def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal] | None:
185
+ raise NotImplementedError("Should implement calculate_signals()")
186
+
187
+ def check_pending_orders(self, *args: Any, **kwargs: Any) -> None: ...
188
+ def get_update_from_portfolio(self, *args: Any, **kwargs: Any) -> None: ...
189
+ def update_trades_from_fill(self, *args: Any, **kwargs: Any) -> None: ...
190
+ def perform_period_end_checks(self, *args: Any, **kwargs: Any) -> None: ...
191
+
192
+
193
+ class BaseStrategy(Strategy):
194
+ """
195
+ Base class containing shared logic for both Backtest and Live MT5 strategies.
196
+ This class handles configuration, logging, and common utility calculations.
197
+ """
198
+
199
+ tf: str
200
+ id: int
201
+ ID: int
202
+ max_trades: Dict[str, int]
203
+ risk_budget: Optional[Union[Dict[str, float], str]]
204
+ symbols: List[str]
205
+ logger: "logger" # type: ignore
206
+ kwargs: Dict[str, Any]
207
+ periodes: int
208
+ NAME: str
209
+ DESCRIPTION: str
210
+
211
+ def __init__(
212
+ self,
213
+ symbol_list: List[str],
214
+ **kwargs: Any,
215
+ ) -> None:
216
+ self.symbols = symbol_list
217
+ self.risk_budget = self._check_risk_budget(**kwargs)
218
+ self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
219
+ self.tf = kwargs.get("time_frame", "D1")
220
+ self.logger = kwargs.get("logger") or logger
221
+ self.kwargs = kwargs
222
+ self.periodes = 0
223
+
224
+ def _check_risk_budget(
225
+ self, **kwargs: Any
226
+ ) -> Optional[Union[Dict[str, float], str]]:
227
+ weights = kwargs.get("risk_weights")
228
+ if weights is not None and isinstance(weights, dict):
229
+ for asset in self.symbols:
230
+ if asset not in weights:
231
+ raise ValueError(f"Risk budget for asset {asset} is missing.")
232
+ total_risk = float(round(sum(weights.values())))
233
+ if not np.isclose(total_risk, 1.0):
234
+ raise ValueError(f"Risk budget weights must sum to 1. got {total_risk}")
235
+ return weights
236
+ elif isinstance(weights, str):
237
+ return weights
238
+ return None
239
+
240
+ @property
241
+ @abstractmethod
242
+ def cash(self) -> float:
243
+ """Returns the available cash (virtual or real)."""
244
+ raise NotImplementedError
245
+
246
+ def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal] | None:
247
+ """
248
+ Provides the mechanisms to calculate signals for the strategy.
249
+ This methods should return a list of signals for the strategy For Live mode and None For Backtest mode.
250
+
251
+ Each signal must be a ``TradeSignal`` object with the following attributes:
252
+ - ``id``: The unique identifier for the strategy or order.
253
+ - ``action``: The order to execute on the symbol (LONG, SHORT, EXIT, etc.), see `bbstrader.core.utils.TradeAction`.
254
+ - ``symbol``: The trading symbol (e.g., stock ticker, forex pair, crypto asset).
255
+ - See ``bbstrader.core.strategy.TradeSignal`` for other optionnal arguments.
256
+ """
257
+ raise NotImplementedError("Should implement calculate_signals()")
258
+
259
+ def perform_period_end_checks(self, *args: Any, **kwargs: Any) -> None:
260
+ """
261
+ Some strategies may require additional checks at the end of the period,
262
+ such as closing all positions or orders or tracking the performance of the strategy etc.
263
+
264
+ This method is called at the end of the period to perform such checks.
265
+ """
266
+ pass
267
+
268
+ @abstractmethod
269
+ def get_asset_values(
270
+ self,
271
+ symbol_list: List[str],
272
+ window: int,
273
+ value_type: str = "returns",
274
+ array: bool = True,
275
+ **kwargs,
276
+ ) -> Optional[Dict[str, Union[np.ndarray, pd.Series]]]:
277
+ """
278
+ Get the historical OHLCV value or returns or custum value
279
+ based on the DataHandker of the assets in the symbol list.
280
+
281
+ Args:
282
+ bars : DataHandler for market data handling, required for backtest mode.
283
+ symbol_list : List of ticker symbols for the pairs trading strategy.
284
+ value_type : The type of value to get (e.g., returns, open, high, low, close, adjclose, volume).
285
+ array : If True, return the values as numpy arrays, otherwise as pandas Series.
286
+ mode : Mode of operation for the strategy.
287
+ window : The lookback period for resquesting the data.
288
+ tf : The time frame for the strategy.
289
+ error : The error handling method for the function.
290
+
291
+ Returns:
292
+ asset_values : Historical values of the assets in the symbol list.
293
+
294
+ Note:
295
+ In Live mode, the `bbstrader.metatrader.rates.Rates` class is used to get the historical data
296
+ so the value_type must be 'returns', 'open', 'high', 'low', 'close', 'adjclose', 'volume'.
297
+ """
298
+ raise NotImplementedError
299
+
300
+ def apply_risk_management(
301
+ self,
302
+ optimizer: str,
303
+ symbols: Optional[List[str]] = None,
304
+ freq: int = 252,
305
+ ) -> Optional[Dict[str, float]]:
306
+ """Apply risk management optimization."""
307
+ if optimizer is None:
308
+ return None
309
+ symbols = symbols or self.symbols
310
+
311
+ prices = self.get_asset_values(
312
+ symbol_list=symbols,
313
+ window=freq,
314
+ value_type="close",
315
+ array=False,
316
+ )
317
+
318
+ if prices is None:
319
+ return None
320
+ prices = pd.DataFrame(prices)
321
+ prices = prices.dropna(axis=0, how="any")
322
+ try:
323
+ weights = optimized_weights(prices=prices, freq=freq, method=optimizer)
324
+ return {symbol: abs(weight) for symbol, weight in weights.items()}
325
+ except Exception:
326
+ return {symbol: 0.0 for symbol in symbols}
327
+
328
+ def get_quantity(
329
+ self,
330
+ symbol: str,
331
+ weight: float,
332
+ price: Optional[float] = None,
333
+ volume: Optional[float] = None,
334
+ maxqty: Optional[int] = None,
335
+ ) -> int:
336
+ """
337
+ Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
338
+ The quantity calculated can be used to evalute a strategy's performance for each symbol
339
+ given the fact that the dollar value is the same for all symbols.
340
+
341
+ Args:
342
+ symbol : The symbol for the trade.
343
+
344
+ Returns:
345
+ qty : The quantity to buy or sell for the symbol.
346
+ """
347
+ current_cash = self.cash
348
+
349
+ if (
350
+ current_cash is None
351
+ or weight == 0
352
+ or current_cash == 0
353
+ or np.isnan(current_cash)
354
+ ):
355
+ return 0
356
+ if price is None:
357
+ vals = self.get_asset_values(
358
+ [symbol], window=1, value_type="close", array=True
359
+ )
360
+ if vals and symbol in vals and len(vals[symbol]) > 0:
361
+ price = float(vals[symbol][-1])
362
+ else:
363
+ price = None
364
+
365
+ if volume is None:
366
+ volume = round(current_cash * weight)
367
+
368
+ if (
369
+ price is None
370
+ or not isinstance(price, (int, float, np.number))
371
+ or volume is None
372
+ or not isinstance(volume, (int, float, np.number))
373
+ or np.isnan(float(price))
374
+ or np.isnan(float(volume))
375
+ ):
376
+ if weight != 0:
377
+ return 1
378
+ return 0
379
+
380
+ qty = round(volume / price, 2)
381
+ qty = max(qty, 0) / self.max_trades.get(symbol, 1)
382
+ if maxqty is not None:
383
+ qty = min(qty, maxqty)
384
+ return int(max(round(qty, 2), 0))
385
+
386
+ def get_quantities(
387
+ self, quantities: Optional[Union[Dict[str, int], int]]
388
+ ) -> Dict[str, Optional[int]]:
389
+ """
390
+ Get the quantities to buy or sell for the symbols in the strategy.
391
+ This method is used when whe need to assign different quantities to the symbols.
392
+
393
+ Args:
394
+ quantities : The quantities for the symbols in the strategy.
395
+ """
396
+ if quantities is None:
397
+ return {symbol: None for symbol in self.symbols}
398
+ if isinstance(quantities, dict):
399
+ return quantities
400
+ elif isinstance(quantities, int):
401
+ return {symbol: quantities for symbol in self.symbols}
402
+ raise TypeError(f"Unsupported type for quantities: {type(quantities)}")
403
+
404
+ @staticmethod
405
+ def calculate_pct_change(current_price: float, lh_price: float) -> float:
406
+ return ((current_price - lh_price) / lh_price) * 100
407
+
408
+ @staticmethod
409
+ def is_signal_time(period_count: Optional[int], signal_inverval: int) -> bool:
410
+ """
411
+ Check if we can generate a signal based on the current period count.
412
+ We use the signal interval as a form of periodicity or rebalancing period.
413
+
414
+ Args:
415
+ period_count : The current period count (e.g., number of bars).
416
+ signal_inverval : The signal interval for generating signals (e.g., every 5 bars).
417
+
418
+ Returns:
419
+ bool : True if we can generate a signal, False otherwise
420
+ """
421
+ if period_count == 0 or period_count is None:
422
+ return True
423
+ return period_count % signal_inverval == 0
424
+
425
+ @staticmethod
426
+ def get_current_dt(time_zone: str = "US/Eastern") -> datetime:
427
+ return datetime.now(pytz.timezone(time_zone))
428
+
429
+ @staticmethod
430
+ def convert_time_zone(
431
+ dt: Union[datetime, int, pd.Timestamp],
432
+ from_tz: str = "UTC",
433
+ to_tz: str = "US/Eastern",
434
+ ) -> pd.Timestamp:
435
+ """
436
+ Convert datetime from one timezone to another.
437
+
438
+ Args:
439
+ dt : The datetime to convert.
440
+ from_tz : The timezone to convert from.
441
+ to_tz : The timezone to convert to.
442
+
443
+ Returns:
444
+ dt_to : The converted datetime.
445
+ """
446
+ from_tz_pytz = pytz.timezone(from_tz)
447
+ if isinstance(dt, (datetime, int)):
448
+ dt_ts = pd.to_datetime(dt, unit="s")
449
+ else:
450
+ dt_ts = dt
451
+ if dt_ts.tzinfo is None:
452
+ dt_ts = dt_ts.tz_localize(from_tz_pytz)
453
+ else:
454
+ dt_ts = dt_ts.tz_convert(from_tz_pytz)
455
+ return dt_ts.tz_convert(pytz.timezone(to_tz))
456
+
457
+ @staticmethod
458
+ def stop_time(time_zone: str, stop_time: str) -> bool:
459
+ now = datetime.now(pytz.timezone(time_zone)).time()
460
+ stop_time_dt = datetime.strptime(stop_time, "%H:%M").time()
461
+ return now >= stop_time_dt
462
+
463
+
464
+ class TWSStrategy(Strategy):
465
+ def calculate_signals(self, *args: Any, **kwargs: Any) -> List[TradeSignal]:
466
+ raise NotImplementedError("Should implement calculate_signals()")
@@ -0,0 +1,48 @@
1
+ """
2
+ Overview
3
+ ========
4
+
5
+ This MetaTrader Module provides a direct interface to the MetaTrader 5 trading platform,
6
+ enabling seamless integration of Python-based trading strategies with a live trading environment.
7
+ It offers a comprehensive set of tools for account management, trade execution, market data retrieval,
8
+ and risk management, all tailored for the MetaTrader 5 platform.
9
+
10
+ Features
11
+ ========
12
+
13
+ - **Direct MetaTrader 5 Integration**: Connects to the MetaTrader 5 terminal to access its full range of trading functionalities.
14
+ - **Account and Trade Management**: Provides tools for querying account information, managing open positions, and executing trades.
15
+ - **Market Data Retrieval**: Fetches historical and real-time market data, including rates and ticks, directly from MetaTrader 5.
16
+ - **Risk Management**: Includes utilities for managing risk, such as setting stop-loss and take-profit levels.
17
+ - **Trade Copying**: Functionality to copy trades between different MetaTrader 5 accounts.
18
+
19
+ Components
20
+ ==========
21
+
22
+ - **Account**: Manages account information, including balance, equity, and margin.
23
+ - **Broker**: Handles the connection to the MetaTrader 5 terminal.
24
+ - **Copier**: Copies trades between accounts.
25
+ - **Rates**: Retrieves historical and current market rates.
26
+ - **Risk**: Provides risk management functionalities.
27
+ - **Trade**: Manages trade execution and position management.
28
+ - **Utils**: Contains utility functions for the MetaTrader module.
29
+
30
+ Examples
31
+ ========
32
+
33
+ >>> from bbstrader.metatrader import Account
34
+ >>> account = Account()
35
+ >>> print(account.get_account_info())
36
+
37
+ Notes
38
+ =====
39
+
40
+ This module requires the MetaTrader 5 terminal to be installed and running.
41
+ """
42
+
43
+ from bbstrader.metatrader.account import * # noqa: F403
44
+ from bbstrader.metatrader.rates import * # noqa: F403
45
+ from bbstrader.metatrader.risk import * # noqa: F403
46
+ from bbstrader.metatrader.trade import * # noqa: F403
47
+ from bbstrader.metatrader.utils import * # noqa: F403
48
+ from bbstrader.metatrader.copier import * # noqa: F403