onesecondtrader 0.13.1__tar.gz → 0.14.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/PKG-INFO +1 -1
  2. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/pyproject.toml +1 -1
  3. onesecondtrader-0.14.1/src/onesecondtrader/brokers/base_broker.py +99 -0
  4. onesecondtrader-0.14.1/src/onesecondtrader/brokers/simulated_broker.py +10 -0
  5. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/core/models.py +22 -22
  6. onesecondtrader-0.14.1/src/onesecondtrader/core/portfolio.py +348 -0
  7. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/datafeeds/base_datafeed.py +4 -8
  8. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/messaging/__init__.py +2 -1
  9. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/messaging/eventbus.py +9 -0
  10. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/messaging/events.py +103 -5
  11. onesecondtrader-0.14.1/src/onesecondtrader/py.typed +0 -0
  12. onesecondtrader-0.14.1/src/onesecondtrader/strategies/__init__.py +0 -0
  13. onesecondtrader-0.14.1/src/onesecondtrader/strategies/base_strategy.py +46 -0
  14. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/LICENSE +0 -0
  15. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/README.md +0 -0
  16. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/__init__.py +0 -0
  17. {onesecondtrader-0.13.1/src/onesecondtrader/core → onesecondtrader-0.14.1/src/onesecondtrader/brokers}/__init__.py +0 -0
  18. {onesecondtrader-0.13.1/src/onesecondtrader/datafeeds → onesecondtrader-0.14.1/src/onesecondtrader/core}/__init__.py +0 -0
  19. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/core/py.typed +0 -0
  20. {onesecondtrader-0.13.1/src/onesecondtrader/indicators → onesecondtrader-0.14.1/src/onesecondtrader/datafeeds}/__init__.py +0 -0
  21. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/datafeeds/csv_datafeed.py +0 -0
  22. {onesecondtrader-0.13.1/src/onesecondtrader/monitoring → onesecondtrader-0.14.1/src/onesecondtrader/indicators}/__init__.py +0 -0
  23. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/indicators/base_indicator.py +0 -0
  24. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/indicators/moving_averages.py +0 -0
  25. /onesecondtrader-0.13.1/src/onesecondtrader/monitoring/py.typed → /onesecondtrader-0.14.1/src/onesecondtrader/monitoring/__init__.py +0 -0
  26. {onesecondtrader-0.13.1 → onesecondtrader-0.14.1}/src/onesecondtrader/monitoring/console.py +0 -0
  27. {onesecondtrader-0.13.1/src/onesecondtrader → onesecondtrader-0.14.1/src/onesecondtrader/monitoring}/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.13.1
3
+ Version: 0.14.1
4
4
  Summary: The Trading Infrastructure Toolkit for Python. Research, simulate, and deploy algorithmic trading strategies — all in one place.
5
5
  License-File: LICENSE
6
6
  Author: Nils P. Kujath
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "onesecondtrader"
3
- version = "0.13.1"
3
+ version = "0.14.1"
4
4
  description = "The Trading Infrastructure Toolkit for Python. Research, simulate, and deploy algorithmic trading strategies — all in one place."
