onesecondtrader 0.14.2__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,359 +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.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.
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.
55
28
  """
56
- # INITIALIZE EVENT BUS
57
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
58
35
  self.event_bus: messaging.EventBus = (
59
36
  event_bus if event_bus else messaging.system_event_bus
60
37
  )
61
-
62
- # SUBSCRIBE HANDLER METHODS TO EVENTS VIA event_bus.subscribe
63
- # ------------------------------------------------------------------------------
64
38
  self.event_bus.subscribe(events.Strategy.SymbolRelease, self.on_symbol_release)
65
39
 
66
- # INITIALIZE LOCK FOR THREAD-SAFE OPERATIONS WITHIN THE PORTFOLIO
67
40
  # ------------------------------------------------------------------------------
68
- self._lock = threading.Lock()
41
+ # INITIALIZE SYMBOLS TO STRATEGY REGISTRY
42
+ self.symbols_to_strategy: dict[str, base_strategy.Strategy] = {}
69
43
 
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] = {}
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.
75
48
 
76
- # INITIALIZE BROKER
49
+ Args:
50
+ event (messaging.events.Base.Event): Symbol release event.
51
+ """
77
52
  # ------------------------------------------------------------------------------
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
- )
53
+ # IGNORE UNRELATED EVENT TYPES
54
+ if not isinstance(event, events.Strategy.SymbolRelease):
98
55
  return
99
56
 
100
- # CONNECT TO BROKER
101
57
  # ------------------------------------------------------------------------------
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__}"
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__)}"
107
71
  )
108
- except Exception as e:
109
- console.logger.error(f"Broker connect failed: {e}")
110
72
 
