onesecondtrader 0.14.0__tar.gz → 0.14.2__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.14.0 → onesecondtrader-0.14.2}/PKG-INFO +1 -1
  2. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/pyproject.toml +1 -1
  3. onesecondtrader-0.14.2/src/onesecondtrader/brokers/base_broker.py +99 -0
  4. onesecondtrader-0.14.2/src/onesecondtrader/brokers/simulated_broker.py +10 -0
  5. onesecondtrader-0.14.2/src/onesecondtrader/core/portfolio.py +359 -0
  6. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/messaging/__init__.py +1 -1
  7. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/messaging/events.py +85 -17
  8. onesecondtrader-0.14.2/src/onesecondtrader/py.typed +0 -0
  9. onesecondtrader-0.14.2/src/onesecondtrader/strategies/__init__.py +0 -0
  10. onesecondtrader-0.14.2/src/onesecondtrader/strategies/base_strategy.py +46 -0
  11. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/LICENSE +0 -0
  12. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/README.md +0 -0
  13. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/__init__.py +0 -0
  14. {onesecondtrader-0.14.0/src/onesecondtrader/core → onesecondtrader-0.14.2/src/onesecondtrader/brokers}/__init__.py +0 -0
  15. {onesecondtrader-0.14.0/src/onesecondtrader/datafeeds → onesecondtrader-0.14.2/src/onesecondtrader/core}/__init__.py +0 -0
  16. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/core/models.py +0 -0
  17. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/core/py.typed +0 -0
  18. {onesecondtrader-0.14.0/src/onesecondtrader/indicators → onesecondtrader-0.14.2/src/onesecondtrader/datafeeds}/__init__.py +0 -0
  19. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/datafeeds/base_datafeed.py +0 -0
  20. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/datafeeds/csv_datafeed.py +0 -0
  21. {onesecondtrader-0.14.0/src/onesecondtrader/monitoring → onesecondtrader-0.14.2/src/onesecondtrader/indicators}/__init__.py +0 -0
  22. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/indicators/base_indicator.py +0 -0
  23. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/indicators/moving_averages.py +0 -0
  24. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/messaging/eventbus.py +0 -0
  25. /onesecondtrader-0.14.0/src/onesecondtrader/monitoring/py.typed → /onesecondtrader-0.14.2/src/onesecondtrader/monitoring/__init__.py +0 -0
  26. {onesecondtrader-0.14.0 → onesecondtrader-0.14.2}/src/onesecondtrader/monitoring/console.py +0 -0
  27. {onesecondtrader-0.14.0/src/onesecondtrader → onesecondtrader-0.14.2/src/onesecondtrader/monitoring}/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.14.0
