Qubx 0.6.71__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.72__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.

qubx/backtester/runner.py CHANGED
@@ -4,10 +4,9 @@ import numpy as np
4
4
  import pandas as pd
5
5
  from tqdm.auto import tqdm
6
6
 
7
- from qubx import logger
7
+ from qubx import QubxLogConfig, logger
8
8
  from qubx.backtester.sentinels import NoDataContinue
9
9
  from qubx.backtester.simulated_data import IterableSimulationData
10
- from qubx.backtester.utils import SimulationDataConfig, TimeGuardedWrapper
11
10
  from qubx.core.account import CompositeAccountProcessor
12
11
  from qubx.core.basics import SW, DataType, Instrument, TransactionCostsCalculator
13
12
  from qubx.core.context import StrategyContext
@@ -41,6 +40,7 @@ from .utils import (
41
40
  SimulatedTimeProvider,
42
41
  SimulationDataConfig,
43
42
  SimulationSetup,
43
+ TimeGuardedWrapper,
44
44
  )
45
45
 
46
46
 
@@ -356,8 +356,9 @@ class SimulationRunner:
356
356
  return False # No scheduled events, stop simulation
357
357
 
358
358
  def print_latency_report(self) -> None:
359
- _l_r = SW.latency_report()
360
- if _l_r is not None:
359
+ if (_l_r := SW.latency_report()) is not None:
360
+ _llvl = QubxLogConfig.get_log_level()
361
+ QubxLogConfig.set_log_level("INFO")
361
362
  logger.info(
362
363
  "<BLUE> Time spent in simulation report </BLUE>\n<r>"
363
364
  + _frame_to_str(
@@ -365,6 +366,7 @@ class SimulationRunner:
365
366
  )
366
367
  + "</r>"
367
368
  )
369
+ QubxLogConfig.set_log_level(_llvl)
368
370
 
369
371
  def _create_backtest_context(self) -> IStrategyContext:
370
372
  logger.debug(
@@ -1,224 +1,206 @@
1
1
  """
2
- Generic polling adapter to convert CCXT fetch_* methods into watch_* behavior.
2
+ Simplified polling adapter to convert CCXT fetch_* methods into watch_* behavior.
3
3
 
4
- This adapter allows any exchange's fetch_* method to be used as a watch_* method
5
- by implementing intelligent polling with proper resource management and unwatch functionality.
4
+ This adapter provides a much simpler approach:
5
+ - No background tasks or queues
6
+ - get_next_data() waits until it's time to poll, then polls synchronously
7
+ - Time-aligned polling (e.g., 11:30, 11:35, 11:40 for 5-minute intervals)
8
+ - Immediate polling when symbols change
6
9
  """
7
10
 
8
11
  import asyncio
9
- import concurrent.futures
12
+ import math
10
13
  import time
11
- from asyncio import CancelledError
14
+ from dataclasses import dataclass
12
15
  from typing import Any, Callable, Dict, List, Optional, Set
13
16
 
14
17
  from qubx import logger
15
- from qubx.utils.misc import AsyncThreadLoop
18
+
19
+ # Constants
20
+ DEFAULT_POLL_INTERVAL = 300 # 5 minutes
21
+ MIN_POLL_INTERVAL = 1 # 1 second minimum
22
+ MAX_POLL_INTERVAL = 3600 # 1 hour maximum
23
+
24
+
25
+ @dataclass
26
+ class PollingConfig:
27
+ """Configuration for polling adapter."""
28
+
29
+ poll_interval_seconds: float = DEFAULT_POLL_INTERVAL
30
+
31
+ def __post_init__(self):
32
+ """Validate configuration after initialization."""
33
+ if not MIN_POLL_INTERVAL <= self.poll_interval_seconds <= MAX_POLL_INTERVAL:
34
+ raise ValueError(
35
+ f"poll_interval_seconds must be between {MIN_POLL_INTERVAL} and {MAX_POLL_INTERVAL}, "
36
+ f"got {self.poll_interval_seconds}"
37
+ )
16
38
 
17
39
 
18
40
  class PollingToWebSocketAdapter:
19
41
  """
20
- Generic adapter to convert fetch_* methods to watch_* behavior using intelligent polling.
21
-
22
- This adapter provides:
23
- - Dynamic symbol management (add/remove symbols during runtime)
24
- - Comprehensive unwatch functionality (symbol-level and complete shutdown)
25
- - Proper resource cleanup and error handling
26
- - Thread-safe operations for concurrent access
27
- - Configurable polling intervals
42
+ Simplified polling adapter that polls synchronously when data is requested.
43
+
44
+ Key features:
45
+ - No background tasks or queues
46
+ - Time-aligned polling (respects clock boundaries)
47
+ - Immediate polling when symbols change
48
+ - Thread-safe symbol management
28
49
  """
29
50
 
30
51
  def __init__(
31
52
  self,
32
53
  fetch_method: Callable,
33
- poll_interval_seconds: int = 300, # 5 minutes default
34
54
  symbols: Optional[List[str]] = None,
35
55
  params: Optional[Dict[str, Any]] = None,
36
- use_time_boundaries: bool = False, # Make boundary logic optional
37
- boundary_tolerance_seconds: int = 30,
38
- event_loop: Optional[asyncio.AbstractEventLoop] = None, # Optional explicit loop
56
+ config: Optional[PollingConfig] = None,
39
57
  ):
40
58
  """
41
- Initialize the polling adapter.
59
+ Initialize the simplified polling adapter.
42
60
 
43
61
  Args:
44
- fetch_method: The CCXT fetch_* method to call (e.g., self.fetch_funding_rates)
45
- poll_interval_seconds: How often to poll in seconds (default: 300 = 5 minutes)
62
+ fetch_method: The CCXT fetch_* method to call
46
63
  symbols: Initial list of symbols to watch
47
64
  params: Additional parameters for fetch_method
48
- use_time_boundaries: Whether to align polling to time boundaries (like open_interest.py)
49
- boundary_tolerance_seconds: Tolerance for boundary alignment (only used if use_time_boundaries=True)
50
- event_loop: Optional explicit asyncio event loop to use for background tasks
65
+ config: PollingConfig instance (uses default if None)
51
66
  """
67
+ self.config = config if config is not None else PollingConfig()
52
68
  self.fetch_method = fetch_method
53
- self.poll_interval_seconds = poll_interval_seconds
54
69
  self.params = params or {}
55
- self.adapter_id = f"polling_adapter_{id(self)}" # Auto-generated for logging
56
- self.use_time_boundaries = use_time_boundaries
57
- self.boundary_tolerance_seconds = boundary_tolerance_seconds
58
- self.event_loop = event_loop # Store explicit loop if provided
70
+ self.adapter_id = f"polling_adapter_{id(self)}"
59
71
 
60
72
  # Thread-safe symbol management
61
73
  self._symbols_lock = asyncio.Lock()
62
74
  self._symbols: Set[str] = set(symbols or [])
63
75
 
64
- # Polling state management
65
- self._polling_task: Optional[asyncio.Task] = None
66
- self._stop_event = asyncio.Event()
67
- self._is_running = False
76
+ # Polling state
77
+ self._last_poll_time: Optional[float] = None
78
+ self._symbols_changed = False # Flag to trigger immediate poll
68
79
 
69
- # Statistics for monitoring
80
+ # Statistics
70
81
  self._poll_count = 0
71
82
  self._error_count = 0
72
- self._last_poll_time: Optional[float] = None
73
83
 
74
- # Data management for awaitable pattern
75
- self._data_queue: asyncio.Queue = asyncio.Queue()
76
- self._latest_data: Optional[Dict[str, Any]] = None
77
- self._data_condition = asyncio.Condition()
78
-
79
- async def start_watching(self) -> None:
84
+ async def get_next_data(self) -> Dict[str, Any]:
80
85
  """
81
- Start the polling loop in background.
86
+ Get the next available data by waiting until it's time to poll, then polling.
82
87
 
83
- This method starts the background polling task that will continuously
84
- fetch data for the configured symbols and store latest results.
88
+ This method:
89
+ 1. Checks if symbols changed (immediate poll)
90
+ 2. Calculates when next poll should happen based on time alignment
91
+ 3. Waits until that time
92
+ 4. Polls and returns fresh data
93
+
94
+ Returns:
95
+ Dictionary containing fetched data for symbols
85
96
  """
86
- if self._is_running:
87
- logger.warning(f"Adapter {self.adapter_id} is already running")
88
- return
97
+ async with self._symbols_lock:
98
+ current_symbols = list(self._symbols)
99
+ symbols_changed = self._symbols_changed
89
100
 
90
- self._is_running = True
91
- self._stop_event.clear()
101
+ if not current_symbols:
102
+ raise ValueError(f"No symbols configured for adapter {self.adapter_id}")
92
103
 
93
- logger.debug(f"Starting polling adapter {self.adapter_id} with {len(self._symbols)} symbols")
104
+ # If symbols changed, poll immediately
105
+ if symbols_changed:
106
+ logger.debug(f"Symbols changed, polling immediately for adapter {self.adapter_id}")
107
+ async with self._symbols_lock:
108
+ self._symbols_changed = False
109
+ return await self._poll_now(current_symbols)
94
110
 
95
- # Do immediate initial poll before starting background loop
96
- async with self._symbols_lock:
97
- current_symbols = list(self._symbols)
98
-
99
- if current_symbols:
100
- try:
101
- await self._poll_once(current_symbols)
102
- except Exception as e:
103
- logger.error(f"Initial poll failed for adapter {self.adapter_id}: {e}")
104
-
105
- # Start the polling task for subsequent polls
106
- if self.event_loop is not None:
107
- # Use explicit loop if provided (for testing environments)
108
-
109
- # Always use AsyncThreadLoop for cross-thread task submission
110
- # This handles both same-thread and cross-thread cases properly
111
- async_loop = AsyncThreadLoop(self.event_loop)
112
- self._polling_task = async_loop.submit(self._polling_loop())
113
- else:
114
- # Use current event loop (normal operation)
115
- self._polling_task = asyncio.create_task(self._polling_loop())
111
+ # Calculate wait time for next aligned poll
112
+ wait_time = self._calculate_wait_time()
116
113
 
117
- async def get_next_data(self) -> Dict[str, Any]:
114
+ if wait_time > 0:
115
+ logger.debug(f"Waiting {wait_time:.1f}s for next poll cycle for adapter {self.adapter_id}")
116
+ await asyncio.sleep(wait_time)
117
+
118
+ # Time to poll
119
+ logger.debug(f"Polling now for adapter {self.adapter_id}")
120
+ return await self._poll_now(current_symbols)
121
+
122
+ def _calculate_wait_time(self) -> float:
118
123
  """
119
- Get the next available data (CCXT awaitable pattern).
120
-
121
- This is the method that watch_* methods should call to get data.
122
- It waits for new data from the polling task.
123
-
124
+ Calculate how long to wait until the next aligned poll time.
125
+
126
+ For intervals >= 1 minute: aligns to clock boundaries (11:30, 11:35, 11:40)
127
+ For intervals < 1 minute: uses simple interval-based timing
128
+
124
129
  Returns:
125
- Dictionary containing fetched data for symbols
130
+ Number of seconds to wait (0 if should poll now)
126
131
  """
127
- if not self._is_running:
128
- logger.debug(f"Starting adapter {self.adapter_id} on first data request")
129
- await self.start_watching()
130
- # Give the background task a chance to start and do initial poll
131
- await asyncio.sleep(0.5)
132
-
133
- # Wait for new data from polling task
134
- try:
135
- # First, check if we have data immediately available
136
- try:
137
- data = self._data_queue.get_nowait()
138
- return data
139
- except asyncio.QueueEmpty:
140
- pass
141
-
142
- # If no immediate data, wait with timeout
143
- data = await asyncio.wait_for(self._data_queue.get(), timeout=30.0)
144
- return data
145
- except asyncio.TimeoutError:
146
- logger.debug(f"Timeout waiting for data from adapter {self.adapter_id}, falling back to cached data")
147
-
148
- # If no new data, return latest data if available
149
- if self._latest_data is not None:
150
- return self._latest_data
151
- else:
152
- # Try a manual poll as fallback - this handles pytest environment issues
153
- async with self._symbols_lock:
154
- current_symbols = list(self._symbols)
155
- if current_symbols:
156
- try:
157
- await self._poll_once(current_symbols)
158
- if self._latest_data is not None:
159
- return self._latest_data
160
- except Exception as e:
161
- logger.error(f"Manual poll failed for adapter {self.adapter_id}: {e}")
162
-
163
- raise TimeoutError(f"No data available from polling adapter {self.adapter_id}")
132
+ current_time = time.time()
133
+ interval_seconds = self.config.poll_interval_seconds
164
134
 
165
- async def stop(self) -> None:
166
- """
167
- Stop polling completely and cleanup all resources.
135
+ # First poll is always immediate
136
+ if self._last_poll_time is None:
137
+ return 0
138
+
139
+ if interval_seconds >= 60:
140
+ # Time-aligned polling for intervals >= 1 minute using UTC
141
+ # Calculate next boundary based on seconds since epoch
142
+ next_boundary = math.ceil(current_time / interval_seconds) * interval_seconds
143
+ wait_time = next_boundary - current_time
144
+ return max(0, wait_time)
145
+ else:
146
+ # Simple interval-based polling for sub-minute intervals
147
+ next_poll_time = self._last_poll_time + interval_seconds
148
+ wait_time = next_poll_time - current_time
149
+ return max(0, wait_time)
150
+
151
+ async def _poll_now(self, symbols: List[str]) -> Dict[str, Any]:
168
152
  """
169
- if not self._is_running:
170
- return
153
+ Perform a poll operation immediately.
171
154
 
172
- logger.debug(f"Stopping polling adapter {self.adapter_id}")
155
+ Args:
156
+ symbols: List of symbols to poll for
173
157
 
174
- # Signal stop
175
- self._stop_event.set()
176
- self._is_running = False
158
+ Returns:
159
+ Dictionary containing fetched data for symbols
160
+ """
161
+ self._poll_count += 1
162
+ self._last_poll_time = time.time()
177
163
 
178
- # Clear data queue to prevent stale results from being processed
179
- while not self._data_queue.empty():
180
- try:
181
- self._data_queue.get_nowait()
182
- except asyncio.QueueEmpty:
183
- break
164
+ logger.debug(f"Polling {len(symbols)} symbols for adapter {self.adapter_id}")
184
165
 
185
- await self._cleanup_polling_task()
166
+ try:
167
+ # Filter out adapter-specific parameters
168
+ adapter_params = {"pollInterval", "interval", "updateInterval", "poll_interval_minutes"}
169
+ fetch_params = {k: v for k, v in self.params.items() if k not in adapter_params}
186
170
 
187
- logger.debug(f"Adapter {self.adapter_id} stopped (polled {self._poll_count} times, {self._error_count} errors)")
171
+ # Call the fetch method
172
+ result = await self.fetch_method(symbols, **fetch_params)
188
173
 
189
- async def _cleanup_polling_task(self) -> None:
190
- """Clean up the polling task."""
191
- if self._polling_task and not self._polling_task.done():
192
- try:
193
- # Handle both Task (normal operation) and Future (explicit event loop) objects
194
- if hasattr(self._polling_task, 'result') and not hasattr(self._polling_task, '__await__'):
195
- # This is a Future from AsyncThreadLoop.submit()
196
- self._polling_task.result(timeout=5.0)
197
- else:
198
- # This is a regular Task
199
- await asyncio.wait_for(self._polling_task, timeout=5.0)
200
- except (asyncio.TimeoutError, concurrent.futures.TimeoutError):
201
- logger.debug(f"Polling task for adapter {self.adapter_id} didn't stop gracefully, cancelling")
202
- self._polling_task.cancel()
203
- try:
204
- if hasattr(self._polling_task, 'result') and not hasattr(self._polling_task, '__await__'):
205
- # Future cancellation is handled by the future itself
206
- pass
207
- else:
208
- # Task cancellation
209
- await self._polling_task
210
- except (CancelledError, concurrent.futures.CancelledError):
211
- pass
212
-
213
- self._polling_task = None
174
+ logger.debug(f"Poll completed successfully for adapter {self.adapter_id}")
175
+ return result
214
176
 
215
- async def add_symbols(self, new_symbols: List[str]) -> None:
177
+ except Exception as e:
178
+ self._error_count += 1
179
+ logger.error(f"Poll failed for adapter {self.adapter_id}: {e}")
180
+ raise
181
+
182
+ async def update_symbols(self, new_symbols: List[str]) -> None:
216
183
  """
217
- Add symbols to the existing watch list.
184
+ Update the symbol list.
185
+
186
+ If symbols changed, the next call to get_next_data() will poll immediately.
218
187
 
219
188
  Args:
220
- new_symbols: List of symbols to add
189
+ new_symbols: New complete list of symbols to watch
221
190
  """
191
+ async with self._symbols_lock:
192
+ old_symbols = self._symbols.copy()
193
+ self._symbols = set(new_symbols or [])
194
+ symbols_changed = old_symbols != self._symbols
195
+
196
+ if symbols_changed:
197
+ self._symbols_changed = True
198
+ logger.debug(
199
+ f"Symbols updated for adapter {self.adapter_id}: {len(old_symbols)} -> {len(self._symbols)}"
200
+ )
201
+
202
+ async def add_symbols(self, new_symbols: List[str]) -> None:
203
+ """Add symbols to the existing watch list."""
222
204
  if not new_symbols:
223
205
  return
224
206
 
@@ -226,18 +208,13 @@ class PollingToWebSocketAdapter:
226
208
  before_count = len(self._symbols)
227
209
  self._symbols.update(new_symbols)
228
210
  after_count = len(self._symbols)
229
- added_count = after_count - before_count
230
211
 
231
- if added_count > 0:
232
- logger.debug(f"Added {added_count} symbols to adapter {self.adapter_id} (total: {after_count})")
212
+ if after_count > before_count:
213
+ self._symbols_changed = True
214
+ logger.debug(f"Added {after_count - before_count} symbols to adapter {self.adapter_id}")
233
215
 
234
216
  async def remove_symbols(self, symbols_to_remove: List[str]) -> None:
235
- """
236
- Remove specific symbols from the watch list.
237
-
238
- Args:
239
- symbols_to_remove: List of symbols to remove
240
- """
217
+ """Remove symbols from the watch list."""
241
218
  if not symbols_to_remove:
242
219
  return
243
220
 
@@ -245,195 +222,29 @@ class PollingToWebSocketAdapter:
245
222
  before_count = len(self._symbols)
246
223
  self._symbols.difference_update(symbols_to_remove)
247
224
  after_count = len(self._symbols)
248
- removed_count = before_count - after_count
249
225
 
250
- if removed_count > 0:
251
- logger.debug(f"Removed {removed_count} symbols from adapter {self.adapter_id} (total: {after_count})")
252
-
253
- # If no symbols left, we could optionally stop polling
254
- # For now, we'll keep polling in case symbols are added back
255
-
256
- async def update_symbols(self, new_symbols: List[str]) -> None:
257
- """
258
- Replace entire symbol list (atomic operation).
259
-
260
- Args:
261
- new_symbols: New complete list of symbols to watch
262
- """
263
- async with self._symbols_lock:
264
- old_symbols = self._symbols.copy()
265
- self._symbols = set(new_symbols or [])
266
-
267
- logger.debug(f"Updated symbols for adapter {self.adapter_id}: {len(old_symbols)} -> {len(self._symbols)}")
226
+ if after_count < before_count:
227
+ self._symbols_changed = True
228
+ logger.debug(f"Removed {before_count - after_count} symbols from adapter {self.adapter_id}")
268
229
 
269
230
  def is_watching(self, symbol: Optional[str] = None) -> bool:
270
- """
271
- Check if adapter has symbols configured to watch.
272
-
273
- Args:
274
- symbol: Optional specific symbol to check. If None, checks if has any symbols.
275
-
276
- Returns:
277
- True if has the specified symbol (or any symbols if symbol=None)
278
- """
231
+ """Check if adapter has symbols configured to watch."""
279
232
  if symbol is None:
280
233
  return len(self._symbols) > 0
281
234
  else:
282
235
  return symbol in self._symbols
283
236
 
284
- def is_running(self) -> bool:
285
- """
286
- Check if adapter is actively running (polling).
287
-
288
- Returns:
289
- True if adapter is currently running
290
- """
291
- return self._is_running
292
-
293
237
  def get_statistics(self) -> Dict[str, Any]:
294
238
  """Get adapter statistics for monitoring."""
295
239
  return {
296
240
  "adapter_id": self.adapter_id,
297
- "is_running": self._is_running,
298
241
  "symbol_count": len(self._symbols),
299
242
  "poll_count": self._poll_count,
300
243
  "error_count": self._error_count,
301
244
  "last_poll_time": self._last_poll_time,
302
- "poll_interval_seconds": self.poll_interval_seconds,
245
+ "poll_interval_seconds": self.config.poll_interval_seconds,
303
246
  }
304
247
 
305
- async def _polling_loop(self) -> None:
306
- """
307
- Main polling loop that runs in the background.
308
- """
309
- # Always do an initial poll immediately
310
- first_poll = True
311
-
312
- # Ensure we yield control to allow other tasks to run
313
- await asyncio.sleep(0)
314
-
315
- try:
316
- while not self._stop_event.is_set():
317
- try:
318
- # Get current symbols (thread-safe)
319
- async with self._symbols_lock:
320
- current_symbols = list(self._symbols)
321
-
322
- # Skip polling if no symbols
323
- if not current_symbols:
324
- await self._cancellable_sleep(10) # Short sleep when no symbols
325
- continue
326
-
327
- # Determine if we should poll now
328
- should_poll = first_poll or self._should_poll_now()
329
-
330
- if should_poll:
331
- await self._poll_once(current_symbols)
332
- first_poll = False
333
-
334
- # Sleep longer after successful poll
335
- sleep_time = self.poll_interval_seconds if not self.use_time_boundaries else 60
336
- await self._cancellable_sleep(sleep_time)
337
- else:
338
- # Not time to poll yet, sleep briefly and check again
339
- await self._cancellable_sleep(5)
340
-
341
- except CancelledError:
342
- break
343
- except Exception as e:
344
- self._error_count += 1
345
- logger.error(f"Polling error in adapter {self.adapter_id}: {e}")
346
-
347
- # Sleep before retry, but not too long
348
- # Ensure minimum 1 second sleep to avoid tight retry loops
349
- sleep_time = max(1, min(30, self.poll_interval_seconds // 10))
350
- await self._cancellable_sleep(sleep_time)
351
-
352
- except CancelledError:
353
- pass
354
- finally:
355
- logger.debug(f"Polling loop stopped for adapter {self.adapter_id}")
356
-
357
- def _should_poll_now(self) -> bool:
358
- """
359
- Determine if we should poll now based on timing logic.
360
-
361
- Returns:
362
- True if we should poll now
363
- """
364
- if not self.use_time_boundaries:
365
- # Simple interval-based polling
366
- if self._last_poll_time is None:
367
- return True
368
- return (time.time() - self._last_poll_time) >= self.poll_interval_seconds
369
- else:
370
- # Boundary-based polling (like the open_interest.py logic)
371
- # This would need access to data provider time, which we don't have here
372
- # For now, fall back to simple interval polling
373
- # TODO: Implement boundary logic if needed when data provider is available
374
- if self._last_poll_time is None:
375
- return True
376
- return (time.time() - self._last_poll_time) >= self.poll_interval_seconds
377
-
378
- async def _poll_once(self, symbols: List[str]) -> None:
379
- """
380
- Perform a single polling operation.
381
-
382
- Args:
383
- symbols: List of symbols to poll for
384
- """
385
- self._poll_count += 1
386
- self._last_poll_time = time.time()
387
-
388
- logger.debug(f"Polling {len(symbols)} symbols for adapter {self.adapter_id}")
389
-
390
- try:
391
- # Filter out adapter-specific parameters before calling fetch method
392
- # These parameters are for the adapter, not the underlying fetch method
393
- adapter_params = {'pollInterval', 'interval', 'updateInterval'}
394
- fetch_params = {k: v for k, v in self.params.items() if k not in adapter_params}
395
-
396
- # Call the fetch method with symbols and filtered params
397
- result = await self.fetch_method(symbols, **fetch_params)
398
-
399
- # Check if we've been stopped while the fetch was happening
400
- if self._stop_event.is_set():
401
- return
402
-
403
- # Store result and put it in queue for get_next_data to return
404
- self._latest_data = result
405
-
406
- # Put data in queue for get_next_data to return
407
- try:
408
- self._data_queue.put_nowait(result)
409
- except asyncio.QueueFull:
410
- # Clear old data and add new
411
- try:
412
- self._data_queue.get_nowait()
413
- except asyncio.QueueEmpty:
414
- pass
415
- self._data_queue.put_nowait(result)
416
-
417
- except Exception as e:
418
- # Check if we've been stopped during the error
419
- if self._stop_event.is_set():
420
- return
421
- # Re-raise to be handled by polling loop
422
- logger.error(f"Fetch failed for adapter {self.adapter_id}: {e}")
423
- raise
424
-
425
- async def _cancellable_sleep(self, seconds: float) -> None:
426
- """
427
- Sleep that can be interrupted by the stop event.
428
-
429
- Args:
430
- seconds: Number of seconds to sleep
431
- """
432
- if seconds <= 0:
433
- return
434
-
435
- try:
436
- await asyncio.wait_for(self._stop_event.wait(), timeout=seconds)
437
- except asyncio.TimeoutError:
438
- # Timeout is expected - means we slept for the full duration
439
- pass
248
+ async def stop(self) -> None:
249
+ """Stop the adapter (cleanup method for compatibility)."""
250
+ logger.debug(f"Adapter {self.adapter_id} stopped (polled {self._poll_count} times, {self._error_count} errors)")