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

@@ -1,5 +1,24 @@
1
1
  from abc import ABCMeta, abstractmethod
2
- from typing import Dict, Union
2
+ import pytz
3
+ import math
4
+ import pandas as pd
5
+ import numpy as np
6
+ from queue import Queue
7
+ from datetime import datetime
8
+ from bbstrader.config import config_logger
9
+ from bbstrader.btengine.event import SignalEvent
10
+ from bbstrader.btengine.data import DataHandler
11
+ from bbstrader.metatrader.account import Account
12
+ from bbstrader.metatrader.rates import Rates
13
+ from typing import (
14
+ Dict,
15
+ Union,
16
+ Any,
17
+ List,
18
+ Literal
19
+ )
20
+
21
+ __all__ = ['Strategy', 'MT5Strategy']
3
22
 
4
23
  class Strategy(metaclass=ABCMeta):
5
24
  """
@@ -22,11 +41,452 @@ class Strategy(metaclass=ABCMeta):
22
41
  """
23
42
 
24
43
  @abstractmethod
25
- def calculate_signals(self, *args, **kwargs) -> Dict[str, Union[str, None]]:
44
+ def calculate_signals(self, *args, **kwargs) -> Any:
45
+ raise NotImplementedError(
46
+ "Should implement calculate_signals()"
47
+ )
48
+
49
+ def check_pending_orders(self): ...
50
+
51
+
52
+ class MT5Strategy(Strategy):
53
+ """
54
+ A `MT5Strategy()` object is a subclass of `Strategy` that is used to
55
+ calculate signals for the MetaTrader 5 trading platform. The signals
56
+ are generated by the `MT5Strategy` object and sent to the the `MT5ExecutionEngine`
57
+ for live trading and `MT5BacktestEngine` objects for backtesting.
58
+ """
59
+
60
+ def __init__(self, events: Queue=None, symbol_list: List[str]=None,
61
+ bars: DataHandler=None, mode: str=None, **kwargs):
62
+ """
63
+ Initialize the `MT5Strategy` object.
64
+
65
+ Args:
66
+ events : The event queue.
67
+ symbol_list : The list of symbols for the strategy.
68
+ bars : The data handler object.
69
+ mode : The mode of operation for the strategy (backtest or live).
70
+ **kwargs : Additional keyword arguments for other classes (e.g, Portfolio, ExecutionHandler).
26
71
  """
27
- Provides the mechanisms to calculate the list of signals.
72
+ self.events = events
73
+ self.data = bars
74
+ self.symbols = symbol_list
75
+ self.mode = mode
76
+ self.volume = kwargs.get("volume")
77
+ self.logger = kwargs.get("logger", config_logger("mt5_strategy.log"))
78
+ self._construct_positions_and_orders()
79
+
80
+ def _construct_positions_and_orders(self):
81
+ self.positions: Dict[str, Dict[str, int]] = {}
82
+ self.orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
83
+ positions = ['LONG', 'SHORT']
84
+ orders = ['BLMT', 'BSTP', 'BSTPLMT', 'SLMT', 'SSTP', 'SSTPLMT']
85
+ for symbol in self.symbols:
86
+ self.positions[symbol] = {position: 0 for position in positions}
87
+ self.orders[symbol] = {order: [] for order in orders}
88
+
89
+ def calculate_signals(self, *args, **kwargs
90
+ ) -> Dict[str, Union[str, dict, None]] | None:
91
+ """
92
+ Provides the mechanisms to calculate signals for the strategy.
28
93
  This methods should return a dictionary of symbols and their respective signals.
94
+ The returned signals should be either string or dictionary objects.
95
+
96
+ If a string is used, it should be:
97
+ - ``LONG`` , ``BMKT``, ``BLMT``, ``BSTP``, ``BSTPLMT`` for a long signal (market, limit, stop, stop-limit).
98
+ - ``SHORT``, ``SMKT``, ``SLMT``, ``SSTP``, ``SSTPLMT`` for a short signal (market, limit, stop, stop-limit).
99
+ - ``EXIT``, ``EXIT_LONG``, ``EXIT_LONG_STOP``, ``EXIT_LONG_LIMIT``, ``EXIT_LONG_STOP_LIMIT`` for an exit signal (long).
100
+ - ``EXIT_SHORT``, ``EXIT_SHORT_STOP``, ``EXIT_SHORT_LIMIT``, ``EXIT_SHORT_STOP_LIMIT`` for an exit signal (short).
101
+ - ``EXIT_ALL_ORDERS`` for cancelling all orders.
102
+ - ``EXIT_ALL_POSITIONS`` for exiting all positions.
103
+ - ``EXIT_PROFITABLES`` for exiting all profitable positions.
104
+ - ``EXIT_LOSINGS`` for exiting all losing positions.
105
+
106
+ The signals could also be ``EXIT_STOP``, ``EXIT_LIMIT``, ``EXIT_STOP_LIMIT`` for exiting a position.
107
+
108
+ If a dictionary is used, it should be:
109
+ for each symbol, a dictionary with the following keys
110
+ - ``action``: The action to take for the symbol (LONG, SHORT, EXIT, etc.)
111
+ - ``price``: The price at which to execute the action.
112
+ - ``stoplimit``: The stop-limit price for STOP-LIMIT orders.
113
+
114
+ The dictionary can be use for pending orders (limit, stop, stop-limit) where the price is required.
29
115
  """