3
+ Version: 0.14.2
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.14.0"
3
+ version = "0.14.2"
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)
@@ -0,0 +1,359 @@
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.strategy_removal_pending (set[base_strategy.Strategy]): Set of
51
+ strategies that are still active but marked for removal once all symbols
52
+ are released.
53
+ self.broker (base_broker.BaseBroker | None): Instantiated broker; may be
54
+ disconnected if connect failed.
55
+ """
56
+ # INITIALIZE EVENT BUS
57
+ # ------------------------------------------------------------------------------
58
+ self.event_bus: messaging.EventBus = (
59
+ event_bus if event_bus else messaging.system_event_bus
60
+ )
61
+
62
+ # SUBSCRIBE HANDLER METHODS TO EVENTS VIA event_bus.subscribe
63
+ # ------------------------------------------------------------------------------
64
+ self.event_bus.subscribe(events.Strategy.SymbolRelease, self.on_symbol_release)
65
+
66
+ # INITIALIZE LOCK FOR THREAD-SAFE OPERATIONS WITHIN THE PORTFOLIO
67
+ # ------------------------------------------------------------------------------
68
+ self._lock = threading.Lock()
69
+
70
+ # KEEP TRACK OF STRATEGIES AND SYMBOL OWNERSHIP
71
+ # ------------------------------------------------------------------------------
72
+ self._strategies: set[base_strategy.Strategy] = set()
73
+ self.strategy_removal_pending: set[base_strategy.Strategy] = set()
74
+ self._symbol_owner: dict[str, base_strategy.Strategy] = {}
75
+
76
+ # INITIALIZE BROKER
77
+ # ------------------------------------------------------------------------------
78
+ # TODO Decouple by also doing this via an event
79
+ self.broker: base_broker.BaseBroker | None = None
80
+ if broker_class is None or not issubclass(broker_class, base_broker.BaseBroker):
81
+ broker_name = (
82
+ getattr(broker_class, "__name__", str(broker_class))
83
+ if broker_class
84
+ else None
85
+ )
86
+ console.logger.error(
87
+ "Portfolio requires a valid broker_class (subclass of BaseBroker), "
88
+ f"got {broker_name}"
89
+ )
90
+ return
91
+ try:
92
+ self.broker = broker_class(self.event_bus)
93
+ except Exception as e:
94
+ console.logger.error(
95
+ f"Failed to instantiate broker "
96
+ f"{getattr(broker_class, '__name__', str(broker_class))}: {e}"
97
+ )
98
+ return
99
+
100
+ # CONNECT TO BROKER
101
+ # ------------------------------------------------------------------------------
102
+ try:
103
+ connected = self.broker.connect()
104
+ if not connected:
105
+ console.logger.error(
106
+ f"Failed to connect broker {type(self.broker).__name__}"
107
+ )
108
+ except Exception as e:
109
+ console.logger.error(f"Broker connect failed: {e}")
110
+
111
+ def add_strategy(
112
+ self,
113
+ strategy_instance: base_strategy.Strategy,
114
+ symbols: Iterable[str] | None = None,
115
+ ) -> bool:
116
+ """
117
+ Register a Strategy instance and optionally assign a list of symbols to it.
118
+
119
+ If symbols are provided, potential conflicts are checked first under a lock.
120
+ If any conflicts exist, no symbols are assigned; a warning is logged listing
121
+ both non_conflicting and conflicting symbols and instructions to use
122
+ assign_symbols(...) are provided.
123
+ If no conflicts exist, all provided symbols are claimed by the strategy.
124
+
125
+ Args:
126
+ strategy_instance (base_strategy.Strategy): Strategy instance to register.
127
+ symbols (Iterable[str] | None): Optional list of symbols to assign to the
128
+ strategy.
129
+
130
+ Returns:
131
+ bool: True if the strategy was registered, False otherwise.
132
+ """
133
+ # VALIDATE THAT INSTANCE IS A SUBCLASS OF base_strategy.Strategy
134
+ # ------------------------------------------------------------------------------
135
+ if not isinstance(strategy_instance, base_strategy.Strategy):
136
+ console.logger.error("add_strategy: strategy must inherit from Strategy")
137
+ return False
138
+
139
+ # ADD STRATEGY INSTANCE TO REGISTRY IF NOT ALREADY REGISTERED
140
+ # ------------------------------------------------------------------------------
141
+ with self._lock:
142
+ if strategy_instance in self._strategies:
143
+ console.logger.warning("add_strategy: strategy already registered")
144
+ return False
145
+ self._strategies.add(strategy_instance)
146
+
147
+ # ASSIGN SYMBOLS IF PROVIDED AND NO CONFLICTS EXIST, ELSE LOG WARNING
148
+ # ------------------------------------------------------------------------------
149
+ if symbols is not None:
150
+ # Create an ordered list of unique, non-empty, trimmed symbols
151
+ symbols_list = list(
152
+ dict.fromkeys(s.strip() for s in symbols if s and s.strip())
153
+ )
154
+
155
+ # Check for conflicts, claim symbols for strategy if no conflicts arise
156
+ if symbols_list:
157
+ non_conflicting: list[str] = []
158
+ conflicting: list[str] = []
159
+ with self._lock:
160
+ for sym in symbols_list:
161
+ owner = self._symbol_owner.get(sym)
162
+ if owner is None or owner is strategy_instance:
163
+ non_conflicting.append(sym)
164
+ else:
165
+ conflicting.append(sym)
166
+ if conflicting:
167
+ console.logger.warning(
168
+ "add_strategy: symbols not assigned due to conflicts; "
169
+ "use Portfolio.assign_symbols(...) after resolving. "
170
+ f"non_conflicting={non_conflicting}, conflicts={conflicting}"
171
+ )
172
+ return False
173
+ else:
174
+ for sym in symbols_list:
175
+ self._symbol_owner[sym] = strategy_instance
176
+ self.event_bus.publish(
177
+ events.Strategy.SymbolAssignment(
178
+ strategy=strategy_instance,
179
+ symbol_list=symbols_list,
180
+ )
181
+ )
182
+ return True
183
+
184
+ return True
185
+
186
+ def remove_strategy(
187
+ self,
188
+ strategy_instance: base_strategy.Strategy,
189
+ shutdown_mode: StrategyShutdownMode = StrategyShutdownMode.SOFT,
190
+ ) -> bool:
191
+ """
192
+ Mark a strategy for removal and request it to close its positions in the manner
193
+ dictated via the `shutdown_mode` argument (default to soft shutdown, i.e. wait
194
+ for open positions to close naturally and release symbols once they are flat).
195
+
196
+ Args:
197
+ strategy_instance (base_strategy.Strategy): Strategy instance to remove.
198
+ shutdown_mode (StrategyShutdownMode): Shutdown mode to use. Defaults to
199
+ StrategyShutdownMode.SOFT.
200
+ """
201
+
202
+ # IF STRATEGY IS REGISTERED, MARK IT FOR REMOVAL AND PUBLISH STOP TRADING EVENT
203
+ # ------------------------------------------------------------------------------
204
+ with self._lock:
205
+ if strategy_instance not in self._strategies:
206
+ console.logger.warning("remove_strategy: strategy not registered")
207
+ return False
208
+ self.strategy_removal_pending.add(strategy_instance)
209
+
210
+ # PUBLISH STOP TRADING REQUEST TO EVENT BUS
211
+ # ------------------------------------------------------------------------------
212
+ self.event_bus.publish(
213
+ events.Strategy.StopTrading(
214
+ strategy=strategy_instance,
215
+ shutdown_mode=shutdown_mode,
216
+ )
217
+ )
218
+ return True
219
+
220
+ def assign_symbols(
221
+ self, strategy_instance: base_strategy.Strategy, symbols: Iterable[str]
222
+ ):
223
+ """
224
+ Assign a list of symbols to a strategy.
225
+
226
+ Args:
227
+ strategy_instance (base_strategy.Strategy): Strategy instance to assign
228
+ symbols to.
229
+ symbols (Iterable[str]): List of symbols to assign.
230
+ """
231
+ # IF STRATEGY IS REGISTERED, MARK IT FOR REMOVAL AND PUBLISH STOP TRADING EVENT
232
+ # ------------------------------------------------------------------------------
233
+ with self._lock:
234
+ if strategy_instance not in self._strategies:
235
+ console.logger.warning("remove_strategy: strategy not registered")
236
+ return False
237
+ self.strategy_removal_pending.add(strategy_instance)
238
+
239
+ # ASSIGN SYMBOLS IF PROVIDED AND NO CONFLICTS EXIST, ELSE LOG WARNING
240
+ # ------------------------------------------------------------------------------
241
+ # TODO This is an repetition of the same logic as in add_strategy; refactor
242
+
243
+ if symbols is not None:
244
+ # Create an ordered list of unique, non-empty, trimmed symbols
245
+ symbols_list = list(
246
+ dict.fromkeys(s.strip() for s in symbols if s and s.strip())
247
+ )
248
+
249
+ # Check for conflicts, claim symbols for strategy if no conflicts arise
250
+ if symbols_list:
251
+ non_conflicting: list[str] = []
252
+ conflicting: list[str] = []
253
+ with self._lock:
254
+ for sym in symbols_list:
255
+ owner = self._symbol_owner.get(sym)
256
+ if owner is None or owner is strategy_instance:
257
+ non_conflicting.append(sym)
258
+ else:
259
+ conflicting.append(sym)
260
+ if conflicting:
261
+ console.logger.warning(
262
+ "assign_symbols: symbols not assigned due to conflicts; "
263
+ "use Portfolio.assign_symbols(...) after resolving. "
264
+ f"non_conflicting={non_conflicting}, conflicts={conflicting}"
265
+ )
266
+ return False
267
+ else:
268
+ for sym in symbols_list:
269
+ self._symbol_owner[sym] = strategy_instance
270
+ self.event_bus.publish(
271
+ events.Strategy.SymbolAssignment(
272
+ strategy=strategy_instance,
273
+ symbol_list=symbols_list,
274
+ )
275
+ )
276
+ return True
277
+
278
+ def unassign_symbols(
279
+ self, strategy: base_strategy.Strategy, symbols: Iterable[str]
280
+ ) -> list[str]:
281
+ if not isinstance(strategy, base_strategy.Strategy):
282
+ console.logger.error(
283
+ "unassign_symbols: strategy must inherit from Strategy"
284
+ )
285
+ return []
286
+ symbols_list = list(
287
+ dict.fromkeys(s.strip() for s in symbols if s and s.strip())
288
+ )
289
+ if not symbols_list:
290
+ return []
291
+ removed: list[str] = []
292
+ with self._lock:
293
+ for sym in symbols_list:
294
+ if self._symbol_owner.get(sym) is strategy:
295
+ del self._symbol_owner[sym]
296
+ removed.append(sym)
297
+ if removed:
298
+ strategy.remove_symbols(removed)
299
+ return removed
300
+
301
+ def owner_of(self, symbol: str) -> base_strategy.Strategy | None:
302
+ """
303
+ Return the owning strategy for a symbol or None if unowned.
304
+ """
305
+ with self._lock:
306
+ return self._symbol_owner.get(symbol)
307
+
308
+ def on_symbol_release(self, event: events.Base.Event) -> None:
309
+ """
310
+ Handle symbol release events. Ignores unrelated event types.
311
+
312
+ If a symbol is released, it is unassigned from the strategy that owns it
313
+ (inside the symbol_owner registry). If the strategy is marked for removal and
314
+ has no more symbols assigned to it, it is automatically deregistered.
315
+ """
316
+
317
+ # IGNORE UNRELATED EVENT TYPES
318
+ # ------------------------------------------------------------------------------
319
+ if not isinstance(event, events.Strategy.SymbolRelease):
320
+ return
321
+
322
+ # IF STRATEGY IS REGISTERED, RELEASE SYMBOL FROM STRATEGY
323
+ # ------------------------------------------------------------------------------
324
+ # TODO This needs reworking; this will be the same logic as unassig_symbols
325
+ strategy = event.strategy
326
+ with self._lock:
327
+ if strategy not in self._strategies:
328
+ console.logger.warning("on_symbol_release: strategy not registered")
329
+ return
330
+ removed = self.unassign_symbols(strategy, [event.symbol])
331
+ if not removed:
332
+ console.logger.warning(
333
+ f"on_symbol_release: symbol {event.symbol} not owned by "
334
+ f"{getattr(event.strategy, 'name', type(event.strategy).__name__)}"
335
+ )
336
+ return
337
+ pending = False
338
+ with self._lock:
339
+ pending = strategy in self.strategy_removal_pending
340
+ has_owned_left = any(
341
+ owner is strategy for owner in self._symbol_owner.values()
342
+ )
343
+ if pending and not has_owned_left:
344
+ try:
345
+ if bool(strategy.is_flat()):
346
+ with self._lock:
347
+ if strategy in self._strategies:
348
+ self._strategies.remove(strategy)
349
+ self.strategy_removal_pending.discard(strategy)
350
+ console.logger.info(
351
+ f"Strategy {getattr(strategy, 'name', type(strategy).__name__)} "
352
+ f"removed: all symbols released and flat"
353
+ )
354
+ except Exception:
355
+ pass
356
+ console.logger.info(
357
+ f"on_symbol_release: symbol {event.symbol} released from "
358
+ f"{getattr(event.strategy, 'name', type(event.strategy).__name__)}"
359
+ )
@@ -5,5 +5,5 @@ from .events import (
5
5
  Request as Request,
6
6
  Response as Response,
7
7
  System as System,
8
- Portfolio as Portfolio,
8
+ Strategy as Strategy,
9
9
  )
@@ -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,7 +23,7 @@ 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.Portfolio]
26
+ R5[events.Base.Strategy]
27
27
 
28
28
  R --> R1
29
29
  R --> R2
@@ -85,11 +85,17 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
85
85
 
86
86
  style D1 fill:#6F42C1,fill-opacity:0.3
87
87
 
88
- E1[events.Portfolio.SymbolRelease]
88
+ E1[events.Strategy.SymbolRelease]
89
+ E2[events.Strategy.SymbolAssignment]
90
+ E3[events.Strategy.StopTrading]
89
91
 
90
92
  R5 --> E1
93
+ R5 --> E2
94
+ R5 --> E3
91
95
 
92
96
  style E1 fill:#6F42C1,fill-opacity:0.3
97
+ style E2 fill:#6F42C1,fill-opacity:0.3
98
+ style E3 fill:#6F42C1,fill-opacity:0.3
93
99
 
94
100
  subgraph Market ["Market Update Event Messages"]
95
101
  R1
@@ -152,12 +158,16 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
152
158
 
153
159
  end
154
160
 
155
- subgraph Portfolio ["Portfolio Coord. Event Messages"]
161
+ subgraph Strategy ["Strategy Coord. Event Messages"]
156
162
  R5
157
163
  E1
164
+ E2
165
+ E3
158
166
 
159
- subgraph PortfolioNamespace ["events.Portfolio Namespace"]
167
+ subgraph StrategyNamespace ["events.Strategy Namespace"]
160
168
  E1
169
+ E2
170
+ E3
161
171
  end
162
172
 
163
173
  end
@@ -170,6 +180,7 @@ import re
170
180
  import uuid
171
181
  from onesecondtrader.core import models
172
182
  from onesecondtrader.monitoring import console
183
+ from onesecondtrader.strategies import base_strategy
173
184
 
174
185
 
175
186
  class Base:
@@ -297,7 +308,7 @@ class Base:
297
308
  )
298
309
  _Validate.quantity(self.quantity, f"Order {self.order_id}")
299
310
 
300
- if self.time_in_force.value == 4:
311
+ if self.time_in_force is models.TimeInForce.GTD:
301
312
  if self.order_expiration is None:
302
313
  console.logger.error(
303
314
  f"Order {self.order_id}: GTD order missing expiration "
@@ -363,9 +374,9 @@ class Base:
363
374
  return super().__new__(cls)
364
375
 
365
376
  @dataclasses.dataclass(kw_only=True, frozen=True)
366
- class Portfolio(Event):
377
+ class Strategy(Event):
367
378
  """