111
- def add_strategy(
112
- self,
113
- strategy_instance: base_strategy.Strategy,
114
- symbols: Iterable[str] | None = None,
73
+ def assign_symbols(
74
+ self, strategy_instance: base_strategy.Strategy, symbols: list[str]
115
75
  ) -> bool:
116
76
  """
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.
77
+ Assign a list of symbols to a strategy if no conflicts exist and notify the
78
+ strategy of the assignment.
124
79
 
125
80
  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.
81
+ strategy_instance (base_strategy.Strategy): Strategy instance to assign
82
+ symbols to.
83
+ symbols (list[str]): List of symbols to assign.
132
84
  """
133
- # VALIDATE THAT INSTANCE IS A SUBCLASS OF base_strategy.Strategy
134
85
  # ------------------------------------------------------------------------------
86
+ # VALIDATE THAT INSTANCE IS A SUBCLASS OF base_strategy.Strategy
135
87
  if not isinstance(strategy_instance, base_strategy.Strategy):
136
- console.logger.error("add_strategy: strategy must inherit from Strategy")
88
+ console.logger.error("assign_symbols: strategy must inherit from Strategy")
137
89
  return False
138
90
 
139
- # ADD STRATEGY INSTANCE TO REGISTRY IF NOT ALREADY REGISTERED
140
91
  # ------------------------------------------------------------------------------
92
+ # CHECK FOR CONFLICTS
93
+ non_conflicting: list[str] = []
94
+ conflicting: list[str] = []
141
95
  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
96
+ for symbol in symbols:
97
+ owner = self.symbols_to_strategy.get(symbol)
98
+ if owner is None:
99
+ non_conflicting.append(symbol)
173
100
  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
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
185
125
 
186
- def remove_strategy(
126
+ def unassign_symbols(
187
127
  self,
188
- strategy_instance: base_strategy.Strategy,
189
- shutdown_mode: StrategyShutdownMode = StrategyShutdownMode.SOFT,
128
+ symbols: list[str],
129
+ shutdown_mode: models.SymbolShutdownMode = models.SymbolShutdownMode.SOFT,
190
130
  ) -> bool:
191
131
  """
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.
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.
225
140
 
226
141
  Args:
227
- strategy_instance (base_strategy.Strategy): Strategy instance to assign
228
- symbols to.
229
- symbols (Iterable[str]): List of symbols to assign.
142
+ symbols (list[str]): List of symbols to unassign.
143
+ shutdown_mode (models.SymbolShutdownMode): Shutdown mode to use. Defaults
144
+ to `models.SymbolShutdownMode.SOFT`.
230
145
  """
231
- # IF STRATEGY IS REGISTERED, MARK IT FOR REMOVAL AND PUBLISH STOP TRADING EVENT
232
146
  # ------------------------------------------------------------------------------
147
+ # CHECK THAT SYMBOLS ARE REGISTERED
148
+ conflicting: list[str] = []
233
149
  with self._lock:
234
- if strategy_instance not in self._strategies:
235
- 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
+ )
236
159
  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
160
+ else:
161
+ # ----------------------------------------------------------------------
162
+ # PUBLISH STOP TRADING SYMBOL EVENT FOR EACH SYMBOL
163
+ for symbol in symbols:
164
+ # noinspection PyArgumentList
270
165
  self.event_bus.publish(
271
- events.Strategy.SymbolAssignment(
272
- strategy=strategy_instance,
273
- symbol_list=symbols_list,
166
+ events.Strategy.StopTradingSymbol(
167
+ strategy=self.symbols_to_strategy[symbol],
168
+ symbol=symbol,
169
+ shutdown_mode=shutdown_mode,
274
170
  )
275
171
  )
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
172
  console.logger.info(
351
- f"Strategy {getattr(strategy, 'name', type(strategy).__name__)} "
352
- f"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}"
353
176
  )
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
- )
177
+ return True
@@ -88,14 +88,17 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
88
88
  E1[events.Strategy.SymbolRelease]
89
89
  E2[events.Strategy.SymbolAssignment]
90
90
  E3[events.Strategy.StopTrading]
91
+ E4[events.Strategy.StopTradingSymbol]
91
92
 
92
93
  R5 --> E1
93
94
  R5 --> E2
94
95
  R5 --> E3
96
+ R5 --> E4
95
97
 
96
98
  style E1 fill:#6F42C1,fill-opacity:0.3
97
99
  style E2 fill:#6F42C1,fill-opacity:0.3
98
100
  style E3 fill:#6F42C1,fill-opacity:0.3
101
+ style E4 fill:#6F42C1,fill-opacity:0.3
99
102
 
100
103
  subgraph Market ["Market Update Event Messages"]
101
104
  R1
@@ -163,11 +166,13 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
163
166
  E1
164
167
  E2
165
168
  E3
169
+ E4
166
170
 
167
171
  subgraph StrategyNamespace ["events.Strategy Namespace"]
168
172
  E1
169
173
  E2
170
174
  E3
175
+ E4
171
176
  end
172
177
 
173
178
  end
@@ -242,7 +247,6 @@ class Base:
242
247
 
243
248
  def __post_init__(self) -> None:
244
249
  super().__post_init__()
245
- _Validate.symbol(self.symbol, "Market event")
246
250
 
247
251
  @dataclasses.dataclass(kw_only=True, frozen=True)
248
252
  class Request(Event):
@@ -301,7 +305,6 @@ class Base:
301
305
 
302
306
  def __post_init__(self) -> None:
303
307
  super().__post_init__()
304
- _Validate.symbol(self.symbol, f"Order {self.order_id}")
305
308
 
306
309
  _Validate.timezone_aware(
307
310
  self.order_expiration, "order_expiration", f"Order {self.order_id}"
@@ -583,7 +586,6 @@ class Request:
583
586
 
584
587
  def __post_init__(self) -> None:
585
588
  super().__post_init__()
586
- _Validate.symbol(self.symbol, "Flush request")
587
589
 
588
590
  @dataclasses.dataclass(kw_only=True, frozen=True)
589
591
  class FlushAll(Base.Request):
@@ -781,7 +783,6 @@ class Strategy:
781
783
 
782
784
  def __post_init__(self) -> None:
783
785
  super().__post_init__()
784
- _Validate.symbol(self.symbol, "Strategy.SymbolRelease")
785
786
 
786
787
  @dataclasses.dataclass(kw_only=True, frozen=True)
787
788
  class SymbolAssignment(Base.Strategy):
@@ -803,8 +804,6 @@ class Strategy:
803
804
 
804
805
  def __post_init__(self) -> None:
805
806
  super().__post_init__()
806
- for symbol in self.symbol_list:
807
- _Validate.symbol(symbol, "Strategy.SymbolAssignment")
808
807
 
809
808
  @dataclasses.dataclass(kw_only=True, frozen=True)
810
809
  class StopTrading(Base.Strategy):
@@ -813,6 +812,7 @@ class Strategy:
813
812
 
814
813
  Attributes:
815
814
  shutdown_mode (models.StrategyShutdownMode): Shutdown mode to use.
815
+ Defaults to `SOFT`.
816
816
 
817
817
  Examples:
818
818
  >>> from onesecondtrader.messaging import events
@@ -824,6 +824,32 @@ class Strategy:
824
824
 
825
825
  shutdown_mode: models.StrategyShutdownMode
826
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
+
827
853
 
828
854
  class _Validate:
829
855
  """Internal validation utilities for events."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.14.2
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=7MOASfqz6qykMSadu2cfYAzLTZrtOw5q6b04hk4tt-0,15950
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=ggP5wDTqeFxLijTukEoDEMnH4VZsVaYjHny-XXKsnls,27377
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.2.dist-info/METADATA,sha256=1Qvu0gEoIIrQSGGqWCvEzKEVdye_p-hgPbNyvsy7Hh4,9638
25
- onesecondtrader-0.14.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
26
- onesecondtrader-0.14.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
27
- onesecondtrader-0.14.2.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,,