30
- raise NotImplementedError(
31
- "Should implement calculate_signals()"
32
- )
116
+ pass
117
+
118
+ def get_quantity(self, symbol) -> int:
119
+ """
120
+ Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
121
+ The quantity calculated can be used to evalute a strategy's performance for each symbol
122
+ given the fact that the dollar value is the same for all symbols.
123
+
124
+ Args:
125
+ symbol : The symbol for the trade.
126
+
127
+ Returns:
128
+ qty : The quantity to buy or sell for the symbol.
129
+ """
130
+ if self.volume is None:
131
+ raise ValueError("Volume must be provided for the method.")
132
+ current_price = self.data.get_latest_bar_value(symbol, 'close')
133
+ qty = math.ceil(self.volume / current_price)
134
+ return max(qty, 1)
135
+
136
+ def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
137
+ """
138
+ Get the quantities to buy or sell for the symbols in the strategy.
139
+ This method is used when whe need to assign different quantities to the symbols.
140
+
141
+ Args:
142
+ quantities : The quantities for the symbols in the strategy.
143
+ """
144
+ if quantities is None:
145
+ return {symbol: None for symbol in self.symbols}
146
+ if isinstance(quantities, dict):
147
+ return quantities
148
+ elif isinstance(quantities, int):
149
+ return {symbol: quantities for symbol in self.symbols}
150
+
151
+ def _send_order(self, id, symbol: str, signal: str, strength: float, price: float,
152
+ quantity: int, dtime: datetime | pd.Timestamp):
153
+
154
+ position = SignalEvent(id, symbol, dtime, signal,
155
+ quantity=quantity, strength=strength, price=price)
156
+ self.events.put(position)
157
+ self.logger.info(
158
+ f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
159
+
160
+ def buy(self, id: int, symbol: str, price: float, quantity: int,
161
+ strength: float=1.0, dtime: datetime | pd.Timestamp=None):
162
+ """
163
+ Open a long position
164
+
165
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
166
+ """
167
+ self._send_order(id, symbol, 'LONG', strength, price, quantity, dtime)
168
+ self.positions[symbol]['LONG'] += quantity
169
+
170
+ def sell(self, id, symbol, price, quantity, strength=1.0, dtime=None):
171
+ """
172
+ Open a short position
173
+
174
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
175
+ """
176
+ self._send_order(id, symbol, 'SHORT', strength, price, quantity, dtime)
177
+ self.positions[symbol]['SHORT'] += quantity
178
+
179
+ def close(self, id, symbol, price, quantity, strength=1.0, dtime=None):
180
+ """
181
+ Close a position
182
+
183
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
184
+ """
185
+ self._send_order(id, symbol, 'EXIT', strength, price, quantity, dtime)
186
+ self.positions[symbol]['LONG'] -= quantity
187
+
188
+ def buy_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
189
+ """
190
+ Open a pending order to buy at a stop price
191
+
192
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
193
+ """
194
+ current_price = self.data.get_latest_bar_value(symbol, 'close')
195
+ if price <= current_price:
196
+ raise ValueError(
197
+ "The buy_stop price must be greater than the current price.")
198
+ order = SignalEvent(id, symbol, dtime, 'LONG',
199
+ quantity=quantity, strength=strength, price=price)
200
+ self.orders[symbol]['BSTP'].append(order)
201
+
202
+ def sell_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
203
+ """
204
+ Open a pending order to sell at a stop price
205
+
206
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
207
+ """
208
+ current_price = self.data.get_latest_bar_value(symbol, 'close')
209
+ if price >= current_price:
210
+ raise ValueError(
211
+ "The sell_stop price must be less than the current price.")
212
+ order = SignalEvent(id, symbol, dtime, 'SHORT',
213
+ quantity=quantity, strength=strength, price=price)
214
+ self.orders[symbol]['SSTP'].append(order)
215
+
216
+ def buy_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
217
+ """
218
+ Open a pending order to buy at a limit price
219
+
220
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
221
+ """
222
+ current_price = self.data.get_latest_bar_value(symbol, 'close')
223
+ if price >= current_price:
224
+ raise ValueError(
225
+ "The buy_limit price must be less than the current price.")
226
+ order = SignalEvent(id, symbol, dtime, 'LONG',
227
+ quantity=quantity, strength=strength, price=price)
228
+ self.orders[symbol]['BLMT'].append(order)
229
+
230
+ def sell_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
231
+ """
232
+ Open a pending order to sell at a limit price
233
+
234
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
235
+ """
236
+ current_price = self.data.get_latest_bar_value(symbol, 'close')
237
+ if price <= current_price:
238
+ raise ValueError(
239
+ "The sell_limit price must be greater than the current price.")
240
+ order = SignalEvent(id, symbol, dtime, 'SHORT',
241
+ quantity=quantity, strength=strength, price=price)
242
+ self.orders[symbol]['SLMT'].append(order)
243
+
244
+ def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
245
+ quantity: int, strength: float=1.0, dtime: datetime | pd.Timestamp = None):
246
+ """
247
+ Open a pending order to buy at a stop-limit price
248
+
249
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
250
+ """
251
+ current_price = self.data.get_latest_bar_value(symbol, 'close')
252
+ if price <= current_price:
253
+ raise ValueError(
254
+ f"The stop price {price} must be greater than the current price {current_price}.")
255
+ if price >= stoplimit:
256
+ raise ValueError(
257
+ f"The stop-limit price {stoplimit} must be greater than the price {price}.")
258
+ order = SignalEvent(id, symbol, dtime, 'LONG',
259
+ quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
260
+ self.orders[symbol]['BSTPLMT'].append(order)
261
+
262
+ def sell_stop_limit(self, id, symbol, price, stoplimit, quantity, strength=1.0, dtime=None):
263
+ """
264
+ Open a pending order to sell at a stop-limit price
265
+
266
+ See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
267
+ """
268
+ current_price = self.data.get_latest_bar_value(symbol, 'close')
269
+ if price >= current_price:
270
+ raise ValueError(
271
+ f"The stop price {price} must be less than the current price {current_price}.")
272
+ if price <= stoplimit:
273
+ raise ValueError(
274
+ f"The stop-limit price {stoplimit} must be less than the price {price}.")
275
+ order = SignalEvent(id, symbol, dtime, 'SHORT',
276
+ quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
277
+ self.orders[symbol]['SSTPLMT'].append(order)
278
+
279
+ def check_pending_orders(self):
280
+ """
281
+ Check for pending orders and handle them accordingly.
282
+ """
283
+ for symbol in self.symbols:
284
+ dtime = self.data.get_latest_bar_datetime(symbol)
285
+ for order in self.orders[symbol]['BLMT'].copy():
286
+ if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
287
+ self.buy(order.strategy_id, symbol,
288
+ order.price, order.quantity, dtime)
289
+ self.logger.info(
290
+ f"BUY LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
291
+ f"PRICE @ {order.price}", custom_time=dtime)
292
+ self.orders[symbol]['BLMT'].remove(order)
293
+ for order in self.orders[symbol]['SLMT'].copy():
294
+ if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
295
+ self.sell(order.strategy_id, symbol,
296
+ order.price, order.quantity, dtime)
297
+ self.logger.info(
298
+ f"SELL LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
299
+ f"PRICE @ {order.price}", custom_time=dtime)
300
+ self.orders[symbol]['SLMT'].remove(order)
301
+ for order in self.orders[symbol]['BSTP'].copy():
302
+ if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
303
+ self.buy(order.strategy_id, symbol,
304
+ order.price, order.quantity, dtime)
305
+ self.logger.info(
306
+ f"BUY STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
307
+ f"PRICE @ {order.price}", custom_time=dtime)
308
+ self.orders[symbol]['BSTP'].remove(order)
309
+ for order in self.orders[symbol]['SSTP'].copy():
310
+ if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
311
+ self.sell(order.strategy_id, symbol,
312
+ order.price, order.quantity, dtime)
313
+ self.logger.info(
314
+ f"SELL STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
315
+ f"PRICE @ {order.price}", custom_time=dtime)
316
+ self.orders[symbol]['SSTP'].remove(order)
317
+ for order in self.orders[symbol]['BSTPLMT'].copy():
318
+ if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
319
+ self.buy_limit(order.strategy_id, symbol,
320
+ order.stoplimit, order.quantity, dtime)
321
+ self.logger.info(
322
+ f"BUY STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
323
+ f"PRICE @ {order.price}", custom_time=dtime)
324
+ self.orders[symbol]['BSTPLMT'].remove(order)
325
+ for order in self.orders[symbol]['SSTPLMT'].copy():
326
+ if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
327
+ self.sell_limit(order.strategy_id, symbol,
328
+ order.stoplimit, order.quantity, dtime)
329
+ self.logger.info(
330
+ f"SELL STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
331
+ f"PRICE @ {order.price}", custom_time=dtime)
332
+ self.orders[symbol]['SSTPLMT'].remove(order)
333
+
334
+ def get_asset_values(self,
335
+ symbol_list: List[str],
336
+ window: int,
337
+ value_type: str = 'returns',
338
+ array: bool = True,
339
+ bars: DataHandler = None,
340
+ mode: Literal['backtest', 'live'] = 'backtest',
341
+ tf: str = 'D1'
342
+ ) -> Dict[str, np.ndarray | pd.Series] | None:
343
+ """
344
+ Get the historical OHLCV value or returns or custum value
345
+ based on the DataHandker of the assets in the symbol list.
346
+
347
+ Args:
348
+ bars : DataHandler for market data handling, required for backtest mode.
349
+ symbol_list : List of ticker symbols for the pairs trading strategy.
350
+ value_type : The type of value to get (e.g., returns, open, high, low, close, adjclose, volume).
351
+ array : If True, return the values as numpy arrays, otherwise as pandas Series.
352
+ mode : Mode of operation for the strategy.
353
+ window : The lookback period for resquesting the data.
354
+ tf : The time frame for the strategy.
355
+
356
+ Returns:
357
+ asset_values : Historical values of the assets in the symbol list.
358
+
359
+ Note:
360
+ In Live mode, the `bbstrader.metatrader.rates.Rates` class is used to get the historical data
361
+ so the value_type must be 'returns', 'open', 'high', 'low', 'close', 'adjclose', 'volume'.
362
+ """
363
+ if mode not in ['backtest', 'live']:
364
+ raise ValueError('Mode must be either backtest or live.')
365
+ asset_values = {}
366
+ if mode == 'backtest':
367
+ if bars is None:
368
+ raise ValueError('DataHandler is required for backtest mode.')
369
+ for asset in symbol_list:
370
+ if array:
371
+ values = bars.get_latest_bars_values(
372
+ asset, value_type, N=window)
373
+ asset_values[asset] = values[~np.isnan(values)]
374
+ else:
375
+ values = bars.get_latest_bars(asset, N=window)
376
+ asset_values[asset] = getattr(values, value_type)
377
+ elif mode == 'live':
378
+ for asset in symbol_list:
379
+ rates = Rates(symbol=asset, time_frame=tf, count=window + 1)
380
+ if array:
381
+ values = getattr(rates, value_type).values
382
+ asset_values[asset] = values[~np.isnan(values)]
383
+ else:
384
+ values = getattr(rates, value_type)
385
+ asset_values[asset] = values
386
+ if all(len(values) >= window for values in asset_values.values()):
387
+ return {a: v[-window:] for a, v in asset_values.items()}
388
+ else:
389
+ return None
390
+
391
+ def is_signal_time(self, period_count, signal_inverval) -> bool:
392
+ """
393
+ Check if we can generate a signal based on the current period count.
394
+ We use the signal interval as a form of periodicity or rebalancing period.
395
+
396
+ Args:
397
+ period_count : The current period count (e.g., number of bars).
398
+ signal_inverval : The signal interval for generating signals (e.g., every 5 bars).
399
+
400
+ Returns:
401
+ bool : True if we can generate a signal, False otherwise
402
+ """
403
+ if period_count == 0 or period_count is None:
404
+ return True
405
+ return period_count % signal_inverval == 0
406
+
407
+ def ispositions(self, symbol, strategy_id, position, max_trades, one_true=False, account=None) -> bool:
408
+ """
409
+ This function is use for live trading to check if there are open positions
410
+ for a given symbol and strategy. It is used to prevent opening more trades
411
+ than the maximum allowed trades per symbol.
412
+
413
+ Args:
414
+ symbol : The symbol for the trade.
415
+ strategy_id : The unique identifier for the strategy.
416
+ position : The position type (1: short, 0: long).
417
+ max_trades : The maximum number of trades allowed per symbol.
418
+ one_true : If True, return True if there is at least one open position.
419
+ account : The `bbstrader.metatrader.Account` object for the strategy.
420
+
421
+ Returns:
422
+ bool : True if there are open positions, False otherwise
423
+ """
424
+ account = account or Account()
425
+ positions = account.get_positions(symbol=symbol)
426
+ if positions is not None:
427
+ open_positions = [
428
+ pos for pos in positions if pos.type == position
429
+ and pos.magic == strategy_id
430
+ ]
431
+ if one_true:
432
+ return len(open_positions) in range(1, max_trades + 1)
433
+ return len(open_positions) >= max_trades
434
+ return False
435
+
436
+ def get_positions_prices(self, symbol, strategy_id, position, account=None):
437
+ """
438
+ Get the buy or sell prices for open positions of a given symbol and strategy.
439
+
440
+ Args:
441
+ symbol : The symbol for the trade.
442
+ strategy_id : The unique identifier for the strategy.
443
+ position : The position type (1: short, 0: long).
444
+ account : The `bbstrader.metatrader.Account` object for the strategy.
445
+
446
+ Returns:
447
+ prices : numpy array of buy or sell prices for open positions if any or an empty array.
448
+ """
449
+ account = account or Account()
450
+ positions = account.get_positions(symbol=symbol)
451
+ if positions is not None:
452
+ prices = np.array([
453
+ pos.price_open for pos in positions
454
+ if pos.type == position and pos.magic == strategy_id
455
+ ])
456
+ return prices
457
+ return np.array([])
458
+
459
+ def get_current_dt(self, time_zone: str = 'US/Eastern') -> datetime:
460
+ return datetime.now(pytz.timezone(time_zone))
461
+
462
+ def convert_time_zone(self, dt: datetime | int | pd.Timestamp,
463
+ from_tz: str = 'UTC',
464
+ to_tz: str = 'US/Eastern'
465
+ ) -> pd.Timestamp:
466
+ """
467
+ Convert datetime from one timezone to another.
468
+
469
+ Args:
470
+ dt : The datetime to convert.
471
+ from_tz : The timezone to convert from.
472
+ to_tz : The timezone to convert to.
473
+
474
+ Returns:
475
+ dt_to : The converted datetime.
476
+ """
477
+ from_tz = pytz.timezone(from_tz)
478
+ if isinstance(dt, datetime):
479
+ dt = pd.to_datetime(dt, unit='s')
480
+ elif isinstance(dt, int):
481
+ dt = pd.to_datetime(dt, unit='s')
482
+ if dt.tzinfo is None:
483
+ dt = dt.tz_localize(from_tz)
484
+ else:
485
+ dt = dt.tz_convert(from_tz)
486
+
487
+ dt_to = dt.tz_convert(pytz.timezone(to_tz))
488
+ return dt_to
489
+
490
+
491
+ class TWSStrategy(Strategy):
492
+ ...
bbstrader/config.py ADDED
@@ -0,0 +1,111 @@
1
+ import logging
2
+ from typing import List
3
+ from pathlib import Path
4
+ from datetime import datetime
5
+
6
+ def get_config_dir(name: str=".bbstrader") -> Path:
7
+ """
8
+ Get the path to the configuration directory.
9
+
10
+ Args:
11
+ name: The name of the configuration directory.
12
+
13
+ Returns:
14
+ The path to the configuration directory.
15
+ """
16
+ home_dir = Path.home() / name
17
+ if not home_dir.exists():
18
+ home_dir.mkdir()
19
+ return home_dir
20
+
21
+ BBSTRADER_DIR = get_config_dir()
22
+
23
+
24
+ class LogLevelFilter(logging.Filter):
25
+ def __init__(self, levels: List[int]):
26
+ """
27
+ Initializes the filter with specific logging levels.
28
+
29
+ Args:
30
+ levels: A list of logging level values (integers) to include.
31
+ """
32
+ super().__init__()
33
+ self.levels = levels
34
+
35
+ def filter(self, record: logging.LogRecord) -> bool:
36
+ """
37
+ Filters log records based on their level.
38
+
39
+ Args:
40
+ record: The log record to check.
41
+
42
+ Returns:
43
+ True if the record's level is in the allowed levels, False otherwise.
44
+ """
45
+ return record.levelno in self.levels
46
+
47
+
48
+ class CustomFormatter(logging.Formatter):
49
+ def formatTime(self, record, datefmt=None):
50
+ if hasattr(record, 'custom_time'):
51
+ # Use the custom time if provided
52
+ record.created = record.custom_time.timestamp()
53
+ return super().formatTime(record, datefmt)
54
+
55
+
56
+ class CustomLogger(logging.Logger):
57
+ def __init__(self, name, level=logging.NOTSET):
58
+ super().__init__(name, level)
59
+
60
+ def _log(self, level, msg, args, exc_info=None,
61
+ extra=None, stack_info=False, stacklevel=1, custom_time=None):
62
+ if extra is None:
63
+ extra = {}
64
+ # Add custom_time to the extra dictionary if provided
65
+ if custom_time:
66
+ extra['custom_time'] = custom_time
67
+ super()._log(level, msg, args, exc_info, extra, stack_info, stacklevel)
68
+
69
+ def info(self, msg, *args, custom_time=None, **kwargs):
70
+ self._log(logging.INFO, msg, args, custom_time=custom_time, **kwargs)
71
+
72
+ def debug(self, msg, *args, custom_time=None, **kwargs):
73
+ self._log(logging.DEBUG, msg, args, custom_time=custom_time, **kwargs)
74
+
75
+ def warning(self, msg, *args, custom_time=None, **kwargs):
76
+ self._log(logging.WARNING, msg, args, custom_time=custom_time, **kwargs)
77
+
78
+ def error(self, msg, *args, custom_time=None, **kwargs):
79
+ self._log(logging.ERROR, msg, args, custom_time=custom_time, **kwargs)
80
+
81
+ def critical(self, msg, *args, custom_time=None, **kwargs):
82
+ self._log(logging.CRITICAL, msg, args, custom_time=custom_time, **kwargs)
83
+
84
+
85
+ def config_logger(log_file: str, console_log=True):
86
+
87
+ # Use the CustomLogger
88
+ logging.setLoggerClass(CustomLogger)
89
+ logger = logging.getLogger(__name__)
90
+ logger.setLevel(logging.DEBUG)
91
+
92
+ # File handler
93
+ file_handler = logging.FileHandler(log_file)
94
+ file_handler.setLevel(logging.INFO)
95
+
96
+ # Custom formatter
97
+ formatter = CustomFormatter(
98
+ '%(asctime)s - %(levelname)s: %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
99
+ file_handler.setFormatter(formatter)
100
+
101
+ # Add the handler to the logger
102
+ logger.addHandler(file_handler)
103
+
104
+ if console_log:
105
+ # Handler for the console with a different level
106
+ console_handler = logging.StreamHandler()
107
+ console_handler.setLevel(logging.DEBUG)
108
+ console_handler.setFormatter(formatter)
109
+ logger.addHandler(console_handler)
110
+
111
+ return logger
@@ -1,6 +1,6 @@
1
1
 
2
- from bbstrader.metatrader.account import Account
3
- from bbstrader.metatrader.rates import Rates
4
- from bbstrader.metatrader.risk import RiskManagement
5
- from bbstrader.metatrader.trade import Trade, create_trade_instance
2
+ from bbstrader.metatrader.account import *
3
+ from bbstrader.metatrader.rates import *
4
+ from bbstrader.metatrader.risk import *
5
+ from bbstrader.metatrader.trade import *
6
6
  from bbstrader.metatrader.utils import *