368
- Base event message dataclass for portfolio coordination messages.
379
+ Base event message dataclass for strategy coordination messages.
369
380
  This dataclass cannot be instantiated directly.
370
381
 
371
382
  Attributes:
@@ -376,14 +387,22 @@ class Base:
376
387
  ts_event: pd.Timestamp = dataclasses.field(
377
388
  default_factory=lambda: pd.Timestamp.now(tz="UTC")
378
389
  )
390
+ strategy: base_strategy.Strategy
379
391
 
380
392
  def __new__(cls, *args, **kwargs):
381
- if cls is Base.Portfolio:
393
+ if cls is Base.Strategy:
382
394
  console.logger.error(
383
395
  f"Cannot instantiate abstract class '{cls.__name__}' directly"
384
396
  )
385
397
  return super().__new__(cls)
386
398
 
399
+ def __post_init__(self) -> None:
400
+ super().__post_init__()
401
+ if not isinstance(self.strategy, base_strategy.Strategy):
402
+ console.logger.error(
403
+ f"{type(self).__name__}: strategy must inherit from Strategy"
404
+ )
405
+
387
406
 
388
407
  class Market:
389
408
  """
@@ -413,7 +432,7 @@ class Market:
413
432
  ... volume=10000,
414
433
  ... ),
415
434
  ... )
416
- ```
435
+
417
436
  """