5
5
  authors = [
6
6
  {name = "Nils P. Kujath",email = "63961429+NilsKujath@users.noreply.github.com"}
@@ -0,0 +1,99 @@
1
+ import abc
2
+ from onesecondtrader import messaging
3
+ from onesecondtrader.messaging import events
4
+
5
+
6
+ class BaseBroker(abc.ABC):
7
+ def __init__(self, event_bus: messaging.EventBus | None = None):
8
+ self.event_bus: messaging.EventBus = (
9
+ event_bus if event_bus else messaging.system_event_bus
10
+ )
11
+ self._is_connected: bool = False
12
+
13
+ def connect(self) -> bool:
14
+ if self._is_connected:
15
+ return True
16
+ self._subscribe_to_events()
17
+ self._is_connected = True
18
+ return True
19
+
20
+ def disconnect(self) -> None:
21
+ if not self._is_connected:
22
+ return
23
+ try:
24
+ self.event_bus.unsubscribe(events.System.Shutdown, self.on_system_shutdown)
25
+ finally:
26
+ self._is_connected = False
27
+
28
+ @property
29
+ def is_connected(self) -> bool:
30
+ return self._is_connected
31
+
32
+ def _subscribe_to_events(self) -> None:
33
+ """
34
+ Subscribe to relevant events from the event bus.
35
+ """
36
+ self.event_bus.subscribe(events.System.Shutdown, self.on_system_shutdown)
37
+ self.event_bus.subscribe(events.Strategy.SymbolRelease, self.on_symbol_release)
38
+
39
+ def on_system_shutdown(self, event: events.Base.Event) -> None:
40
+ """
41
+ Handle system shutdown events (ignore unrelated events).
42
+ """
43
+ if not isinstance(event, events.System.Shutdown):
44
+ return
45
+ # Default no-op
46
+ return
47
+
48
+ def on_symbol_release(self, event: events.Base.Event) -> None:
49
+ """
50
+ Handle portfolio symbol release events (ignore unrelated events).
51
+ Intended for brokers to perform any symbol-specific cleanup if necessary.
52
+ Default implementation is a no-op.
53
+ """
54
+ if not isinstance(event, events.Strategy.SymbolRelease):
55
+ return
56
+ # Default no-op
57
+ return
58
+
59
+ def on_request_market_order(self, event: events.Request.MarketOrder) -> None:
60
+ """
61
+ Handle market order requests.
62
+ """
63
+ pass
64
+
65
+ def on_request_limit_order(self, event: events.Request.LimitOrder) -> None:
66
+ """
67
+ Handle limit order requests.
68
+ """
69
+ pass
70
+
71
+ def on_request_stop_order(self, event: events.Request.StopOrder) -> None:
72
+ """
73
+ Handle stop order requests.
74
+ """
75
+ pass
76
+
77
+ def on_request_stop_limit_order(self, event: events.Request.StopLimitOrder) -> None:
78
+ """
79
+ Handle stop limit order requests.
80
+ """
81
+ pass
82
+
83
+ def on_request_cancel_order(self, event: events.Request.CancelOrder) -> None:
84
+ """
85
+ Handle cancel order requests.
86
+ """
87
+ pass
88
+
89
+ def on_request_flush_symbol(self, event: events.Request.FlushSymbol) -> None:
90
+ """
91
+ Handle flush symbol requests.
92
+ """
93
+ pass
94
+
95
+ def on_request_flush_all(self, event: events.Request.FlushAll) -> None:
96
+ """
97
+ Handle flush all requests.
98
+ """
99
+ pass
@@ -0,0 +1,10 @@
1
+ from onesecondtrader.brokers import base_broker
2
+
3
+
4
+ class SimulatedBroker(base_broker.BaseBroker):
5
+ """
6
+ Simple simulated broker used as a safe default.
7
+ """
8
+
9
+ def __init__(self, event_bus=None):
10
+ super().__init__(event_bus)
@@ -2,26 +2,6 @@ from dataclasses import dataclass
2
2
  import enum
3
3
 
4
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
5
  @dataclass(frozen=True, slots=True)
26
6
  class Bar:
27
7
  """
@@ -101,8 +81,8 @@ class OrderLifecycleState(enum.Enum):
101
81
 
102
82
  | Enum | Value | Description |
103
83
  |------|-------|-------------|
104
- | `PENDING` | `enum.auto()` | Order has been submitted, but not yet acknowledged by the broker |
105
- | `OPEN` | `enum.auto()` | Order has been acknowledged by the broker, but not yet filled or cancelled |
84
+ | `PENDING` | `enum.auto()` | Order has been submitted, but not yet acknowledged by the brokers |
85
+ | `OPEN` | `enum.auto()` | Order has been acknowledged by the brokers, but not yet filled or cancelled |
106
86
  | `FILLED` | `enum.auto()` | Order has been filled |
107
87
  | `CANCELLED` | `enum.auto()` | Order has been cancelled |
108
88
  """
@@ -186,3 +166,23 @@ class XMAMode(enum.Enum):
186
166
  CLOSE = enum.auto()
187
167
  TYPICAL_PRICE = enum.auto()
188
168
  WEIGHTED_CLOSE = enum.auto()
169
+
170
+
171
+ class Position:
172
+ pass
173
+
174
+
175
+ class StrategyShutdownMode(enum.Enum):
176
+ """
177
+ Enum for strategy shutdown modes.
178
+
179
+ **Attributes:**
180
+
181
+ | Enum | Value | Description |
182
+ |------|-------|-------------|
183
+ | `SOFT` | `enum.auto()` | Do not open new positions; wait until current positions close naturally |
184
+ | `HARD` | `enum.auto()` | Close all positions immediately with market orders |
185
+ """
186
+
187
+ SOFT = enum.auto()
188
+ HARD = enum.auto()
@@ -0,0 +1,348 @@
1
+ """
2
+ This module provides the Portfolio class.
3
+ """
4
+
5
+ import threading
6
+ from collections.abc import Iterable
7
+ from onesecondtrader import messaging
8
+ from onesecondtrader.messaging import events
9
+ from onesecondtrader.monitoring import console
10
+ from onesecondtrader.brokers import base_broker
11
+ from onesecondtrader.strategies import base_strategy
12
+ from onesecondtrader.core.models import StrategyShutdownMode
13
+
14
+
15
+ class Portfolio:
16
+ """
17
+ The Portfolio class orchestrates the trading infrastructure's components.
18
+ It manages the broker connection, market data reception, and strategy execution.
19
+
20
+ Multiple instances of the same Strategy class can be registered concurrently.
21
+ Symbol ownership is exclusive and enforced by the portfolio: each symbol may be
22
+ owned by at most one strategy instance at a time. Use `add_strategy()` with a list
23
+ of symbols or `assign_symbols(...)` with a specific strategy and a list of symbols
24
+ to claim symbols; `owner_of(symbol)` returns the current owner.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ event_bus: messaging.EventBus | None = None,
30
+ broker_class: type[base_broker.BaseBroker] | None = None,
31
+ ):
32
+ """
33
+ Initialize the Portfolio class, subscribe to events, and connect to the broker.
34
+
35
+ Args:
36
+ event_bus (EventBus | None): Event bus to use; defaults to
37
+ system_event_bus when None.
38
+ broker_class (type[base_broker.BaseBroker] | None): Broker class to
39
+ instantiate and connect. Must be a subclass of BaseBroker.
40
+
41
+ Attributes:
42
+ self.event_bus (eventbus.EventBus): Event bus used for communication between
43
+ the trading infrastructure's components.
44
+ self._lock (threading.Lock): Lock for thread-safe operations.
45
+ self._strategies (set[base_strategy.Strategy]): Registered strategy
46
+ instances.
47
+ self._symbol_owner (dict[str, base_strategy.Strategy]): Exclusive symbol
48
+ ownership map; each symbol is owned by at most one strategy instance at
49
+ a time.
50
+ self._removal_pending (set[base_strategy.Strategy]): Set of strategies that
51
+ are still active but marked for removal once all symbols are released.
52
+ self.broker (base_broker.BaseBroker | None): Instantiated broker; may be
53
+ disconnected if connect failed.
54
+ """
55
+ # INITIALIZE EVENT BUS
56
+ # ------------------------------------------------------------------------------
57
+ self.event_bus: messaging.EventBus = (
58
+ event_bus if event_bus else messaging.system_event_bus
59
+ )
60
+
61
+ # SUBSCRIBE HANDLER METHODS TO EVENTS VIA event_bus.subscribe
62
+ # ------------------------------------------------------------------------------
63
+ self.event_bus.subscribe(events.Strategy.SymbolRelease, self.on_symbol_release)
64
+
65
+ # INITIALIZE LOCK FOR THREAD-SAFE OPERATIONS WITHIN THE PORTFOLIO
66
+ # ------------------------------------------------------------------------------
67
+ self._lock = threading.Lock()
68
+
69
+ # KEEP TRACK OF STRATEGIES AND SYMBOL OWNERSHIP
70
+ # ------------------------------------------------------------------------------
71
+ self._strategies: set[base_strategy.Strategy] = set()
72
+ self._symbol_owner: dict[str, base_strategy.Strategy] = {}
73
+ self._removal_pending: set[base_strategy.Strategy] = set()
74
+
75
+ # INITIALIZE BROKER
76
+ # ------------------------------------------------------------------------------
77
+ self.broker: base_broker.BaseBroker | None = None
78
+ if broker_class is None or not issubclass(broker_class, base_broker.BaseBroker):
79
+ broker_name = (
80
+ getattr(broker_class, "__name__", str(broker_class))
81
+ if broker_class
82
+ else None
83
+ )
84
+ console.logger.error(
85
+ "Portfolio requires a valid broker_class (subclass of BaseBroker), "
86
+ f"got {broker_name}"
87
+ )
88
+ return
89
+ try:
90
+ self.broker = broker_class(self.event_bus)
91
+ except Exception as e:
92
+ console.logger.error(
93
+ f"Failed to instantiate broker "
94
+ f"{getattr(broker_class, '__name__', str(broker_class))}: {e}"
95
+ )
96
+ return
97
+
98
+ # CONNECT TO BROKER
99
+ # ------------------------------------------------------------------------------
100
+ try:
101
+ connected = self.broker.connect()
102
+ if not connected:
103
+ console.logger.error(
104
+ f"Failed to connect broker {type(self.broker).__name__}"
105
+ )
106
+ except Exception as e:
107
+ console.logger.error(f"Broker connect failed: {e}")
108
+
109
+ def add_strategy(
110
+ self,
111
+ strategy_instance: base_strategy.Strategy,
112
+ symbols: Iterable[str] | None = None,
113
+ ) -> bool:
114
+ """
115
+ Register a Strategy instance and optionally assign a list of symbols to it.
116
+
117
+ If symbols are provided, potential conflicts are checked first under a lock.
118
+ If any conflicts exist, no symbols are assigned; a warning is logged listing
119
+ both non_conflicting and conflicting symbols and instructions to use
120
+ assign_symbols(...) are provided.
121
+ If no conflicts exist, all provided symbols are claimed by the strategy.
122
+
123
+ Args:
124
+ strategy_instance (base_strategy.Strategy): Strategy instance to register.
125
+ symbols (Iterable[str] | None): Optional list of symbols to assign to the
126
+ strategy.
127
+
128
+ Returns:
129
+ bool: True if the strategy was registered, False otherwise.
130
+ """
131
+ # VALIDATE THAT INSTANCE IS A SUBCLASS OF base_strategy.Strategy
132
+ # ------------------------------------------------------------------------------
133
+ if not isinstance(strategy_instance, base_strategy.Strategy):
134
+ console.logger.error("add_strategy: strategy must inherit from Strategy")
135
+ return False
136
+
137
+ # ADD STRATEGY INSTANCE TO REGISTRY IF NOT ALREADY REGISTERED
138
+ # ------------------------------------------------------------------------------
139
+ with self._lock:
140
+ if strategy_instance in self._strategies:
141
+ console.logger.warning("add_strategy: strategy already registered")
142
+ return False
143
+ self._strategies.add(strategy_instance)
144
+
145
+ # ASSIGN SYMBOLS IF PROVIDED AND NO CONFLICTS EXIST, ELSE LOG WARNING
146
+ # ------------------------------------------------------------------------------
147
+ if symbols is not None:
148
+ # Create an ordered list of unique, non-empty, trimmed symbols
149
+ symbols_list = list(
150
+ dict.fromkeys(s.strip() for s in symbols if s and s.strip())
151
+ )
152
+
153
+ # Check for conflicts, claim symbols for strategy if no conflicts arise
154
+ if symbols_list:
155
+ non_conflicting: list[str] = []
156
+ conflicting: list[str] = []
157
+ with self._lock:
158
+ for sym in symbols_list:
159
+ owner = self._symbol_owner.get(sym)
160
+ if owner is None or owner is strategy_instance:
161
+ non_conflicting.append(sym)
162
+ else:
163
+ conflicting.append(sym)
164
+ if conflicting:
165
+ console.logger.warning(
166
+ "add_strategy: symbols not assigned due to conflicts; "
167
+ "use Portfolio.assign_symbols(...) after resolving. "
168
+ f"non_conflicting={non_conflicting}, conflicts={conflicting}"
169
+ )
170
+ else:
171
+ self.assign_symbols(strategy_instance, symbols_list)
172
+ return True
173
+
174
+ def remove_strategy(
175
+ self,
176
+ strategy: base_strategy.Strategy,
177
+ shutdown_mode: StrategyShutdownMode = StrategyShutdownMode.SOFT,
178
+ ) -> bool:
179
+ """
180
+ Mark a strategy for removal and request it to close its positions in the manner
181
+ dictated via the `shutdown_mode` argument (default to soft shutdown, i.e. wait
182
+ for open positions to close naturally and release symbols once they are flat).
183
+
184
+ Args:
185
+ strategy (base_strategy.Strategy): Strategy instance to remove.
186
+ shutdown_mode (StrategyShutdownMode): Shutdown mode to use. Defaults to
187
+ StrategyShutdownMode.SOFT.
188
+ """
189
+
190
+ # IF STRATEGY IS REGISTERED, MARK IT FOR REMOVAL
191
+ # ------------------------------------------------------------------------------
192
+ with self._lock:
193
+ if strategy not in self._strategies:
194
+ console.logger.warning("remove_strategy: strategy not registered")
195
+ return False
196
+ self._removal_pending.add(strategy)
197
+
198
+ try:
199
+ strategy.request_close(shutdown_mode)
200
+ except Exception:
201
+ console.logger.warning(
202
+ "remove_strategy: strategy does not support request_close; proceeding to flatness check"
203
+ )
204
+
205
+ try:
206
+ if bool(strategy.is_flat()):
207
+ # If the strategy is already flat and owns no symbols, deregister now
208
+ with self._lock:
209
+ has_owned_left = any(
210
+ owner is strategy for owner in self._symbol_owner.values()
211
+ )
212
+ if not has_owned_left:
213
+ if strategy in self._strategies:
214
+ self._strategies.remove(strategy)
215
+ self._removal_pending.discard(strategy)
216
+ console.logger.info(
217
+ f"Strategy {getattr(strategy, 'name', type(strategy).__name__)} removed: flat and no symbols owned"
218
+ )
219
+ return True
220
+ except Exception:
221
+ console.logger.warning(
222
+ "remove_strategy: strategy does not implement is_flat; will wait for symbol releases"
223
+ )
224
+ return False
225
+
226
+ def assign_symbols(
227
+ self,
228
+ strategy: base_strategy.Strategy,
229
+ symbols: Iterable[str],
230
+ ) -> tuple[list[str], list[str]]:
231
+ """
232
+ Assign symbols to a strategy with exclusivity enforcement.
233
+
234
+ Returns:
235
+ tuple[list[str], list[str]]: (accepted, conflicts)
236
+ """
237
+ if not isinstance(strategy, base_strategy.Strategy):
238
+ console.logger.error("assign_symbols: strategy must inherit from Strategy")
239
+ return [], list(symbols)
240
+ symbols_list = list(
241
+ dict.fromkeys(s.strip() for s in symbols if s and s.strip())
242
+ )
243
+ if not symbols_list:
244
+ return [], []
245
+ accepted: list[str] = []
246
+ conflicts: list[str] = []
247
+ with self._lock:
248
+ for sym in symbols_list:
249
+ current = self._symbol_owner.get(sym)
250
+ if current is None or current is strategy:
251
+ self._symbol_owner[sym] = strategy
252
+ accepted.append(sym)
253
+ else:
254
+ conflicts.append(sym)
255
+ if accepted:
256
+ strategy.add_symbols(accepted)
257
+ if conflicts:
258
+ console.logger.warning(
259
+ f"assign_symbols: conflicts for {len(conflicts)} symbol(s): {conflicts}"
260
+ )
261
+ return accepted, conflicts
262
+
263
+ def unassign_symbols(
264
+ self, strategy: base_strategy.Strategy, symbols: Iterable[str]
265
+ ) -> list[str]:
266
+ """
267
+ Release symbol ownership from a strategy.
268
+
269
+ Returns:
270
+ list[str]: Symbols actually unassigned.
271
+ """
272
+ if not isinstance(strategy, base_strategy.Strategy):
273
+ console.logger.error(
274
+ "unassign_symbols: strategy must inherit from Strategy"
275
+ )
276
+ return []
277
+ symbols_list = list(
278
+ dict.fromkeys(s.strip() for s in symbols if s and s.strip())
279
+ )
280
+ if not symbols_list:
281
+ return []
282
+ removed: list[str] = []
283
+ with self._lock:
284
+ for sym in symbols_list:
285
+ if self._symbol_owner.get(sym) is strategy:
286
+ del self._symbol_owner[sym]
287
+ removed.append(sym)
288
+ if removed:
289
+ strategy.remove_symbols(removed)
290
+ return removed
291
+
292
+ def owner_of(self, symbol: str) -> base_strategy.Strategy | None:
293
+ """
294
+ Return the owning strategy for a symbol or None if unowned.
295
+ """
296
+ with self._lock:
297
+ return self._symbol_owner.get(symbol)
298
+
299
+ def release_symbols_from_strategy(
300
+ self, strategy: base_strategy.Strategy, symbols: Iterable[str]
301
+ ) -> list[str]:
302
+ """
303
+ Release symbols from the given strategy.
304
+
305
+ If the strategy was previously marked for removal and ends up with no owned
306
+ symbols after this call, and the strategy is flat, it will be automatically
307
+ deregistered.
308
+ """
309
+ removed = self.unassign_symbols(strategy, symbols)
310
+ if not removed:
311
+ return removed
312
+ # Auto-deregister if pending removal and no more owned symbols
313
+ pending = False
314
+ with self._lock:
315
+ pending = strategy in self._removal_pending
316
+ has_owned_left = any(
317
+ owner is strategy for owner in self._symbol_owner.values()
318
+ )
319
+ if pending and not has_owned_left:
320
+ try:
321
+ if bool(strategy.is_flat()):
322
+ with self._lock:
323
+ if strategy in self._strategies:
324
+ self._strategies.remove(strategy)
325
+ self._removal_pending.discard(strategy)
326
+ console.logger.info(
327
+ f"Strategy {getattr(strategy, 'name', type(strategy).__name__)} removed: all symbols released and flat"
328
+ )
329
+ except Exception:
330
+ pass
331
+ return removed
332
+
333
+ def on_symbol_release(self, event: events.Base.Event) -> None:
334
+ """
335
+ Handle symbol release events. Ignores unrelated event types.
336
+ """
337
+ if not isinstance(event, events.Strategy.SymbolRelease):
338
+ return
339
+ strategy = event.strategy
340
+ with self._lock:
341
+ if strategy not in self._strategies:
342
+ console.logger.warning("on_symbol_release: strategy not registered")
343
+ return
344
+ removed = self.release_symbols_from_strategy(strategy, [event.symbol])
345
+ if not removed:
346
+ console.logger.warning(
347
+ f"on_symbol_release: symbol {event.symbol} not owned by {getattr(event.strategy, 'name', type(event.strategy).__name__)}"
348
+ )
@@ -12,14 +12,10 @@ class BaseDatafeed(abc.ABC):
12
12
  Base class for all datafeeds.
13
13
  """
14
14
 
15
- def __init__(self, event_bus: messaging.EventBus):
16
- """
17
- Initialize the datafeed with an event bus.
18
-
19
- Args:
20
- event_bus (messaging.EventBus): Event bus to publish market data events to.
21
- """
22
- self.event_bus: messaging.EventBus = event_bus
15
+ def __init__(self, event_bus: messaging.EventBus | None = None):
16
+ self.event_bus: messaging.EventBus = (
17
+ event_bus if event_bus else messaging.system_event_bus
18
+ )
23
19
 
24
20
  @abc.abstractmethod
25
21
  def connect(self):
@@ -1,8 +1,9 @@
1
- from .eventbus import EventBus as EventBus
1
+ from .eventbus import EventBus as EventBus, system_event_bus as system_event_bus
2
2
  from .events import (
3
3
  Base as Base,
4
4
  Market as Market,
5
5
  Request as Request,
6
6
  Response as Response,
7
7
  System as System,
8
+ Strategy as Strategy,
8
9
  )
@@ -12,6 +12,12 @@ from onesecondtrader.messaging import events
12
12
  from onesecondtrader.monitoring import console
13
13
 
14
14
 
15
+ __all__ = [
16
+ "EventBus",
17
+ "system_event_bus",
18
+ ]
19
+
20
+
15
21
  class EventBus:
16
22
  # noinspection PyTypeChecker
17
23
  """
@@ -488,3 +494,6 @@ class EventBus:
488
494
  f"total handlers: "
489
495
  f"{sum(len(handlers) for handlers in new_cache.values())}"
490
496
  )
497
+
498
+
499
+ system_event_bus = EventBus()
@@ -1,8 +1,8 @@
1
1
  """
2
2
  This module provides the event messages used for decoupled communication between the
3
3
  trading infrastructure's components.
4
- Events are organized into namespaces (`Market`, `Request`, `Response`, and `System`)
5
- to provide clear semantic groupings.
4
+ Events are organized into namespaces (`Strategy`, `Market`, `Request`, `Response`, and
5
+ `System`) to provide clear semantic groupings.
6
6
  Base event messages used for structure inheritance are grouped under the
7
7
  `Base` namespace.
8
8
  Dataclass field validation logic is grouped under the `_Validate` namespace.
@@ -23,11 +23,13 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
23
23
  R22[events.Base.CancelRequest]
24
24
  R3[events.Base.Response]
25
25
  R4[events.Base.System]
26
+ R5[events.Base.Strategy]
26
27
 
27
28
  R --> R1
28
29
  R --> R2
29
30
  R --> R3
30
31
  R --> R4
32
+ R --> R5
31
33
 
32
34
  R2 --> R21
33
35
  R2 --> R22
@@ -83,6 +85,14 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
83
85
 
84
86
  style D1 fill:#6F42C1,fill-opacity:0.3
85
87
 
88
+ E1[events.Strategy.SymbolRelease]
89
+ E2[events.Strategy.StopTrading]
90
+
91
+ R5 --> E1
92
+ R5 --> E2
93
+
94
+ style E1 fill:#6F42C1,fill-opacity:0.3
95
+ style E2 fill:#6F42C1,fill-opacity:0.3
86
96
 
87
97
  subgraph Market ["Market Update Event Messages"]
88
98
  R1
@@ -143,6 +153,18 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
143
153
  D1
144
154
  end
145
155
 
156
+ end
157
+
158
+ subgraph Strategy ["Strategy Coord. Event Messages"]
159
+ R5
160
+ E1
161
+ E2
162
+
163
+ subgraph StrategyNamespace ["events.Strategy Namespace"]
164
+ E1
165
+ E2
166
+ end
167
+
146
168
  end
147
169
  ```
148
170
  """
@@ -153,6 +175,7 @@ import re
153
175
  import uuid
154
176
  from onesecondtrader.core import models
155
177
  from onesecondtrader.monitoring import console
178
+ from onesecondtrader.strategies import base_strategy
156
179
 
157
180
 
158
181
  class Base:
@@ -280,7 +303,7 @@ class Base:
280
303
  )
281
304
  _Validate.quantity(self.quantity, f"Order {self.order_id}")
282
305
 
283
- if self.time_in_force.value == 4:
306
+ if self.time_in_force is models.TimeInForce.GTD:
284
307
  if self.order_expiration is None:
285
308
  console.logger.error(
286
309
  f"Order {self.order_id}: GTD order missing expiration "
@@ -345,6 +368,36 @@ class Base:
345
368
  )
346
369
  return super().__new__(cls)
347
370
 
371
+ @dataclasses.dataclass(kw_only=True, frozen=True)
372
+ class Strategy(Event):
373
+ """
374
+ Base event message dataclass for strategy coordination messages.
375
+ This dataclass cannot be instantiated directly.
376
+
377
+ Attributes:
378
+ ts_event: Timestamp of the event. (defaults to current UTC time;
379
+ auto-generated)
380
+ """
381
+
382
+ ts_event: pd.Timestamp = dataclasses.field(
383
+ default_factory=lambda: pd.Timestamp.now(tz="UTC")
384
+ )
385
+ strategy: base_strategy.Strategy
386
+
387
+ def __new__(cls, *args, **kwargs):
388
+ if cls is Base.Strategy:
389
+ console.logger.error(
390
+ f"Cannot instantiate abstract class '{cls.__name__}' directly"
391
+ )
392
+ return super().__new__(cls)
393
+
394
+ def __post_init__(self) -> None:
395
+ super().__post_init__()
396
+ if not isinstance(self.strategy, base_strategy.Strategy):
397
+ console.logger.error(
398
+ f"{type(self).__name__}: strategy must inherit from Strategy"
399
+ )
400
+
348
401
 
349
402
  class Market:
350
403
  """
@@ -374,7 +427,7 @@ class Market:
374
427
  ... volume=10000,
375
428
  ... ),
376
429
  ... )
377
- ```
430
+
378
431
  """
379
432
 
380
433
  bar: models.Bar
@@ -621,7 +674,7 @@ class Response:
621
674
 
622
675
  gross_value = self.filled_at_price * self.quantity_filled
623
676
 
624
- if self.side.value == 1:
677
+ if self.side is models.Side.BUY:
625
678
  net_value = gross_value + self.commission_and_fees
626
679
  else:
627
680
  net_value = gross_value - self.commission_and_fees
@@ -699,6 +752,51 @@ class System:
699
752
  pass
700
753
 
701
754
 
755
+ class Strategy:
756
+ """
757
+ Namespace for strategy coordination event messages.
758
+ """
759
+
760
+ @dataclasses.dataclass(kw_only=True, frozen=True)
761
+ class SymbolRelease(Base.Strategy):
762
+ """
763
+ Event to indicate a strategy releases ownership of a symbol.
764
+
765
+ Attributes:
766
+ symbol (str): Symbol released.
767
+
768
+ Examples:
769
+ >>> from onesecondtrader.messaging import events
770
+ >>> event = events.Strategy.SymbolRelease(
771
+ ... symbol="AAPL",
772
+ ... )
773
+ """
774
+
775
+ symbol: str
776
+
777
+ def __post_init__(self) -> None:
778
+ super().__post_init__()
779
+ _Validate.symbol(self.symbol, "Strategy.SymbolRelease")
780
+
781
+ @dataclasses.dataclass(kw_only=True, frozen=True)
782
+ class StopTrading(Base.Strategy):
783
+ """
784
+ Event to indicate a strategy should stop trading.
785
+
786
+ Attributes:
787
+ shutdown_mode (models.StrategyShutdownMode): Shutdown mode to use.
788
+
789
+ Examples:
790
+ >>> from onesecondtrader.messaging import events
791
+ >>> event = events.Strategy.StopTrading(
792
+ ... strategy=my_strategy,
793
+ ... shutdown_mode=models.StrategyShutdownMode.SOFT,
794
+ ... )
795
+ """
796
+
797
+ shutdown_mode: models.StrategyShutdownMode
798
+
799
+
702
800
  class _Validate:
703
801
  """Internal validation utilities for events."""
704
802
 
File without changes
@@ -0,0 +1,46 @@
1
+ import abc
2
+ import threading
3
+ from onesecondtrader.messaging.eventbus import EventBus, system_event_bus
4
+ from onesecondtrader.core import models
5
+
6
+
7
+ class Strategy(abc.ABC):
8
+ def __init__(self, name: str, event_bus: EventBus | None = None):
9
+ self.name = name
10
+ self.event_bus = event_bus if event_bus else system_event_bus
11
+ self._lock = threading.Lock()
12
+ self._active_symbols: set[str] = set()
13
+ self._close_only_symbols: set[str] = set()
14
+ self._close_open_positions_only: bool = False
15
+ self._close_mode: models.StrategyShutdownMode | None = None
16
+
17
+ def __repr__(self) -> str:
18
+ return f"{type(self).__name__}(name='{self.name}')"
19
+
20
+ def request_close(
21
+ self, mode: models.StrategyShutdownMode = models.StrategyShutdownMode.SOFT
22
+ ) -> None:
23
+ # Minimalist soft/hard close signalling; subclasses act on this
24
+ self._close_open_positions_only = True
25
+ self._close_mode = mode
26
+
27
+ @abc.abstractmethod
28
+ def is_flat(self) -> bool:
29
+ raise NotImplementedError
30
+
31
+ def add_symbols(self, symbols: list[str]) -> None:
32
+ """
33
+ Add symbols to the strategy. Thread-safe and idempotent.
34
+ """
35
+ clean = [s.strip() for s in symbols if s and s.strip()]
36
+ with self._lock:
37
+ self._active_symbols.update(clean)
38
+
39
+ def remove_symbols(self, symbols: list[str]) -> None:
40
+ """
41
+ Remove symbols from the strategy. Thread-safe and idempotent.
42
+ """
43
+ clean = [s.strip() for s in symbols if s and s.strip()]
44
+ with self._lock:
45
+ for sym in clean:
46
+ self._active_symbols.discard(sym)