Qubx 0.6.95__cp312-cp312-manylinux_2_39_x86_64.whl → 0.7.4__cp312-cp312-manylinux_2_39_x86_64.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 Qubx might be problematic. Click here for more details.

Files changed (66) hide show
  1. qubx/backtester/utils.py +1 -1
  2. qubx/connectors/ccxt/data.py +32 -29
  3. qubx/connectors/xlighter/account.py +101 -34
  4. qubx/connectors/xlighter/broker.py +117 -273
  5. qubx/connectors/xlighter/client.py +53 -3
  6. qubx/connectors/xlighter/constants.py +2 -0
  7. qubx/connectors/xlighter/data.py +50 -11
  8. qubx/connectors/xlighter/factory.py +34 -3
  9. qubx/connectors/xlighter/handlers/orderbook.py +225 -30
  10. qubx/connectors/xlighter/handlers/trades.py +4 -4
  11. qubx/connectors/xlighter/nonce.py +21 -0
  12. qubx/connectors/xlighter/rate_limits.py +96 -0
  13. qubx/connectors/xlighter/websocket.py +31 -1
  14. qubx/core/basics.py +23 -1
  15. qubx/core/context.py +169 -47
  16. qubx/core/exceptions.py +4 -0
  17. qubx/core/interfaces.py +61 -8
  18. qubx/core/mixins/processing.py +17 -3
  19. qubx/core/mixins/trading.py +30 -5
  20. qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
  21. qubx/core/series.pyi +15 -1
  22. qubx/core/series.pyx +110 -2
  23. qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
  24. qubx/data/readers.py +116 -2
  25. qubx/data/storages/questdb.py +1 -1
  26. qubx/emitters/composite.py +17 -1
  27. qubx/emitters/questdb.py +81 -15
  28. qubx/exporters/formatters/slack.py +6 -4
  29. qubx/exporters/slack.py +35 -74
  30. qubx/gathering/simplest.py +1 -1
  31. qubx/notifications/__init__.py +5 -5
  32. qubx/notifications/composite.py +29 -17
  33. qubx/notifications/slack.py +109 -132
  34. qubx/pandaz/ta.py +32 -0
  35. qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
  36. qubx/ta/indicators.pyi +11 -0
  37. qubx/ta/indicators.pyx +253 -1
  38. qubx/utils/hft/__init__.py +5 -0
  39. qubx/utils/hft/numba_utils.py +12 -0
  40. qubx/utils/hft/orderbook.cpython-312-x86_64-linux-gnu.so +0 -0
  41. qubx/utils/hft/orderbook.pyi +177 -0
  42. qubx/utils/hft/orderbook.pyx +416 -0
  43. qubx/utils/nonce.py +53 -0
  44. qubx/utils/orderbook.py +1 -276
  45. qubx/utils/rate_limiter.py +225 -0
  46. qubx/utils/ringbuffer.cpython-312-x86_64-linux-gnu.so +0 -0
  47. qubx/utils/ringbuffer.pxd +17 -0
  48. qubx/utils/ringbuffer.pyi +197 -0
  49. qubx/utils/ringbuffer.pyx +253 -0
  50. qubx/utils/runner/factory.py +11 -14
  51. qubx/utils/runner/runner.py +3 -3
  52. qubx/utils/runner/textual/app.py +41 -33
  53. qubx/utils/runner/textual/handlers.py +16 -5
  54. qubx/utils/runner/textual/init_code.py +21 -24
  55. qubx/utils/runner/textual/widgets/orders_table.py +208 -27
  56. qubx/utils/runner/textual/widgets/positions_table.py +288 -49
  57. qubx/utils/runner/textual/widgets/quotes_table.py +166 -31
  58. qubx/utils/runner/textual/widgets/repl_output.py +87 -82
  59. qubx/utils/slack.py +177 -0
  60. qubx/utils/time.py +97 -1
  61. qubx/utils/websocket_manager.py +10 -5
  62. {qubx-0.6.95.dist-info → qubx-0.7.4.dist-info}/METADATA +2 -2
  63. {qubx-0.6.95.dist-info → qubx-0.7.4.dist-info}/RECORD +66 -52
  64. {qubx-0.6.95.dist-info → qubx-0.7.4.dist-info}/WHEEL +0 -0
  65. {qubx-0.6.95.dist-info → qubx-0.7.4.dist-info}/entry_points.txt +0 -0
  66. {qubx-0.6.95.dist-info → qubx-0.7.4.dist-info}/licenses/LICENSE +0 -0