418
437
 
419
438
  bar: models.Bar
@@ -660,7 +679,7 @@ class Response:
660
679
 
661
680
  gross_value = self.filled_at_price * self.quantity_filled
662
681
 
663
- if self.side.value == 1:
682
+ if self.side is models.Side.BUY:
664
683
  net_value = gross_value + self.commission_and_fees
665
684
  else:
666
685
  net_value = gross_value - self.commission_and_fees
@@ -738,23 +757,72 @@ class System:
738
757
  pass
739
758
 
740
759
 
741
- class Portfolio:
760
+ class Strategy:
742
761
  """
743
- Namespace for portfolio coordination event messages.
762
+ Namespace for strategy coordination event messages.
744
763
  """
745
764
 
746
765
  @dataclasses.dataclass(kw_only=True, frozen=True)
747
- class SymbolRelease(Base.Portfolio):
766
+ class SymbolRelease(Base.Strategy):
748
767
  """
749
768
  Event to indicate a strategy releases ownership of a symbol.
769
+
770
+ Attributes:
771
+ symbol (str): Symbol released.
772
+
773
+ Examples:
774
+ >>> from onesecondtrader.messaging import events
775
+ >>> event = events.Strategy.SymbolRelease(
776
+ ... symbol="AAPL",
777
+ ... )
750
778
  """
