onesecondtrader 0.14.1__py3-none-any.whl → 0.15.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.
@@ -186,3 +186,19 @@ class StrategyShutdownMode(enum.Enum):
186
186
 
187
187
  SOFT = enum.auto()
188
188
  HARD = enum.auto()
189
+
190
+
191
+ class SymbolShutdownMode(enum.Enum):
192
+ """
193
+ Enum for symbol shutdown modes.
194
+
195
+ **Attributes:**
196
+
197
+ | Enum | Value | Description |
198
+ |------|-------|-------------|
199
+ | `SOFT` | `enum.auto()` | Do not open new positions; wait until current positions close naturally |
200
+ | `HARD` | `enum.auto()` | Close all positions immediately with market orders |
201
+ """
202
+
203
+ SOFT = enum.auto()
204
+ HARD = enum.auto()
@@ -1,348 +1,177 @@
1
- """
2
- This module provides the Portfolio class.
3
- """
4
-
5
1
  import threading
6
- from collections.abc import Iterable
2
+
7
3
  from onesecondtrader import messaging
8
4
  from onesecondtrader.messaging import events
9
- from onesecondtrader.monitoring import console
10
- from onesecondtrader.brokers import base_broker
11
5
  from onesecondtrader.strategies import base_strategy
12
- from onesecondtrader.core.models import StrategyShutdownMode
6
+ from onesecondtrader.monitoring import console
7
+ from onesecondtrader.core import models
13
8
 
14
9
 
15
10
  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
- ):
11
+ def __init__(self, event_bus: messaging.EventBus | None = None):
32
12
  """
33
- Initialize the Portfolio class, subscribe to events, and connect to the broker.
13
+ Initialize the Portfolio class and subscribe to events.
14
+ Most importantly, the `symbol_to_strategy` registry is initialized,
15
+ which keeps track of which symbols are currently assigned to which strategy
16
+ in order to enforce exclusive symbol ownership.
34
17
 
35
18
  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.
19
+ event_bus (messaging.EventBus | None): Event bus to use; defaults to
20
+ messaging.system_event_bus when None.
40
21
 
41
22
  Attributes:
42
- self.event_bus (eventbus.EventBus): Event bus used for communication between
43
- the trading infrastructure's components.
44
23
  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.
24
+ self.event_bus (messaging.EventBus): Event bus used for communication
25
+ between the trading infrastructure's components.
26
+ self.symbols_to_strategy (dict[str, base_strategy.Strategy]): Registry of
27
+ symbols to strategies.
54
28
  """
55
- # INITIALIZE EVENT BUS
56
29
  # ------------------------------------------------------------------------------
30
+ # INITIALIZE LOCK FOR THREAD-SAFE OPERATIONS
31
+ self._lock: threading.Lock = threading.Lock()
32
+
33
+ # ------------------------------------------------------------------------------
34
+ # INITIALIZE EVENT BUS AND SUBSCRIBE TO EVENTS
57
35
  self.event_bus: messaging.EventBus = (
58
36
  event_bus if event_bus else messaging.system_event_bus
59
37
  )
60
-
61
- # SUBSCRIBE HANDLER METHODS TO EVENTS VIA event_bus.subscribe
62
- # ------------------------------------------------------------------------------
63
38
  self.event_bus.subscribe(events.Strategy.SymbolRelease, self.on_symbol_release)
64
39
 
65
- # INITIALIZE LOCK FOR THREAD-SAFE OPERATIONS WITHIN THE PORTFOLIO
66
40
  # ------------------------------------------------------------------------------
67
- self._lock = threading.Lock()
41
+ # INITIALIZE SYMBOLS TO STRATEGY REGISTRY
42
+ self.symbols_to_strategy: dict[str, base_strategy.Strategy] = {}
68
43
 
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()
44
+ def on_symbol_release(self, event: messaging.events.Base.Event) -> None:
45
+ """
46
+ Event handler for symbol release events (`events.Strategy.SymbolRelease`).
47
+ The symbol is removed from the `symbols_to_strategy` registry.
74
48
 
75
- # INITIALIZE BROKER
49
+ Args:
50
+ event (messaging.events.Base.Event): Symbol release event.
51
+ """
76
52
  # ------------------------------------------------------------------------------
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
- )
53
+ # IGNORE UNRELATED EVENT TYPES
54
+ if not isinstance(event, events.Strategy.SymbolRelease):
96
55
  return