qubx/backtester/utils.py CHANGED
@@ -155,7 +155,7 @@ class SimulatedLogFormatter:
155
155
  now = self.time_provider.time().astype("datetime64[us]").item().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
156
156
 
157
157
  # prefix = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] " % record["level"].icon
158
- prefix = f"<lc>{now}</lc> [<level>{record['level'].icon}</level>] "
158
+ prefix = f"<lc>{now}</lc> [<level>{record['level'].icon}</level>] <cyan>({{module}})</cyan> "
159
159
 
160
160
  if record["exception"] is not None:
161
161
  record["extra"]["stack"] = stackprinter.format(record["exception"], style="darkbg3")
@@ -43,18 +43,15 @@ class CcxtDataProvider(IDataProvider):
43
43
  ):
44
44
  # Store the exchange manager (always ExchangeManager now)
45
45
  self._exchange_manager = exchange_manager
46
-
46
+
47
47
  self.time_provider = time_provider
48
48
  self.channel = channel
49
49
  self.max_ws_retries = max_ws_retries
50
50
  self._warmup_timeout = warmup_timeout
51
51
  self._health_monitor = health_monitor or DummyHealthMonitor()
52
52
 
53
- self._data_arrival_listeners: List[IDataArrivalListener] = [
54
- self._health_monitor,
55
- self._exchange_manager
56
- ]
57
-
53
+ self._data_arrival_listeners: List[IDataArrivalListener] = [self._health_monitor, self._exchange_manager]
54
+
58
55
  logger.debug(f"Registered {len(self._data_arrival_listeners)} data arrival listeners")
59
56
 
60
57
  # Core components - access exchange directly via exchange_manager.exchange
@@ -106,10 +103,10 @@ class CcxtDataProvider(IDataProvider):
106
103
  def _loop(self) -> AsyncThreadLoop:
107
104
  """Get current AsyncThreadLoop for the exchange."""
108
105
  return AsyncThreadLoop(self._exchange_manager.exchange.asyncio_loop)
109
-
106
+
110
107
  def notify_data_arrival(self, event_type: str, event_time: dt_64) -> None:
111
108
  """Notify all registered listeners about data arrival.
112
-
109
+
113
110
  Args:
114
111
  event_type: Type of data event (e.g., "ohlcv:BTC/USDT:1m")
115
112
  event_time: Timestamp of the data event
@@ -120,10 +117,6 @@ class CcxtDataProvider(IDataProvider):
120
117
  except Exception as e:
121
118
  logger.error(f"Error notifying data arrival listener {type(listener).__name__}: {e}")
122
119
 
123
-
124
-
125
-
126
-
127
120
  @property
128
121
  def is_simulation(self) -> bool:
129
122
  return False
@@ -219,7 +212,7 @@ class CcxtDataProvider(IDataProvider):
219
212
  self._warmup_service.execute_warmup(warmups)
220
213
 
221
214
  def get_quote(self, instrument: Instrument) -> Quote | None:
222
- return self._last_quotes[instrument]
215
+ return self._last_quotes.get(instrument, None)
223
216
 
224
217
  def get_ohlc(self, instrument: Instrument, timeframe: str, nbarsback: int) -> List[Bar]:
225
218
  """Get historical OHLC data (delegated to OhlcDataHandler)."""
@@ -259,9 +252,9 @@ class CcxtDataProvider(IDataProvider):
259
252
  except Exception as e:
260
253
  logger.error(f"Error stopping subscription {subscription_type}: {e}")
261
254
 
262
- # Stop ExchangeManager monitoring
255
+ # Stop ExchangeManager monitoring
263
256
  self._exchange_manager.stop_monitoring()
264
-
257
+
265
258
  # Close exchange connection
266
259
  if hasattr(self._exchange_manager.exchange, "close"):
267
260
  future = self._loop.submit(self._exchange_manager.exchange.close()) # type: ignore
@@ -277,47 +270,57 @@ class CcxtDataProvider(IDataProvider):
277
270
 
278
271
  def _handle_exchange_recreation(self) -> None:
279
272
  """Handle exchange recreation by resubscribing to all active subscriptions."""
280
- logger.info(f"<yellow>{self._exchange_id}</yellow> Handling exchange recreation - resubscribing to active subscriptions")
281
-
273
+ logger.info(
274
+ f"<yellow>{self._exchange_id}</yellow> Handling exchange recreation - resubscribing to active subscriptions"
275
+ )
276
+
282
277
  # Get snapshot of current subscriptions before cleanup
283
278
  active_subscriptions = self._subscription_manager.get_subscriptions()
284
-
279
+
285
280
  resubscription_data = []
286
281
  for subscription_type in active_subscriptions:
287
282
  instruments = self._subscription_manager.get_subscribed_instruments(subscription_type)
288
283
  if instruments:
289
284
  resubscription_data.append((subscription_type, instruments))
290
-
291
- logger.info(f"<yellow>{self._exchange_id}</yellow> Found {len(resubscription_data)} active subscriptions to recreate")
292
-
285
+
286
+ logger.info(
287
+ f"<yellow>{self._exchange_id}</yellow> Found {len(resubscription_data)} active subscriptions to recreate"
288
+ )
289
+
293
290
  # Track success/failure counts for reporting
294
291
  successful_resubscriptions = 0
295
292
  failed_resubscriptions = 0
296
-
293
+
297
294
  # Clean resubscription: unsubscribe then subscribe for each subscription type
298
295
  for subscription_type, instruments in resubscription_data:
299
296
  try:
300
- logger.info(f"<yellow>{self._exchange_id}</yellow> Resubscribing to {subscription_type} with {len(instruments)} instruments")
301
-
297
+ logger.info(
298
+ f"<yellow>{self._exchange_id}</yellow> Resubscribing to {subscription_type} with {len(instruments)} instruments"
299
+ )
300
+
302
301
  self.unsubscribe(subscription_type, instruments)
303
302
 
304
303
  # Resubscribe with reset=True to ensure clean state
305
304
  self.subscribe(subscription_type, instruments, reset=True)
306
-
305
+
307
306
  successful_resubscriptions += 1
308
307
  logger.debug(f"<yellow>{self._exchange_id}</yellow> Successfully resubscribed to {subscription_type}")
309
-
308
+
310
309
  except Exception as e:
311
310
  failed_resubscriptions += 1
312
311
  logger.error(f"<yellow>{self._exchange_id}</yellow> Failed to resubscribe to {subscription_type}: {e}")
313
312
  # Continue with other subscriptions even if one fails
314
-
313
+
315
314
  # Report final status
316
315
  total_subscriptions = len(resubscription_data)
317
316
  if failed_resubscriptions == 0:
318
- logger.info(f"<yellow>{self._exchange_id}</yellow> Exchange recreation resubscription completed successfully ({total_subscriptions}/{total_subscriptions})")
317
+ logger.info(
318
+ f"<yellow>{self._exchange_id}</yellow> Exchange recreation resubscription completed successfully ({total_subscriptions}/{total_subscriptions})"
319
+ )
319
320
  else:
320
- logger.warning(f"<yellow>{self._exchange_id}</yellow> Exchange recreation resubscription completed with errors ({successful_resubscriptions}/{total_subscriptions} successful)")
321
+ logger.warning(
322
+ f"<yellow>{self._exchange_id}</yellow> Exchange recreation resubscription completed with errors ({successful_resubscriptions}/{total_subscriptions} successful)"
323
+ )
321
324
 
322
325
  @property
323
326
  def subscribed_instruments(self) -> Set[Instrument]:
@@ -12,16 +12,16 @@ through channel for strategy notification but do not update positions.
12
12
 
13
13
  import asyncio
14
14
  import time
15
- from typing import Optional
15
+ from typing import Awaitable, Callable, Optional
16
16
 
17
17
  import numpy as np
18
+ import pandas as pd
18
19
 
19
20
  from qubx import logger
20
21
  from qubx.core.account import BasicAccountProcessor
21
22
  from qubx.core.basics import (
22
23
  ZERO_COSTS,
23
24
  CtrlChannel,
24
- DataType,
25
25
  Deal,
26
26
  Instrument,
27
27
  ITimeProvider,
@@ -29,6 +29,7 @@ from qubx.core.basics import (
29
29
  TransactionCostsCalculator,
30
30
  )
31
31
  from qubx.core.interfaces import ISubscriptionManager
32
+ from qubx.core.utils import recognize_timeframe
32
33
  from qubx.utils.misc import AsyncThreadLoop
33
34
 
34
35
  from .client import LighterClient
@@ -121,8 +122,9 @@ class LighterAccountProcessor(BasicAccountProcessor):
121
122
  self._processed_tx_hashes: set[str] = set() # Track processed transaction hashes
122
123
 
123
124
  self._account_stats_initialized = False
125
+ self._account_positions_initialized = False
124
126
 
125
- logger.info(f"Initialized LighterAccountProcessor for account {account_id}")
127
+ self.__info(f"Initialized LighterAccountProcessor for account {account_id}")
126
128
 
127
129
  def set_subscription_manager(self, manager: ISubscriptionManager) -> None:
128
130
  """Set the subscription manager (required by interface)"""
@@ -131,37 +133,37 @@ class LighterAccountProcessor(BasicAccountProcessor):
131
133
  def get_total_capital(self, exchange: str | None = None) -> float:
132
134
  if not self._account_stats_initialized:
133
135
  self._async_loop.submit(self._start_subscriptions())
134
- self._wait_for_account_stats_initialized()
136
+ self._wait_for_account_initialized()
135
137
  return super().get_total_capital(exchange)
136
138
 
137
139
  def start(self):
138
140
  """Start WebSocket subscriptions for account data"""
139
141
  if self._is_running:
140
- logger.debug("Account processor is already running")
142
+ self.__debug("Account processor is already running")
141
143
  return
142
144
 
143
145
  if not self.channel or not self.channel.control.is_set():
144
- logger.warning("Channel not set or control not active, cannot start")
146
+ self.__warning("Channel not set or control not active, cannot start")
145
147
  return
146
148
 
147
149
  self._is_running = True
148
150
 
149
151
  # Start subscription tasks using AsyncThreadLoop
150
- logger.info("Starting Lighter account subscriptions")
152
+ self.__info("Starting Lighter account subscriptions")
151
153
 
152
154
  if not self._account_stats_initialized:
153
155
  # Submit connection and subscription tasks to the event loop
154
156
  self._async_loop.submit(self._start_subscriptions())
155
- self._wait_for_account_stats_initialized()
157
+ self._wait_for_account_initialized()
156
158
 
157
- logger.info("Lighter account subscriptions started")
159
+ self.__info("Lighter account subscriptions started")
158
160
 
159
161
  def stop(self):
160
162
  """Stop all WebSocket subscriptions"""
161
163
  if not self._is_running:
162
164
  return
163
165
 
164
- logger.info("Stopping Lighter account subscriptions")
166
+ self.__info("Stopping Lighter account subscriptions")
165
167
 
166
168
  # Cancel all subscription tasks
167
169
  for task in self._subscription_tasks:
@@ -170,7 +172,7 @@ class LighterAccountProcessor(BasicAccountProcessor):
170
172
 
171
173
  self._subscription_tasks.clear()
172
174
  self._is_running = False
173
- logger.info("Lighter account subscriptions stopped")
175
+ self.__info("Lighter account subscriptions stopped")
174
176
 
175
177
  def process_deals(self, instrument: Instrument, deals: list[Deal], is_snapshot: bool = False) -> None:
176
178
  """