751
779
 
752
780
  symbol: str
753
- strategy_name: str
754
781
 
755
782
  def __post_init__(self) -> None:
756
783
  super().__post_init__()
757
- _Validate.symbol(self.symbol, "Portfolio.SymbolRelease")
784
+ _Validate.symbol(self.symbol, "Strategy.SymbolRelease")
785
+
786
+ @dataclasses.dataclass(kw_only=True, frozen=True)
787
+ class SymbolAssignment(Base.Strategy):
788
+ """
789
+ Event message to indicate that a symbol should be assigned to a strategy.
790
+
791
+ Attributes:
792
+ symbol_list (list[str]): List of symbols to be assigned.
793
+
794
+ Examples:
795
+ >>> from onesecondtrader.messaging import events
796
+ >>> event = events.Strategy.SymbolAssignment(
797
+ ... strategy=my_strategy,
798
+ ... symbol=["AAPL"],
799
+ ... )
800
+ """
801
+
802
+ symbol_list: list[str]
803
+
804
+ def __post_init__(self) -> None:
805
+ super().__post_init__()
806
+ for symbol in self.symbol_list:
807
+ _Validate.symbol(symbol, "Strategy.SymbolAssignment")
808
+
809
+ @dataclasses.dataclass(kw_only=True, frozen=True)
810
+ class StopTrading(Base.Strategy):
811
+ """
812
+ Event to indicate a strategy should stop trading.
813
+
814
+ Attributes:
815
+ shutdown_mode (models.StrategyShutdownMode): Shutdown mode to use.
816
+
817
+ Examples:
818
+ >>> from onesecondtrader.messaging import events
819
+ >>> event = events.Strategy.StopTrading(
820
+ ... strategy=my_strategy,
821
+ ... shutdown_mode=models.StrategyShutdownMode.SOFT,
822
+ ... )
823
+ """
824
+
825
+ shutdown_mode: models.StrategyShutdownMode
758
826
 
759
827
 
760
828
  class _Validate:
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)