97
56
 
98
- # CONNECT TO BROKER
99
57
  # ------------------------------------------------------------------------------
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__}"
58
+ # RELEASE SYMBOL FROM STRATEGY
59
+ symbol = event.symbol
60
+ with self._lock:
61
+ if symbol in self.symbols_to_strategy:
62
+ del self.symbols_to_strategy[symbol]
63
+ console.logger.info(
64
+ f"on_symbol_release: symbol {symbol} released from "
65
+ f"{getattr(event.strategy, 'name', type(event.strategy).__name__)}"
66
+ )
67
+ else:
68
+ console.logger.warning(
69
+ f"on_symbol_release: symbol {symbol} not owned by "
70
+ f"{getattr(event.strategy, 'name', type(event.strategy).__name__)}"
105
71
  )
106
- except Exception as e:
107
- console.logger.error(f"Broker connect failed: {e}")
108
72
 
109
- def add_strategy(
110
- self,
111
- strategy_instance: base_strategy.Strategy,
112
- symbols: Iterable[str] | None = None,
73
+ def assign_symbols(
74
+ self, strategy_instance: base_strategy.Strategy, symbols: list[str]
113
75
  ) -> bool:
114
76
  """
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.
77
+ Assign a list of symbols to a strategy if no conflicts exist and notify the
78
+ strategy of the assignment.
122
79
 
123
80
  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.
81
+ strategy_instance (base_strategy.Strategy): Strategy instance to assign
82
+ symbols to.
83
+ symbols (list[str]): List of symbols to assign.
130
84
  """
131
- # VALIDATE THAT INSTANCE IS A SUBCLASS OF base_strategy.Strategy
132
85
  # ------------------------------------------------------------------------------
86
+ # VALIDATE THAT INSTANCE IS A SUBCLASS OF base_strategy.Strategy
133
87
  if not isinstance(strategy_instance, base_strategy.Strategy):
134
- console.logger.error("add_strategy: strategy must inherit from Strategy")
88
+ console.logger.error("assign_symbols: strategy must inherit from Strategy")
135
89
  return False
136
90
 
137
- # ADD STRATEGY INSTANCE TO REGISTRY IF NOT ALREADY REGISTERED
138
91
  # ------------------------------------------------------------------------------
92
+ # CHECK FOR CONFLICTS
93
+ non_conflicting: list[str] = []
94
+ conflicting: list[str] = []
139
95
  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
- )
96
+ for symbol in symbols:
97
+ owner = self.symbols_to_strategy.get(symbol)
98
+ if owner is None:
99
+ non_conflicting.append(symbol)
170
100
  else:
171
- self.assign_symbols(strategy_instance, symbols_list)
172
- return True
101
+ conflicting.append(symbol)
102
+ if conflicting:
103
+ console.logger.warning(
104
+ "assign_symbols: symbols not assigned due to conflicts; "
105
+ "use Portfolio.assign_symbols(...) after resolving. "
106
+ f"non_conflicting={non_conflicting}, conflicts={conflicting}"
107
+ )
108
+ return False
109
+ else:
110
+ # --------------------------------------------------------------------------
111
+ # ASSIGN SYMBOLS TO REGISTRY
112
+ for symbol in symbols:
113
+ self.symbols_to_strategy[symbol] = strategy_instance
114
+
115
+ # --------------------------------------------------------------------------
116
+ # PUBLISH SYMBOL ASSIGNMENT EVENT
117
+ # noinspection PyArgumentList
118
+ self.event_bus.publish(
119
+ events.Strategy.SymbolAssignment(
120
+ strategy=strategy_instance,
121
+ symbol_list=symbols,
122
+ )
123
+ )
124
+ return True
173
125
 