@@ -200,12 +202,12 @@ class LighterAccountProcessor(BasicAccountProcessor):
200
202
  # Add fee to position's commission tracking
201
203
  position.commissions += deal.fee_amount
202
204
 
203
- logger.debug(
205
+ self.__debug(
204
206
  f"Tracked fee for {instrument.symbol}: {deal.fee_amount:.6f} {deal.fee_currency} "
205
207
  f"(total commissions: {position.commissions:.6f})"
206
208
  )
207
209
 
208
- logger.debug(
210
+ self.__debug(
209
211
  f"Processed {len(deals)} deal(s) for {instrument.symbol} - fees tracked, positions synced from account_all"
210
212
  )
211
213
 
@@ -227,7 +229,7 @@ class LighterAccountProcessor(BasicAccountProcessor):
227
229
  # Get the existing order stored under client_id
228
230
  existing_order = self._active_orders[order.client_id]
229
231
 
230
- logger.debug(f"Migrating order: client_id={order.client_id} → server_id={order.id}")
232
+ self.__debug(f"Migrating order: client_id={order.client_id} → server_id={order.id}")
231
233
 
232
234
  # Remove from old location
233
235
  self._active_orders.pop(order.client_id)
@@ -245,13 +247,13 @@ class LighterAccountProcessor(BasicAccountProcessor):
245
247
  # The base class will now find the existing order under order.id and merge in place
246
248
  super().process_order(order, update_locked_value)
247
249
 
248
- def _wait_for_account_stats_initialized(self):
250
+ def _wait_for_account_initialized(self):
249
251
  max_wait_time = 20.0 # seconds
250
252
  elapsed = 0.0
251
253
  interval = 0.1
252
- while not self._account_stats_initialized:
254
+ while not self._account_stats_initialized or not self._account_positions_initialized:
253
255
  if elapsed >= max_wait_time:
254
- raise TimeoutError(f"Account stats were not initialized within {max_wait_time} seconds")
256
+ raise TimeoutError(f"Account was not initialized within {max_wait_time} seconds")
255
257
  time.sleep(interval)
256
258
  elapsed += interval
257
259
 
@@ -260,26 +262,27 @@ class LighterAccountProcessor(BasicAccountProcessor):
260
262
  try:
261
263
  # Ensure WebSocket is connected
262
264
  if not self.ws_manager.is_connected:
263
- logger.info("Connecting to Lighter WebSocket...")
265
+ self.__info("Connecting to Lighter WebSocket...")
264
266
  await self.ws_manager.connect()
265
- logger.info("Connected to Lighter WebSocket")
267
+ self.__info("Connected to Lighter WebSocket")
266
268
 
267
269
  # Start all subscriptions
268
270
  await self._subscribe_account_all()
269
271
  await self._subscribe_account_all_orders()
270
272
  await self._subscribe_user_stats()
273
+ await self._poller(name="sync_orders", coroutine=self._sync_orders, interval="1min")
271
274
 
272
275
  except Exception as e:
273
- logger.error(f"Failed to start subscriptions: {e}")
276
+ self.__error(f"Failed to start subscriptions: {e}")
274
277
  self._is_running = False
275
278
  raise
276
279
 
277
280
  async def _subscribe_account_all(self):
278
281
  try:
279
282
  await self.ws_manager.subscribe_account_all(self._lighter_account_index, self._handle_account_all_message)
280
- logger.info(f"Subscribed to account_all for account {self._lighter_account_index}")
283
+ self.__info(f"Subscribed to account_all for account {self._lighter_account_index}")
281
284
  except Exception as e:
282
- logger.error(f"Failed to subscribe to account_all for account {self._lighter_account_index}: {e}")
285
+ self.__error(f"Failed to subscribe to account_all for account {self._lighter_account_index}: {e}")
283
286
  raise
284
287
 
285
288
  async def _subscribe_account_all_orders(self):
@@ -287,19 +290,64 @@ class LighterAccountProcessor(BasicAccountProcessor):
287
290
  await self.ws_manager.subscribe_account_all_orders(
288
291
  self._lighter_account_index, self._handle_account_all_orders_message
289
292
  )
290
- logger.info(f"Subscribed to account_all_orders for account {self._lighter_account_index}")
293
+ self.__info(f"Subscribed to account_all_orders for account {self._lighter_account_index}")
291
294
  except Exception as e:
292
- logger.error(f"Failed to subscribe to account_all_orders for account {self._lighter_account_index}: {e}")
295
+ self.__error(f"Failed to subscribe to account_all_orders for account {self._lighter_account_index}: {e}")
293
296
  raise
294
297
 
295
298
  async def _subscribe_user_stats(self):
296
299
  try:
297
300
  await self.ws_manager.subscribe_user_stats(self._lighter_account_index, self._handle_user_stats_message)
298
- logger.info(f"Subscribed to user_stats for account {self._lighter_account_index}")
301
+ self.__info(f"Subscribed to user_stats for account {self._lighter_account_index}")
299
302
  except Exception as e:
300
- logger.error(f"Failed to subscribe to user_stats for account {self._lighter_account_index}: {e}")
303
+ self.__error(f"Failed to subscribe to user_stats for account {self._lighter_account_index}: {e}")
301
304
  raise
302
305
 
306
+ async def _sync_orders(self):
307
+ now = self.time_provider.time()
308
+ orders = self.get_orders()
309
+ remove_orders = []
310
+ for order_id, order in orders.items():
311
+ if order.status == "NEW" and order.time < now - recognize_timeframe("1min"):
312
+ remove_orders.append(order_id)
313
+ for order_id in remove_orders:
314
+ self.remove_order(order_id)
315
+
316
+ async def _poller(
317
+ self,
318
+ name: str,
319
+ coroutine: Callable[[], Awaitable],
320
+ interval: str,
321
+ backoff: str | None = None,
322
+ ):
323
+ sleep_time = pd.Timedelta(interval).total_seconds()
324
+ retries = 0
325
+
326
+ if backoff is not None:
327
+ sleep_time = pd.Timedelta(backoff).total_seconds()
328
+ await asyncio.sleep(sleep_time)
329
+
330
+ while self.channel.control.is_set():
331
+ try:
332
+ await coroutine()
333
+ retries = 0 # Reset retry counter on success
334
+ except Exception as e:
335
+ if not self.channel.control.is_set():
336
+ # If the channel is closed, then ignore all exceptions and exit
337
+ break
338
+ logger.error(f"Unexpected error during account polling: {e}")
339
+ logger.exception(e)
340
+ retries += 1
341
+ if retries >= self.max_retries:
342
+ logger.error(f"Max retries ({self.max_retries}) reached. Stopping poller.")
343
+ break
344
+ finally:
345
+ if not self.channel.control.is_set():
346
+ break
347
+ await asyncio.sleep(min(sleep_time * (2 ** (retries)), 60)) # Exponential backoff capped at 60s
348
+
349
+ logger.debug(f"{name} polling task has been stopped")
350
+
303
351
  async def _handle_account_all_message(self, message: dict):
304
352
  """
305
353
  Handle account_all WebSocket messages (primary channel).
@@ -335,11 +383,15 @@ class LighterAccountProcessor(BasicAccountProcessor):
335
383
  if position.last_update_price == 0 or np.isnan(position.last_update_price):
336
384
  position.last_update_price = pos_state.avg_entry_price
337
385
 
338
- logger.debug(
386
+ self.__debug(
339
387
  f"Synced position: {instrument.symbol} = {pos_state.quantity:+.4f} "
340
388
  f"@ avg_price={pos_state.avg_entry_price:.4f}"
341
389
  )
342
390
 
391
+ if not self._account_positions_initialized:
392
+ self._account_positions_initialized = True
393
+ self.__info("Account positions initialized")
394
+
343
395
  # Send deals through channel for strategy notification
344
396
  # Note: process_deals is overridden to track fees without updating positions
345
397
  for instrument, deal in deals:
@@ -347,7 +399,7 @@ class LighterAccountProcessor(BasicAccountProcessor):
347
399
  # False means not a snapshot
348
400
  self.channel.send((instrument, "deals", [deal], False))
349
401
 
350
- logger.debug(
402
+ self.__debug(
351
403
  f"Sent deal: {instrument.symbol} {deal.amount:+.4f} @ {deal.price:.4f} "
352
404
  f"fee={deal.fee_amount:.6f} (id={deal.id})"
353
405
  )
@@ -363,7 +415,7 @@ class LighterAccountProcessor(BasicAccountProcessor):
363
415
  # )
364
416
 
365
417
  except Exception as e:
366
- logger.error(f"Error handling account_all message: {e}")
418
+ self.__error(f"Error handling account_all message: {e}")
367
419
  logger.exception(e)
368
420
 
369
421
  async def _handle_account_all_orders_message(self, message: dict):
@@ -384,13 +436,13 @@ class LighterAccountProcessor(BasicAccountProcessor):
384
436
  # False means not a snapshot
385
437
  self.channel.send((instrument, "order", order, False))
386
438
 
387
- logger.debug(
439
+ self.__debug(
388
440
  f"Sent order: {instrument.symbol} {order.side} {order.quantity:+.4f} @ {order.price:.4f} "
389
441
  f"[{order.status}] (order_id={order.id})"
390
442
  )
391
443
 
392
444
  except Exception as e:
393
- logger.error(f"Error handling account_all_orders message: {e}")
445
+ self.__error(f"Error handling account_all_orders message: {e}")
394
446
  logger.exception(e)
395
447
 
396
448
  async def _handle_user_stats_message(self, message: dict):
@@ -407,15 +459,30 @@ class LighterAccountProcessor(BasicAccountProcessor):
407
459
  for currency, balance in balances.items():
408
460
  self.update_balance(currency, balance.total, balance.locked)
409
461
 
410
- logger.debug(
462
+ self.__debug(
411
463
  f"Updated balance - {currency}: total={balance.total:.2f}, "
412
464
  f"free={balance.free:.2f}, locked={balance.locked:.2f}"
413
465
  )
414
466
 
415
467
  if not self._account_stats_initialized:
416
468
  self._account_stats_initialized = True
417
- logger.debug("Account stats initialized")
469
+ self.__info("Account stats initialized")
418
470
 
419
471
  except Exception as e:
420
- logger.error(f"Error handling user_stats message: {e}")
472
+ self.__error(f"Error handling user_stats message: {e}")
421
473
  logger.exception(e)
474
+
475
+ def __info(self, msg: str):
476
+ logger.info(self.__format(msg))
477
+
478
+ def __debug(self, msg: str):
479
+ logger.debug(self.__format(msg))
480
+
481
+ def __warning(self, msg: str):
482
+ logger.warning(self.__format(msg))
483
+
484
+ def __error(self, msg: str):
485
+ logger.error(self.__format(msg))
486
+
487
+ def __format(self, msg: str):
488
+ return f"<green>[Lighter]</green> {msg}"