onesecondtrader 0.7.0__py3-none-any.whl → 0.9.0__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.
File without changes
@@ -0,0 +1,133 @@
1
+ from dataclasses import dataclass
2
+ import enum
3
+
4
+
5
+ class BrokerType(enum.Enum):
6
+ """
7
+ Enum for broker types.
8
+
9
+ **Attributes:**
10
+
11
+ | Enum | Value | Description |
12
+ |------|-------|-------------|
13
+ | `LOCAL_SIMULATED` | `enum.auto()` | Locally simulated broker |
14
+ | `IB_SIMULATED` | `enum.auto()` | Interactive Brokers paper trading account |
15
+ | `IB_LIVE` | `enum.auto()` | Interactive Brokers live trading account |
16
+ | `MT5` | `enum.auto()` | MetaTrader 5 |
17
+ """
18
+
19
+ LOCAL_SIMULATED = enum.auto()
20
+ IB_SIMULATED = enum.auto()
21
+ IB_LIVE = enum.auto()
22
+ MT5 = enum.auto()
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class Bar:
27
+ """
28
+ Class for representing a OHLC(V) bar of market data.
29
+
30
+ Attributes:
31
+ open (float): Open price
32
+ high (float): High price
33
+ low (float): Low price
34
+ close (float): Close price
35
+ volume (float): Volume
36
+ """
37
+
38
+ open: float
39
+ high: float
40
+ low: float
41
+ close: float
42
+ volume: float | None = None
43
+
44
+
45
+ class Side(enum.Enum):
46
+ """
47
+ Enum for order sides.
48
+ """
49
+
50
+ BUY = enum.auto()
51
+ SELL = enum.auto()
52
+
53
+
54
+ class TimeInForce(enum.Enum):
55
+ """
56
+ Order time-in-force specifications.
57
+
58
+ **Attributes:**
59
+
60
+ | Enum | Value | Description |
61
+ |------|-------|-------------|
62
+ | `DAY` | `enum.auto()` | Valid until end of trading day |
63
+ | `FOK` | `enum.auto()` | Fill entire order immediately or cancel (Fill-or-Kill) |
64
+ | `GTC` | `enum.auto()` | Active until explicitly cancelled (Good-Till-Cancelled) |
65
+ | `GTD` | `enum.auto()` | Active until specified date (Good-Till-Date) |
66
+ | `IOC` | `enum.auto()` | Execute available quantity immediately, cancel rest
67
+ (Immediate-or-Cancel) |
68
+ """
69
+
70
+ DAY = enum.auto()
71
+ FOK = enum.auto()
72
+ GTC = enum.auto()
73
+ GTD = enum.auto()
74
+ IOC = enum.auto()
75
+
76
+
77
+ class OrderType(enum.Enum):
78
+ """
79
+ Enum for order types.
80
+
81
+ **Attributes:**
82
+
83
+ | Enum | Value | Description |
84
+ |------|-------|-------------|
85
+ | `MARKET` | `enum.auto()` | Market order |
86
+ | `LIMIT` | `enum.auto()` | Limit order |
87
+ | `STOP` | `enum.auto()` | Stop order |
88
+ | `STOP_LIMIT` | `enum.auto()` | Stop-limit order |
89
+ """
90
+
91
+ MARKET = enum.auto()
92
+ LIMIT = enum.auto()
93
+ STOP = enum.auto()
94
+ STOP_LIMIT = enum.auto()
95
+
96
+
97
+ class OrderLifecycleState(enum.Enum):
98
+ """
99
+ Enum for order lifecycle states.
100
+
101
+ **Attributes:**
102
+
103
+ | Enum | Value | Description |
104
+ |------|-------|-------------|
105
+ | `PENDING` | `enum.auto()` | Order has been submitted, but not yet acknowledged by
106
+ the broker |
107
+ | `OPEN` | `enum.auto()` | Order has been acknowledged by the broker, but not yet
108
+ filled or cancelled |
109
+ | `FILLED` | `enum.auto()` | Order has been filled |
110
+ | `CANCELLED` | `enum.auto()` | Order has been cancelled |
111
+ """
112
+
113
+ PENDING = enum.auto()
114
+ OPEN = enum.auto()
115
+ PARTIALLY_FILLED = enum.auto()
116
+ FILLED = enum.auto()
117
+ CANCELLED = enum.auto()
118
+
119
+
120
+ class OrderRejectionReason(enum.Enum):
121
+ """
122
+ Enum for order rejection reasons.
123
+
124
+ **Attributes:**
125
+
126
+ | Enum | Value | Description |
127
+ |------|-------|-------------|
128
+ | `UNKNOWN` | `enum.auto()` | Unknown reason |
129
+ | `NEGATIVE_QUANTITY` | `enum.auto()` | Negative quantity |
130
+ """
131
+
132
+ UNKNOWN = enum.auto()
133
+ NEGATIVE_QUANTITY = enum.auto()
File without changes
File without changes
@@ -0,0 +1,490 @@
1
+ """
2
+ This module provides the event bus for managing event-driven communication between
3
+ the trading infrastructure's components via a publish-subscribe messaging pattern.
4
+ """
5
+
6
+ import collections
7
+ import inspect
8
+ import logging
9
+ import threading
10
+ from collections.abc import Callable
11
+ from onesecondtrader.messaging import events
12
+ from onesecondtrader.monitoring import console
13
+
14
+
15
+ class EventBus:
16
+ # noinspection PyTypeChecker
17
+ """
18
+ Event bus for managing event-driven communication between the trading
19
+ infrastructure's components via a publish-subscribe messaging pattern.
20
+ Supports inheritance-based subscriptions where handlers subscribed to a parent event
21
+ type will receive events of child types.
22
+ Each subscription can include an optional filter function to receive only specific
23
+ events of a given type (e.g. filtering `IncomingBar` events for a specific symbol).
24
+
25
+ Examples:
26
+ >>> # Import necessary modules
27
+ >>> import pandas as pd
28
+ >>> from onesecondtrader.messaging.eventbus import EventBus
29
+ >>> from onesecondtrader.messaging import events
30
+ >>> from onesecondtrader.core import models
31
+
32
+ >>> # Instantiate event bus
33
+ >>> event_bus = EventBus()
34
+
35
+ >>> # Create a dummy handler that simply prints the symbol of the received event
36
+ >>> def dummy_handler(incoming_bar_event: events.Market.IncomingBar):
37
+ ... print(f"Received: {incoming_bar_event.symbol}")
38
+
39
+ >>> # Subscribe to IncomingBar events whose symbol is AAPL
40
+ >>> event_bus.subscribe(
41
+ ... events.Market.IncomingBar,
42
+ ... dummy_handler,
43
+ ... lambda event: event.symbol == "AAPL" # Lambda filter function
44
+ ... )
45
+
46
+ >>> # Create events to publish
47
+ >>> aapl_event = events.Market.IncomingBar(
48
+ ... ts_event=pd.Timestamp("2023-01-01", tz="UTC"),
49
+ ... symbol="AAPL",
50
+ ... bar=models.Bar(
51
+ ... open=100.0, high=101.0, low=99.0,
52
+ ... close=100.5, volume=1000
53
+ ... )
54
+ ... )
55
+ >>> googl_event = events.Market.IncomingBar(
56
+ ... ts_event=pd.Timestamp("2023-01-01", tz="UTC"),
57
+ ... symbol="GOOGL",
58
+ ... bar=models.Bar(
59
+ ... open=2800.0, high=2801.0, low=2799.0,
60
+ ... close=2800.5, volume=500
61
+ ... )
62
+ ... )
63
+
64
+ >>> # Publish events - only AAPL passes filter and will be printed
65
+ >>> event_bus.publish(aapl_event)
66
+ Received: AAPL
67
+ >>> event_bus.publish(googl_event)
68
+
69
+ >>> # Unsubscribe the dummy handler
70
+ >>> event_bus.unsubscribe(events.Market.IncomingBar, dummy_handler)
71
+
72
+ >>> # Publish again - no handler receives it (warning will be logged)
73
+ >>> event_bus.publish(aapl_event) # doctest: +SKIP
74
+ WARNING:root:Published IncomingBar but no subscribers exist - check event wiring
75
+ """
76
+
77
+ def __init__(self) -> None:
78
+ """
79
+ Initializes the event bus with optimized data structures for high-performance
80
+ event publishing.
81
+
82
+ Attributes:
83
+ self._handlers (collections.defaultdict): Direct storage mapping event types
84
+ to handler lists
85
+ self._publish_cache (dict): Pre-computed cache for O(1) publish operations
86
+ self._lock (threading.Lock): Single lock for all operations
87
+ (subscribe/unsubscribe are rare)
88
+ self._sequence_number (int): Sequence number counter for events
89
+ """
90
+ self._handlers: dict[
91
+ type[events.Base.Event],
92
+ list[
93
+ tuple[
94
+ Callable[[events.Base.Event], None],
95
+ Callable[[events.Base.Event], bool],
96
+ ]
97
+ ],
98
+ ] = collections.defaultdict(list)
99
+
100
+ self._publish_cache: dict[
101
+ type[events.Base.Event],
102
+ list[
103
+ tuple[
104
+ Callable[[events.Base.Event], None],
105
+ Callable[[events.Base.Event], bool],
106
+ ]
107
+ ],
108
+ ] = {}
109
+
110
+ self._lock: threading.Lock = threading.Lock()
111
+ self._sequence_number: int = -1
112
+
113
+ self._rebuild_cache()
114
+
115
+ def subscribe(
116
+ self,
117
+ event_type: type[events.Base.Event],
118
+ event_handler: Callable[[events.Base.Event], None],
119
+ event_filter: Callable[[events.Base.Event], bool] | None = None,
120
+ ) -> None:
121
+ """
122
+ The `subscribe` method registers an event handler for event messages of a
123
+ specified type and all its subtypes (expressed as subclasses in the event
124
+ dataclass hierarchy, so-called inheritance-based subscription).
125
+ When an event of that type or any subtype is published, the handler will be
126
+ invoked if the associated `event_filter` returns `True` for that event
127
+ instance.
128
+ A given handler can only be subscribed once per event type.
129
+ If the handler is already subscribed to the given event type
130
+ —regardless of the filter function—
131
+ the subscription attempt is ignored and a warning is logged.
132
+
133
+ Arguments:
134
+ event_type (type[events.Base.Event]): Type of the event to subscribe to,
135
+ must be a subclass of `events.Base.Event`.
136
+ event_handler (Callable[events.Base.Event, None]): Function to call when an
137
+ event of the given type is published.
138
+ This callable must accept a single argument of type `events.Base.Event`
139
+ (or its subclass).
140
+ event_filter (Callable[[events.Base.Event], bool] | None): Function to
141
+ determine whether to call the event handler for a given event.
142
+ Should accept one event and return `True` to handle or `False` to
143
+ ignore.
144
+ Defaults to `None`, which creates a filter that always returns `True`
145
+ (i.e. always call the event handler).
146
+ """
147
+
148
+ if not issubclass(event_type, events.Base.Event):
149
+ console.logger.error(
150
+ f"Invalid subscription attempt: event_type must be a subclass of "
151
+ f"Event, got {type(event_type).__name__}"
152
+ )
153
+ return
154
+
155
+ if not callable(event_handler):
156
+ console.logger.error(
157
+ f"Invalid subscription attempt: event_handler must be callable, "
158
+ f"got {type(event_handler).__name__}"
159
+ )
160
+ return
161
+
162
+ if event_filter is None:
163
+
164
+ def event_filter(event: events.Base.Event) -> bool:
165
+ return True
166
+
167
+ if not callable(event_filter):
168
+ console.logger.error(
169
+ f"Invalid subscription attempt: event_filter must be callable, "
170
+ f"got {type(event_filter).__name__}"
171
+ )
172
+ return
173
+
174
+ is_valid, error_msg = self._validate_filter_signature(event_filter)
175
+ if not is_valid:
176
+ console.logger.error(f"Invalid subscription attempt: {error_msg}")
177
+ return
178
+
179
+ with self._lock:
180
+ if any(
181
+ event_handler == existing_handler
182
+ for existing_handler, _ in self._handlers[event_type]
183
+ ):
184
+ console.logger.warning(
185
+ f"Duplicate subscription attempt: event_handler was already "
186
+ f"subscribed to {event_type.__name__}"
187
+ )
188
+ return
189
+
190
+ self._handlers[event_type].append((event_handler, event_filter))
191
+
192
+ self._rebuild_cache()
193
+
194
+ handler_name = getattr(event_handler, "__name__", "<lambda>")
195
+ console.logger.info(f"Subscribed {handler_name} to {event_type.__name__}.")
196
+
197
+ def unsubscribe(
198
+ self,
199
+ event_type: type[events.Base.Event],
200
+ event_handler: Callable[[events.Base.Event], None],
201
+ ) -> None:
202
+ """
203
+ The `unsubscribe` method removes an event handler from the subscribers list for
204
+ the specified event type.
205
+ If the event handler is not subscribed to the given event type, the
206
+ unsubscription attempt is ignored and a warning is logged.
207
+ After removing the event handler, the event type may have an empty subscribers
208
+ list but remains in the `subscribers` dictionary.
209
+
210
+ Arguments:
211
+ event_type (type[events.Base.Event]): Type of the event to unsubscribe from,
212
+ must be a subclass of `events.Base.Event`.
213
+ event_handler (Callable[events.Base.Event, None]): Event handler to remove
214
+ from the subscribers list (this will also remove the associated filter
215
+ function).
216
+ """
217
+ if not issubclass(event_type, events.Base.Event):
218
+ console.logger.error(
219
+ f"Invalid unsubscription attempt: event_type must be a subclass of "
220
+ f"Event, got {type(event_type).__name__}"
221
+ )
222
+ return
223
+
224
+ if not callable(event_handler):
225
+ console.logger.error(
226
+ f"Invalid unsubscription attempt: callback must be callable, "
227
+ f"got {type(event_handler).__name__}"
228
+ )
229
+ return
230
+
231
+ with self._lock:
232
+ if event_type not in self._handlers:
233
+ console.logger.warning(
234
+ f"Attempted to unsubscribe from {event_type.__name__}, "
235
+ f"but no subscribers exist"
236
+ )
237
+ return
238
+
239
+ current_handlers = self._handlers[event_type]
240
+ new_handlers = [
241
+ (existing_handler, existing_filter)
242
+ for existing_handler, existing_filter in current_handlers
243
+ if existing_handler != event_handler
244
+ ]
245
+
246
+ removed_count = len(current_handlers) - len(new_handlers)
247
+ if removed_count == 0:
248
+ handler_name = getattr(event_handler, "__name__", "<lambda>")
249
+ console.logger.warning(
250
+ f"Attempted to unsubscribe {handler_name} from "
251
+ f"{event_type.__name__}, but it was not subscribed"
252
+ )
253
+ return
254
+
255
+ if new_handlers:
256
+ self._handlers[event_type] = new_handlers
257
+ else:
258
+ # Clean up empty lists
259
+ del self._handlers[event_type]
260
+
261
+ self._rebuild_cache()
262
+
263
+ handler_name = getattr(event_handler, "__name__", "<lambda>")
264
+ console.logger.info(
265
+ f"Unsubscribed {handler_name} from "
266
+ f"{event_type.__name__} (removed {removed_count} subscription(s))"
267
+ )
268
+
269
+ def publish(self, event: events.Base.Event) -> None:
270
+ """
271
+ The `publish` method delivers the event to all handlers subscribed to the
272
+ event's type or any of its parent types (inheritance-based subscription).
273
+ Handlers are only called if their filter function returns True for this event.
274
+ Handlers are called synchronously in the order they were subscribed.
275
+
276
+ This method uses a pre-computed handler cache for O(1) lookup performance
277
+ and runs without locks for maximum concurrency.
278
+
279
+ Arguments:
280
+ event (events.Base.Event): Event to publish. Must be an instance of
281
+ `events.Base.Event` or one of its subclasses.
282
+ """
283
+ if not isinstance(event, events.Base.Event):
284
+ console.logger.error(
285
+ f"Invalid publish attempt: event must be an instance of Event, "
286
+ f"got {type(event).__name__}"
287
+ )
288
+ return
289
+
290
+ object.__setattr__(
291
+ event, "event_bus_sequence_number", self._set_sequence_number()
292
+ )
293
+
294
+ event_type: type[events.Base.Event] = type(event)
295
+
296
+ handlers = self._publish_cache.get(event_type, [])
297
+
298
+ if not handlers:
299
+ console.logger.warning(
300
+ f"Published {event_type.__name__} but no subscribers exist - "
301
+ f"check event wiring"
302
+ )
303
+ return
304
+
305
+ delivered_count = 0
306
+ for event_handler, event_filter in handlers:
307
+ try:
308
+ should_handle = event_filter(event)
309
+
310
+ if not isinstance(should_handle, bool):
311
+ handler_name = getattr(event_handler, "__name__", "<lambda>")
312
+ console.logger.warning(
313
+ f"Filter for handler {handler_name} returned "
314
+ f"{type(should_handle).__name__}, expected bool. "
315
+ f"Treating as False."
316
+ )
317
+ should_handle = False
318
+
319
+ except TypeError as type_error:
320
+ handler_name = getattr(event_handler, "__name__", "<lambda>")
321
+ if "takes" in str(type_error) and "positional argument" in str(
322
+ type_error
323
+ ):
324
+ console.logger.error(
325
+ f"Filter for handler {handler_name} has wrong signature: "
326
+ f"{type_error}"
327
+ )
328
+ else:
329
+ console.logger.exception(
330
+ f"Filter function for handler {handler_name} failed "
331
+ f"processing {event_type.__name__}: {type_error}"
332
+ )
333
+ continue
334
+ except Exception as filter_exception:
335
+ handler_name = getattr(event_handler, "__name__", "<lambda>")
336
+ console.logger.exception(
337
+ f"Filter function for handler {handler_name} failed "
338
+ f"processing {event_type.__name__}: {filter_exception}"
339
+ )
340
+ continue
341
+
342
+ if should_handle:
343
+ try:
344
+ event_handler(event)
345
+ delivered_count += 1
346
+ except Exception as handler_exception:
347
+ handler_name = getattr(event_handler, "__name__", "<lambda>")
348
+ console.logger.exception(
349
+ f"Handler {handler_name} failed processing "
350
+ f"{event_type.__name__}: {handler_exception}"
351
+ )
352
+
353
+ if delivered_count == 0:
354
+ console.logger.warning(
355
+ f"Published {event_type.__name__} but no handlers received it - "
356
+ f"all {len(handlers)} handler(s) filtered out the event"
357
+ )
358
+ else:
359
+ # Conditional debug logging to avoid string formatting overhead
360
+ if console.logger.isEnabledFor(logging.DEBUG):
361
+ console.logger.debug(
362
+ f"Published {event_type.__name__} to {delivered_count} handler(s)"
363
+ )
364
+
365
+ @staticmethod
366
+ def _validate_filter_signature(
367
+ event_filter: Callable[[events.Base.Event], bool],
368
+ ) -> tuple[bool, str | None]:
369
+ """
370
+ Validate that filter function has the correct signature.
371
+
372
+ A valid filter function must:
373
+ - Accept exactly 1 parameter (the event)
374
+ - Not use *args or **kwargs
375
+ - Optionally return bool (if type annotated)
376
+
377
+ Arguments:
378
+ event_filter (Callable): The filter function to validate
379
+
380
+ Returns:
381
+ tuple[bool, str | None]: (is_valid, error_message)
382
+ is_valid: True if signature is valid, False otherwise
383
+ error_message: Description of the issue if invalid, None if valid
384
+ """
385
+ try:
386
+ sig = inspect.signature(event_filter)
387
+ params = list(sig.parameters.values())
388
+
389
+ if len(params) != 1:
390
+ return (
391
+ False,
392
+ f"Filter must accept exactly 1 parameter, got {len(params)}",
393
+ )
394
+
395
+ param = params[0]
396
+ if param.kind == inspect.Parameter.VAR_POSITIONAL:
397
+ return (
398
+ False,
399
+ "Filter cannot use *args - must accept exactly 1 event parameter",
400
+ )
401
+ if param.kind == inspect.Parameter.VAR_KEYWORD:
402
+ return (
403
+ False,
404
+ "Filter cannot use **kwargs - must accept exactly 1 event "
405
+ "parameter",
406
+ )
407
+
408
+ if sig.return_annotation is not inspect.Parameter.empty:
409
+ if sig.return_annotation is not bool:
410
+ return (
411
+ False,
412
+ f"Filter return type should be bool, got "
413
+ f"{sig.return_annotation}",
414
+ )
415
+
416
+ return True, None
417
+
418
+ except Exception as e:
419
+ return False, f"Could not inspect filter signature: {e}"
420
+
421
+ def _set_sequence_number(self) -> int:
422
+ """
423
+ Increment and return the event bus sequence number in a thread-safe manner.
424
+ """
425
+ with self._lock:
426
+ self._sequence_number += 1
427
+ return self._sequence_number
428
+
429
+ @staticmethod
430
+ def _get_all_concrete_event_types() -> list[type[events.Base.Event]]:
431
+ """
432
+ Dynamically discover all concrete event types from the events module.
433
+ Automatically adapts to namespace changes without code modifications.
434
+
435
+ Returns:
436
+ list[type[events.Base.Event]]: List of concrete event classes that can be
437
+ instantiated and published.
438
+ """
439
+ concrete_types = []
440
+
441
+ for attr_name in dir(events):
442
+ if attr_name.startswith("_"):
443
+ continue
444
+
445
+ attr = getattr(events, attr_name)
446
+
447
+ if not inspect.isclass(attr) or attr_name == "Base":
448
+ continue
449
+
450
+ for member_name, member_obj in inspect.getmembers(attr, inspect.isclass):
451
+ if (
452
+ issubclass(member_obj, events.Base.Event)
453
+ and member_obj != events.Base.Event
454
+ and not inspect.isabstract(member_obj)
455
+ ):
456
+ concrete_types.append(member_obj)
457
+
458
+ return concrete_types
459
+
460
+ def _rebuild_cache(self) -> None:
461
+ """
462
+ Rebuild the pre-computed publish cache for all concrete event types.
463
+ This method should be called whenever subscriptions change.
464
+ """
465
+ new_cache = {}
466
+ concrete_event_types = self._get_all_concrete_event_types()
467
+
468
+ for concrete_event_type in concrete_event_types:
469
+ handlers = []
470
+ seen_handler_ids = set()
471
+
472
+ for handler_type, handler_list in self._handlers.items():
473
+ if issubclass(concrete_event_type, handler_type):
474
+ for handler, filter_func in handler_list:
475
+ handler_id = id(handler)
476
+ if handler_id not in seen_handler_ids:
477
+ handlers.append((handler, filter_func))
478
+ seen_handler_ids.add(handler_id)
479
+
480
+ if handlers:
481
+ new_cache[concrete_event_type] = handlers
482
+
483
+ self._publish_cache = new_cache
484
+
485
+ if console.logger.isEnabledFor(logging.DEBUG):
486
+ console.logger.debug(
487
+ f"Publish cache rebuilt: {len(new_cache)} event types cached, "
488
+ f"total handlers: "
489
+ f"{sum(len(handlers) for handlers in new_cache.values())}"
490
+ )