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,588 @@
1
+ from abc import abstractmethod
2
+ from datetime import datetime
3
+ from queue import Queue
4
+ from typing import Any, Callable, Dict, List, Optional, Union
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ from loguru import logger
9
+
10
+ from bbstrader.btengine.data import DataHandler
11
+ from bbstrader.btengine.event import Events, FillEvent, MarketEvent, SignalEvent
12
+ from bbstrader.config import BBSTRADER_DIR
13
+ from bbstrader.core.strategy import BaseStrategy, TradingMode
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
+ __all__ = ["BacktestStrategy"]
23
+
24
+
25
+ class BacktestStrategy(BaseStrategy):
26
+ """
27
+ Strategy implementation specifically for Backtesting.
28
+ Handles internal state for orders, positions, trades, and cash.
29
+ Simulates order execution and pending orders.
30
+ """
31
+
32
+ _orders: Dict[str, Dict[str, List[SignalEvent]]]
33
+ _positions: Dict[str, Dict[str, Union[int, float]]]
34
+ _trades: Dict[str, Dict[str, int]]
35
+ _holdings: Dict[str, float]
36
+ _portfolio_value: Optional[float]
37
+ events: "Queue[Union[SignalEvent, FillEvent]]"
38
+ data: DataHandler
39
+
40
+ def __init__(
41
+ self,
42
+ events: "Queue[Union[SignalEvent, FillEvent]]",
43
+ symbol_list: List[str],
44
+ bars: DataHandler,
45
+ **kwargs: Any,
46
+ ) -> None:
47
+ """
48
+ Initialize the `BacktestStrategy` object.
49
+
50
+ Args:
51
+ events : The event queue.
52
+ symbol_list : The list of symbols for the strategy.
53
+ bars : The data handler object.
54
+ **kwargs : Additional keyword arguments for other classes (e.g, Portfolio, ExecutionHandler).
55
+ - max_trades : The maximum number of trades allowed per symbol.
56
+ - time_frame : The time frame for the strategy.
57
+ - logger : The logger object for the strategy.
58
+ """
59
+ super().__init__(symbol_list, **kwargs)
60
+ self.events = events
61
+ self.data = bars
62
+ self.mode = TradingMode.BACKTEST
63
+ self._portfolio_value = None
64
+ self._initialize_portfolio()
65
+
66
+ def _initialize_portfolio(self) -> None:
67
+ self._orders = {}
68
+ self._positions = {}
69
+ self._trades = {}
70
+ for symbol in self.symbols:
71
+ self._positions[symbol] = {}
72
+ self._orders[symbol] = {}
73
+ self._trades[symbol] = {}
74
+ for position in ["LONG", "SHORT"]:
75
+ self._trades[symbol][position] = 0
76
+ self._positions[symbol][position] = 0.0
77
+ for order in ["BLMT", "BSTP", "BSTPLMT", "SLMT", "SSTP", "SSTPLMT"]:
78
+ self._orders[symbol][order] = []
79
+ self._holdings = {s: 0.0 for s in self.symbols}
80
+
81
+ @property
82
+ def cash(self) -> float:
83
+ return self._portfolio_value or 0.0
84
+
85
+ @cash.setter
86
+ def cash(self, value: float) -> None:
87
+ self._portfolio_value = value
88
+
89
+ @property
90
+ def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
91
+ return self._orders
92
+
93
+ @property
94
+ def trades(self) -> Dict[str, Dict[str, int]]:
95
+ return self._trades
96
+
97
+ @property
98
+ def positions(self) -> Dict[str, Dict[str, Union[int, float]]]:
99
+ return self._positions
100
+
101
+ @property
102
+ def holdings(self) -> Dict[str, float]:
103
+ return self._holdings
104
+
105
+ def get_update_from_portfolio(
106
+ self, positions: Dict[str, float], holdings: Dict[str, float]
107
+ ) -> None:
108
+ """
109
+ Update the positions and holdings for the strategy from the portfolio.
110
+
111
+ Positions are the number of shares of a security that are owned in long or short.
112
+ Holdings are the value (postions * price) of the security that are owned in long or short.
113
+
114
+ Args:
115
+ positions : The positions for the symbols in the strategy.
116
+ holdings : The holdings for the symbols in the strategy.
117
+ """
118
+ for symbol in self.symbols:
119
+ if symbol in positions:
120
+ if positions[symbol] > 0:
121
+ self._positions[symbol]["LONG"] = positions[symbol]
122
+ elif positions[symbol] < 0:
123
+ self._positions[symbol]["SHORT"] = positions[symbol]
124
+ else:
125
+ self._positions[symbol]["LONG"] = 0
126
+ self._positions[symbol]["SHORT"] = 0
127
+ if symbol in holdings:
128
+ self._holdings[symbol] = holdings[symbol]
129
+
130
+ def update_trades_from_fill(self, event: FillEvent) -> None:
131
+ """
132
+ This method updates the trades for the strategy based on the fill event.
133
+ It is used to keep track of the number of trades executed for each order.
134
+ """
135
+ if event.type == Events.FILL:
136
+ if event.order != "EXIT":
137
+ self._trades[event.symbol][event.order] += 1 # type: ignore
138
+ elif event.order == "EXIT" and event.direction == "BUY":
139
+ self._trades[event.symbol]["SHORT"] = 0
140
+ elif event.order == "EXIT" and event.direction == "SELL":
141
+ self._trades[event.symbol]["LONG"] = 0
142
+
143
+ def get_asset_values(
144
+ self,
145
+ symbol_list: List[str],
146
+ window: int,
147
+ value_type: str = "returns",
148
+ array: bool = True,
149
+ **kwargs,
150
+ ) -> Optional[Dict[str, Union[np.ndarray, pd.Series]]]:
151
+ asset_values: Dict[str, Union[np.ndarray, pd.Series]] = {}
152
+ for asset in symbol_list:
153
+ if array:
154
+ values = self.data.get_latest_bars_values(asset, value_type, N=window)
155
+ asset_values[asset] = values[~np.isnan(values)]
156
+ else:
157
+ values_df = self.data.get_latest_bars(asset, N=window)
158
+ if isinstance(values_df, pd.DataFrame):
159
+ asset_values[asset] = values_df[value_type]
160
+
161
+ if all(len(values) >= window for values in asset_values.values()):
162
+ return {a: v[-window:] for a, v in asset_values.items()}
163
+ return None
164
+
165
+ @abstractmethod
166
+ def calculate_signals(self, event: MarketEvent) -> None: ...
167
+
168
+ def _send_order(
169
+ self,
170
+ id: int,
171
+ symbol: str,
172
+ signal: str,
173
+ strength: float,
174
+ price: float,
175
+ quantity: int,
176
+ dtime: Union[datetime, pd.Timestamp],
177
+ ) -> None:
178
+ position = SignalEvent(
179
+ id,
180
+ symbol,
181
+ dtime,
182
+ signal,
183
+ quantity=quantity,
184
+ strength=strength,
185
+ price=price, # type: ignore
186
+ )
187
+ log = False
188
+ if signal in ["LONG", "SHORT"]:
189
+ if self._trades[symbol][signal] < self.max_trades[symbol] and quantity > 0:
190
+ self.events.put(position)
191
+ log = True
192
+ elif signal == "EXIT":
193
+ if (
194
+ self._positions[symbol]["LONG"] > 0
195
+ or self._positions[symbol]["SHORT"] < 0
196
+ ):
197
+ self.events.put(position)
198
+ log = True
199
+ if log:
200
+ self.logger.info(
201
+ f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{round(price, 5)}",
202
+ custom_time=dtime,
203
+ )
204
+
205
+ def buy_mkt(
206
+ self,
207
+ id: int,
208
+ symbol: str,
209
+ price: float,
210
+ quantity: int,
211
+ strength: float = 1.0,
212
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
213
+ ) -> None:
214
+ """
215
+ Open a long position
216
+
217
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
218
+ """
219
+ if dtime is None:
220
+ dtime = self.get_current_dt()
221
+ self._send_order(id, symbol, "LONG", strength, price, quantity, dtime)
222
+
223
+ def sell_mkt(
224
+ self,
225
+ id: int,
226
+ symbol: str,
227
+ price: float,
228
+ quantity: int,
229
+ strength: float = 1.0,
230
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
231
+ ) -> None:
232
+ """
233
+ Open a short position
234
+
235
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
236
+ """
237
+ if dtime is None:
238
+ dtime = self.get_current_dt()
239
+ self._send_order(id, symbol, "SHORT", strength, price, quantity, dtime)
240
+
241
+ def close_positions(
242
+ self,
243
+ id: int,
244
+ symbol: str,
245
+ price: float,
246
+ quantity: int,
247
+ strength: float = 1.0,
248
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
249
+ ) -> None:
250
+ """
251
+ Close a position or exit all positions
252
+
253
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
254
+ """
255
+ if dtime is None:
256
+ dtime = self.get_current_dt()
257
+ self._send_order(id, symbol, "EXIT", strength, price, quantity, dtime)
258
+
259
+ def buy_stop(
260
+ self,
261
+ id: int,
262
+ symbol: str,
263
+ price: float,
264
+ quantity: int,
265
+ strength: float = 1.0,
266
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
267
+ ) -> None:
268
+ """
269
+ Open a pending order to buy at a stop price
270
+
271
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
272
+ """
273
+ current_price = self.data.get_latest_bar_value(symbol, "close")
274
+ if price <= current_price:
275
+ raise ValueError(
276
+ "The buy_stop price must be greater than the current price."
277
+ )
278
+ if dtime is None:
279
+ dtime = self.get_current_dt()
280
+ order = SignalEvent(
281
+ id,
282
+ symbol,
283
+ dtime,
284
+ "LONG",
285
+ quantity=quantity,
286
+ strength=strength,
287
+ price=price, # type: ignore
288
+ )
289
+ self._orders[symbol]["BSTP"].append(order)
290
+
291
+ def sell_stop(
292
+ self,
293
+ id: int,
294
+ symbol: str,
295
+ price: float,
296
+ quantity: int,
297
+ strength: float = 1.0,
298
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
299
+ ) -> None:
300
+ """
301
+ Open a pending order to sell at a stop price
302
+
303
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
304
+ """
305
+ current_price = self.data.get_latest_bar_value(symbol, "close")
306
+ if price >= current_price:
307
+ raise ValueError("The sell_stop price must be less than the current price.")
308
+ if dtime is None:
309
+ dtime = self.get_current_dt()
310
+ order = SignalEvent(
311
+ id,
312
+ symbol,
313
+ dtime, # type: ignore
314
+ "SHORT",
315
+ quantity=quantity,
316
+ strength=strength,
317
+ price=price,
318
+ )
319
+ self._orders[symbol]["SSTP"].append(order)
320
+
321
+ def buy_limit(
322
+ self,
323
+ id: int,
324
+ symbol: str,
325
+ price: float,
326
+ quantity: int,
327
+ strength: float = 1.0,
328
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
329
+ ) -> None:
330
+ """
331
+ Open a pending order to buy at a limit price
332
+
333
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
334
+ """
335
+ current_price = self.data.get_latest_bar_value(symbol, "close")
336
+ if price >= current_price:
337
+ raise ValueError("The buy_limit price must be less than the current price.")
338
+ if dtime is None:
339
+ dtime = self.get_current_dt()
340
+ order = SignalEvent(
341
+ id,
342
+ symbol,
343
+ dtime,
344
+ "LONG",
345
+ quantity=quantity,
346
+ strength=strength,
347
+ price=price, # type: ignore
348
+ )
349
+ self._orders[symbol]["BLMT"].append(order)
350
+
351
+ def sell_limit(
352
+ self,
353
+ id: int,
354
+ symbol: str,
355
+ price: float,
356
+ quantity: int,
357
+ strength: float = 1.0,
358
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
359
+ ) -> None:
360
+ """
361
+ Open a pending order to sell at a limit price
362
+
363
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
364
+ """
365
+ current_price = self.data.get_latest_bar_value(symbol, "close")
366
+ if price <= current_price:
367
+ raise ValueError(
368
+ "The sell_limit price must be greater than the current price."
369
+ )
370
+ if dtime is None:
371
+ dtime = self.get_current_dt()
372
+ order = SignalEvent(
373
+ id,
374
+ symbol,
375
+ dtime, # type: ignore
376
+ "SHORT",
377
+ quantity=quantity,
378
+ strength=strength,
379
+ price=price,
380
+ )
381
+ self._orders[symbol]["SLMT"].append(order)
382
+
383
+ def buy_stop_limit(
384
+ self,
385
+ id: int,
386
+ symbol: str,
387
+ price: float,
388
+ stoplimit: float,
389
+ quantity: int,
390
+ strength: float = 1.0,
391
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
392
+ ) -> None:
393
+ """
394
+ Open a pending order to buy at a stop-limit price
395
+
396
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
397
+ """
398
+ current_price = self.data.get_latest_bar_value(symbol, "close")
399
+ if price <= current_price:
400
+ raise ValueError(
401
+ f"The stop price {price} must be greater than the current price {current_price}."
402
+ )
403
+ if price >= stoplimit:
404
+ raise ValueError(
405
+ f"The stop-limit price {stoplimit} must be greater than the price {price}."
406
+ )
407
+ if dtime is None:
408
+ dtime = self.get_current_dt()
409
+ order = SignalEvent(
410
+ id,
411
+ symbol,
412
+ dtime, # type: ignore
413
+ "LONG",
414
+ quantity=quantity,
415
+ strength=strength,
416
+ price=price,
417
+ stoplimit=stoplimit,
418
+ )
419
+ self._orders[symbol]["BSTPLMT"].append(order)
420
+
421
+ def sell_stop_limit(
422
+ self,
423
+ id: int,
424
+ symbol: str,
425
+ price: float,
426
+ stoplimit: float,
427
+ quantity: int,
428
+ strength: float = 1.0,
429
+ dtime: Optional[Union[datetime, pd.Timestamp]] = None,
430
+ ) -> None:
431
+ """
432
+ Open a pending order to sell at a stop-limit price
433
+
434
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
435
+ """
436
+ current_price = self.data.get_latest_bar_value(symbol, "close")
437
+ if price >= current_price:
438
+ raise ValueError(
439
+ f"The stop price {price} must be less than the current price {current_price}."
440
+ )
441
+ if price <= stoplimit:
442
+ raise ValueError(
443
+ f"The stop-limit price {stoplimit} must be less than the price {price}."
444
+ )
445
+ if dtime is None:
446
+ dtime = self.get_current_dt()
447
+ order = SignalEvent(
448
+ id,
449
+ symbol,
450
+ dtime, # type: ignore
451
+ "SHORT",
452
+ quantity=quantity,
453
+ strength=strength,
454
+ price=price,
455
+ stoplimit=stoplimit,
456
+ )
457
+ self._orders[symbol]["SSTPLMT"].append(order)
458
+
459
+ def check_pending_orders(self) -> None:
460
+ """
461
+ Check for pending orders and handle them accordingly.
462
+ """
463
+
464
+ def logmsg(
465
+ order: SignalEvent,
466
+ type: str,
467
+ symbol: str,
468
+ dtime: Union[datetime, pd.Timestamp],
469
+ ) -> None:
470
+ self.logger.info(
471
+ f"{type} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
472
+ f"PRICE @ {round(order.price, 5)}", # type: ignore
473
+ custom_time=dtime,
474
+ )
475
+
476
+ def process_orders(
477
+ order_type: str,
478
+ condition: Callable[[SignalEvent], bool],
479
+ execute_fn: Callable[[SignalEvent], None],
480
+ log_label: str,
481
+ symbol: str,
482
+ dtime: Union[datetime, pd.Timestamp],
483
+ ) -> None:
484
+ for order in self._orders[symbol][order_type].copy():
485
+ if condition(order):
486
+ execute_fn(order)
487
+ try:
488
+ self._orders[symbol][order_type].remove(order)
489
+ assert order not in self._orders[symbol][order_type]
490
+ except AssertionError:
491
+ self._orders[symbol][order_type] = [
492
+ o for o in self._orders[symbol][order_type] if o != order
493
+ ]
494
+ logmsg(order, log_label, symbol, dtime)
495
+
496
+ for symbol in self.symbols:
497
+ dtime = self.data.get_latest_bar_datetime(symbol)
498
+ latest_close = self.data.get_latest_bar_value(symbol, "close")
499
+
500
+ process_orders(
501
+ "BLMT",
502
+ lambda o: latest_close <= o.price, # type: ignore
503
+ lambda o: self.buy_mkt(
504
+ o.strategy_id,
505
+ symbol,
506
+ o.price,
507
+ o.quantity,
508
+ dtime=dtime, # type: ignore
509
+ ),
510
+ "BUY LIMIT",
511
+ symbol,
512
+ dtime,
513
+ )
514
+
515
+ process_orders(
516
+ "SLMT",
517
+ lambda o: latest_close >= o.price, # type: ignore
518
+ lambda o: self.sell_mkt(
519
+ o.strategy_id,
520
+ symbol,
521
+ o.price,
522
+ o.quantity,
523
+ dtime=dtime, # type: ignore
524
+ ),
525
+ "SELL LIMIT",
526
+ symbol,
527
+ dtime,
528
+ )
529
+
530
+ process_orders(
531
+ "BSTP",
532
+ lambda o: latest_close >= o.price, # type: ignore
533
+ lambda o: self.buy_mkt(
534
+ o.strategy_id,
535
+ symbol,
536
+ o.price,
537
+ o.quantity,
538
+ dtime=dtime, # type: ignore
539
+ ),
540
+ "BUY STOP",
541
+ symbol,
542
+ dtime,
543
+ )
544
+
545
+ process_orders(
546
+ "SSTP",
547
+ lambda o: latest_close <= o.price, # type: ignore
548
+ lambda o: self.sell_mkt(
549
+ o.strategy_id,
550
+ symbol,
551
+ o.price,
552
+ o.quantity,
553
+ dtime=dtime, # type: ignore
554
+ ),
555
+ "SELL STOP",
556
+ symbol,
557
+ dtime,
558
+ )
559
+
560
+ process_orders(
561
+ "BSTPLMT",
562
+ lambda o: latest_close >= o.price, # type: ignore
563
+ lambda o: self.buy_limit(
564
+ o.strategy_id,
565
+ symbol,
566
+ o.stoplimit,
567
+ o.quantity,
568
+ dtime=dtime, # type: ignore
569
+ ),
570
+ "BUY STOP LIMIT",
571
+ symbol,
572
+ dtime,
573
+ )
574
+
575
+ process_orders(
576
+ "SSTPLMT",
577
+ lambda o: latest_close <= o.price, # type: ignore
578
+ lambda o: self.sell_limit(
579
+ o.strategy_id,
580
+ symbol,
581
+ o.stoplimit,
582
+ o.quantity,
583
+ dtime=dtime, # type: ignore
584
+ ),
585
+ "SELL STOP LIMIT",
586
+ symbol,
587
+ dtime,
588
+ )
bbstrader/compat.py ADDED
@@ -0,0 +1,28 @@
1
+ import platform
2
+ import sys
3
+ from typing import Any
4
+
5
+
6
+ def setup_mock_modules() -> None:
7
+ """Mock some modules not available on some OS to prevent import errors."""
8
+ from unittest.mock import MagicMock
9
+
10
+ class Mock(MagicMock):
11
+ @classmethod
12
+ def __getattr__(cls, name: str) -> Any:
13
+ return MagicMock()
14
+
15
+ MOCK_MODULES = []
16
+
17
+ # Mock Metatrader5 on Linux and MacOS
18
+ if platform.system() != "Windows":
19
+ MOCK_MODULES.append("MetaTrader5")
20
+
21
+ # Mock posix On windows
22
+ if platform.system() == "Windows":
23
+ MOCK_MODULES.append("posix")
24
+
25
+ sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
26
+
27
+
28
+ setup_mock_modules()
bbstrader/config.py ADDED
@@ -0,0 +1,100 @@
1
+ import logging
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from typing import Any, List, Optional
5
+
6
+
7
+ def get_config_dir(name: str = ".bbstrader") -> Path:
8
+ """
9
+ Get the path to the configuration directory.
10
+
11
+ Args:
12
+ name: The name of the configuration directory.
13
+
14
+ Returns:
15
+ The path to the configuration directory.
16
+ """
17
+ home_dir = Path.home() / name
18
+ if not home_dir.exists():
19
+ home_dir.mkdir()
20
+ return home_dir
21
+
22
+
23
+ BBSTRADER_DIR = get_config_dir()
24
+
25
+
26
+ class LogLevelFilter(logging.Filter):
27
+ def __init__(self, levels: List[int]) -> None:
28
+ """
29
+ Initializes the filter with specific logging levels.
30
+
31
+ Args:
32
+ levels: A list of logging level values (integers) to include.
33
+ """
34
+ super().__init__()
35
+ self.levels = levels
36
+
37
+ def filter(self, record: logging.LogRecord) -> bool:
38
+ """
39
+ Filters log records based on their level.
40
+
41
+ Args:
42
+ record: The log record to check.
43
+
44
+ Returns:
45
+ True if the record's level is in the allowed levels, False otherwise.
46
+ """
47
+ return record.levelno in self.levels
48
+
49
+
50
+ class CustomFormatter(logging.Formatter):
51
+ def formatTime(
52
+ self, record: logging.LogRecord, datefmt: Optional[str] = None
53
+ ) -> str:
54
+ if hasattr(record, "custom_time"):
55
+ # Use the custom time if provided
56
+ record.created = record.custom_time.timestamp() # type: ignore
57
+ return super().formatTime(record, datefmt)
58
+
59
+
60
+ class CustomLogger(logging.Logger):
61
+ def __init__(self, name: str, level: int = logging.NOTSET) -> None:
62
+ super().__init__(name, level)
63
+
64
+ def log(
65
+ self,
66
+ level: int,
67
+ msg: object,
68
+ *args: object,
69
+ custom_time: Optional[datetime] = None,
70
+ **kwargs: Any,
71
+ ) -> None:
72
+ if custom_time:
73
+ if "extra" not in kwargs or kwargs["extra"] is None:
74
+ kwargs["extra"] = {}
75
+ kwargs["extra"]["custom_time"] = custom_time
76
+ super().log(level, msg, *args, **kwargs)
77
+
78
+
79
+ def config_logger(log_file: str, console_log: bool = True) -> logging.Logger:
80
+ logging.setLoggerClass(CustomLogger)
81
+ logger = logging.getLogger(__name__)
82
+ logger.setLevel(logging.DEBUG)
83
+
84
+ file_handler = logging.FileHandler(log_file)
85
+ file_handler.setLevel(logging.INFO)
86
+
87
+ formatter = CustomFormatter(
88
+ "%(asctime)s - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
89
+ )
90
+ file_handler.setFormatter(formatter)
91
+
92
+ logger.addHandler(file_handler)
93
+
94
+ if console_log:
95
+ console_handler = logging.StreamHandler()
96
+ console_handler.setLevel(logging.DEBUG)
97
+ console_handler.setFormatter(formatter)
98
+ logger.addHandler(console_handler)
99
+
100
+ return logger