174
- def remove_strategy(
126
+ def unassign_symbols(
175
127
  self,
176
- strategy: base_strategy.Strategy,
177
- shutdown_mode: StrategyShutdownMode = StrategyShutdownMode.SOFT,
128
+ symbols: list[str],
129
+ shutdown_mode: models.SymbolShutdownMode = models.SymbolShutdownMode.SOFT,
178
130
  ) -> bool:
179
131
  """
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).
132
+ Unassign a list of symbols from their owning strategy if all of them have
133
+ previously been assigned to a strategy.
134
+ Calling this methods will request the owning strategy to stop trading the symbol
135
+ in the manner dictated via the `shutdown_mode` argument (default to soft
136
+ shutdown, i.e. wait for open positions to close naturally and release symbols
137
+ once they are flat).
138
+ After the owning strategy has released the symbol, the symbol is unassigned from
139
+ the portfolio via the `on_symbol_release` event handler.
183
140
 
184
141
  Args:
185
- strategy (base_strategy.Strategy): Strategy instance to remove.
186
- shutdown_mode (StrategyShutdownMode): Shutdown mode to use. Defaults to
187
- StrategyShutdownMode.SOFT.
142
+ symbols (list[str]): List of symbols to unassign.
143
+ shutdown_mode (models.SymbolShutdownMode): Shutdown mode to use. Defaults
144
+ to `models.SymbolShutdownMode.SOFT`.
188
145
  """
189
-
190
- # IF STRATEGY IS REGISTERED, MARK IT FOR REMOVAL
191
146
  # ------------------------------------------------------------------------------
147
+ # CHECK THAT SYMBOLS ARE REGISTERED
148
+ conflicting: list[str] = []
192
149
  with self._lock:
193
- if strategy not in self._strategies:
194
- console.logger.warning("remove_strategy: strategy not registered")
150
+ for symbol in symbols:
151
+ if symbol not in self.symbols_to_strategy:
152
+ conflicting.append(symbol)
153
+ if conflicting:
154
+ console.logger.warning(
155
+ "unassign_symbols: symbols not unassigned due to conflicts; "
156
+ f"conflicts={conflicting}. "
157
+ f"Use Portfolio.unassign_symbols(...) after resolving."
158
+ )
195
159
  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"
160
+ else:
161
+ # ----------------------------------------------------------------------
162
+ # PUBLISH STOP TRADING SYMBOL EVENT FOR EACH SYMBOL
163
+ for symbol in symbols:
164
+ # noinspection PyArgumentList
165
+ self.event_bus.publish(
166
+ events.Strategy.StopTradingSymbol(
167
+ strategy=self.symbols_to_strategy[symbol],
168
+ symbol=symbol,
169
+ shutdown_mode=shutdown_mode,
218
170
  )
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)
171
+ )
326
172
  console.logger.info(
327
- f"Strategy {getattr(strategy, 'name', type(strategy).__name__)} removed: all symbols released and flat"
173
+ f"unassign_symbols: trading stop for {symbol} trading strategy "
174
+ f"{self.symbols_to_strategy[symbol]} requested with shutdown"
175
+ f"mode {shutdown_mode.name}"
328
176
  )
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
- )
177
+ return True
@@ -86,13 +86,19 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
86
86
  style D1 fill:#6F42C1,fill-opacity:0.3
87
87
 
88
88
  E1[events.Strategy.SymbolRelease]
89
- E2[events.Strategy.StopTrading]
89
+ E2[events.Strategy.SymbolAssignment]
90
+ E3[events.Strategy.StopTrading]
91
+ E4[events.Strategy.StopTradingSymbol]
90
92
 
91
93
  R5 --> E1
92
94
  R5 --> E2
95
+ R5 --> E3
96
+ R5 --> E4
93
97
 
94
98
  style E1 fill:#6F42C1,fill-opacity:0.3
95
99
  style E2 fill:#6F42C1,fill-opacity:0.3
100
+ style E3 fill:#6F42C1,fill-opacity:0.3
101
+ style E4 fill:#6F42C1,fill-opacity:0.3
96
102
 
97
103
  subgraph Market ["Market Update Event Messages"]
98
104
  R1
@@ -159,10 +165,14 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
159
165
  R5
160
166
  E1
161
167
  E2
168
+ E3
169
+ E4
162
170
 
163
171
  subgraph StrategyNamespace ["events.Strategy Namespace"]
164
172
  E1
165
173
  E2
174
+ E3
175
+ E4
166
176
  end
167
177
 
168
178
  end
@@ -237,7 +247,6 @@ class Base:
237
247
 
238
248
  def __post_init__(self) -> None:
239
249
  super().__post_init__()
240
- _Validate.symbol(self.symbol, "Market event")
241
250
 
242
251
  @dataclasses.dataclass(kw_only=True, frozen=True)
243
252
  class Request(Event):
@@ -296,7 +305,6 @@ class Base:
296
305
 
297
306
  def __post_init__(self) -> None:
298
307
  super().__post_init__()
299
- _Validate.symbol(self.symbol, f"Order {self.order_id}")
300
308
 
301
309
  _Validate.timezone_aware(
302
310
  self.order_expiration, "order_expiration", f"Order {self.order_id}"
@@ -578,7 +586,6 @@ class Request:
578
586
 
579
587
  def __post_init__(self) -> None:
580
588
  super().__post_init__()
581
- _Validate.symbol(self.symbol, "Flush request")
582
589
 
583
590
  @dataclasses.dataclass(kw_only=True, frozen=True)
584
591
  class FlushAll(Base.Request):
@@ -776,7 +783,27 @@ class Strategy:
776
783
 
777
784
  def __post_init__(self) -> None:
778
785
  super().__post_init__()
779
- _Validate.symbol(self.symbol, "Strategy.SymbolRelease")
786
+
787
+ @dataclasses.dataclass(kw_only=True, frozen=True)
788
+ class SymbolAssignment(Base.Strategy):
789
+ """
790
+ Event message to indicate that a symbol should be assigned to a strategy.
791
+
792
+ Attributes:
793
+ symbol_list (list[str]): List of symbols to be assigned.
794
+
795
+ Examples:
796
+ >>> from onesecondtrader.messaging import events
797
+ >>> event = events.Strategy.SymbolAssignment(
798
+ ... strategy=my_strategy,
799
+ ... symbol=["AAPL"],
800
+ ... )
801
+ """
802
+
803
+ symbol_list: list[str]
804
+
805
+ def __post_init__(self) -> None:
806
+ super().__post_init__()
780
807
 
781
808
  @dataclasses.dataclass(kw_only=True, frozen=True)
782
809
  class StopTrading(Base.Strategy):
@@ -785,6 +812,7 @@ class Strategy:
785
812
 
786
813
  Attributes:
787
814
  shutdown_mode (models.StrategyShutdownMode): Shutdown mode to use.
815
+ Defaults to `SOFT`.
788
816
 
789
817
  Examples:
790
818
  >>> from onesecondtrader.messaging import events
@@ -796,6 +824,32 @@ class Strategy:
796
824
 
797
825
  shutdown_mode: models.StrategyShutdownMode
798
826
 
827
+ @dataclasses.dataclass(kw_only=True, frozen=True)
828
+ class StopTradingSymbol(Base.Strategy):
829
+ """
830
+ Event to indicate a strategy should stop trading a symbol.
831
+
832
+ Attributes:
833
+ symbol (str): Symbol to stop trading.
834
+ shutdown_mode (models.SymbolShutdownMode): Shutdown mode to use.
835
+ Defaults to `SOFT`.
836
+
837
+ Examples:
838
+ >>> from onesecondtrader.messaging import events
839
+ >>> event = events.Strategy.StopTradingSymbol(
840
+ ... strategy=my_strategy,
841
+ ... symbol="AAPL",
842
+ ... shutdown_mode=models.SymbolShutdownMode.HARD,
843
+ ... )
844
+ """
845
+
846
+ symbol: str
847
+ shutdown_mode: models.SymbolShutdownMode = models.SymbolShutdownMode.SOFT
848
+
849
+ def __post_init__(self) -> None:
850
+ super().__post_init__()
851
+ _Validate.symbol(self.symbol, f"StopTradingSymbol {self.symbol}")
852
+
799
853
 
800
854
  class _Validate:
801
855
  """Internal validation utilities for events."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.14.1
3
+ Version: 0.15.0
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
@@ -3,8 +3,8 @@ onesecondtrader/brokers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
3
3
  onesecondtrader/brokers/base_broker.py,sha256=PtLyFEXY5VisnFqJabOkRGEsSS05SUSTc7JIAzk-OA8,2948
4
4
  onesecondtrader/brokers/simulated_broker.py,sha256=ptbDkGG7NDKpqPn5ZkthALI2p533J9twS9hDQCaMeOY,242
5
5
  onesecondtrader/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- onesecondtrader/core/models.py,sha256=fPI9gpgAhd2JREoo77jwf2x-QZTrSLg8_SWYKLSqwGQ,4721
7
- onesecondtrader/core/portfolio.py,sha256=pysRGNZmjUf2kholg-2s1yW85GcT5dtj4vUW-bLMM9s,14851
6
+ onesecondtrader/core/models.py,sha256=zpjPMDVfPpWJv9NtLdK3IWXs11B_uEluGbX7u1PTKRw,5140
7
+ onesecondtrader/core/portfolio.py,sha256=gJvDKBm3J59SOybJpe77Ie3XW5eLndeArrP00wWefPc,8000
8
8
  onesecondtrader/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  onesecondtrader/datafeeds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  onesecondtrader/datafeeds/base_datafeed.py,sha256=WViw7tzsVoZku-V-DxbqKSjNPkvaiA8G-J5Rs9eKKn8,1299
@@ -14,14 +14,14 @@ onesecondtrader/indicators/base_indicator.py,sha256=eGv5_WYOSsuLXX8MbnyE3_Y8owH-
14
14
  onesecondtrader/indicators/moving_averages.py,sha256=ddZy640Z2aVgeiZ4SFRWsHDFaOBCW7u3mqBmc1wZrmQ,4678
15
15
  onesecondtrader/messaging/__init__.py,sha256=8LMFnw7KsnctDxyC8ZybDHgcdMB8fSy56Fad9Ozj6Bw,243
16
16
  onesecondtrader/messaging/eventbus.py,sha256=sEp5ebYNRHiqTRXaTqytZ2PV2wKDXj5NlWNi1OKn2_4,19447
17
- onesecondtrader/messaging/events.py,sha256=duC1nFPwJubT0t8DDSorIPPQeib7UFQTCMRvz98QeY0,26500
17
+ onesecondtrader/messaging/events.py,sha256=Aawuu77WG9h9dXGATQxEwiGwrFXEyG8dys324xRO7JA,28104
18
18
  onesecondtrader/monitoring/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  onesecondtrader/monitoring/console.py,sha256=1mrojXkyL4ro7ebkvDMGNQiCL-93WEylRuwnfmEKzVs,299
20
20
  onesecondtrader/monitoring/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  onesecondtrader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  onesecondtrader/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  onesecondtrader/strategies/base_strategy.py,sha256=chmJyX8jVe-H24zmFDKeqClrGv-EJFtBlKzLcQe5mmM,1650
24
- onesecondtrader-0.14.1.dist-info/METADATA,sha256=MscQM1dnnZToJJ-tp8ZDyFGQpUsEpuhN4L1FJolfLzY,9638
25
- onesecondtrader-0.14.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
26
- onesecondtrader-0.14.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
27
- onesecondtrader-0.14.1.dist-info/RECORD,,
24
+ onesecondtrader-0.15.0.dist-info/METADATA,sha256=ZGwVcpzLfs6zRdGukXh_4m-xE4O5b4VwO-p0L4yC4Dg,9638
25
+ onesecondtrader-0.15.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
26
+ onesecondtrader-0.15.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
27
+ onesecondtrader-0.15.0.dist-info/RECORD,,