onesecondtrader 0.14.1__py3-none-any.whl → 0.14.2__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.
@@ -47,8 +47,9 @@ class Portfolio:
47
47
  self._symbol_owner (dict[str, base_strategy.Strategy]): Exclusive symbol
48
48
  ownership map; each symbol is owned by at most one strategy instance at
49
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.
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.
52
53
  self.broker (base_broker.BaseBroker | None): Instantiated broker; may be
53
54
  disconnected if connect failed.
54
55
  """
@@ -69,11 +70,12 @@ class Portfolio:
69
70
  # KEEP TRACK OF STRATEGIES AND SYMBOL OWNERSHIP
70
71
  # ------------------------------------------------------------------------------
71
72
  self._strategies: set[base_strategy.Strategy] = set()
73
+ self.strategy_removal_pending: set[base_strategy.Strategy] = set()
72
74
  self._symbol_owner: dict[str, base_strategy.Strategy] = {}
73
- self._removal_pending: set[base_strategy.Strategy] = set()
74
75
 
75
76
  # INITIALIZE BROKER
76
77
  # ------------------------------------------------------------------------------
78
+ # TODO Decouple by also doing this via an event
77
79
  self.broker: base_broker.BaseBroker | None = None
78
80
  if broker_class is None or not issubclass(broker_class, base_broker.BaseBroker):
79
81
  broker_name = (
@@ -167,13 +169,23 @@ class Portfolio:
167
169
  "use Portfolio.assign_symbols(...) after resolving. "
168
170
  f"non_conflicting={non_conflicting}, conflicts={conflicting}"
169
171
  )
172
+ return False
170
173
  else:
171
- self.assign_symbols(strategy_instance, symbols_list)
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
+
172
184
  return True
173
185
 
174
186
  def remove_strategy(
175
187
  self,
176
- strategy: base_strategy.Strategy,
188
+ strategy_instance: base_strategy.Strategy,
177
189
  shutdown_mode: StrategyShutdownMode = StrategyShutdownMode.SOFT,
178
190
  ) -> bool:
179
191
  """
@@ -182,93 +194,90 @@ class Portfolio:
182
194
  for open positions to close naturally and release symbols once they are flat).
183
195
 
184
196
  Args:
185
- strategy (base_strategy.Strategy): Strategy instance to remove.
197
+ strategy_instance (base_strategy.Strategy): Strategy instance to remove.
186
198
  shutdown_mode (StrategyShutdownMode): Shutdown mode to use. Defaults to
187
199
  StrategyShutdownMode.SOFT.
188
200
  """
189
201
 
190
- # IF STRATEGY IS REGISTERED, MARK IT FOR REMOVAL
202
+ # IF STRATEGY IS REGISTERED, MARK IT FOR REMOVAL AND PUBLISH STOP TRADING EVENT
191
203
  # ------------------------------------------------------------------------------
192
204
  with self._lock:
193
- if strategy not in self._strategies:
205
+ if strategy_instance not in self._strategies:
194
206
  console.logger.warning("remove_strategy: strategy not registered")
195
207
  return False
196
- self._removal_pending.add(strategy)
208
+ self.strategy_removal_pending.add(strategy_instance)
197
209
 
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"
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,
223
216
  )
224
- return False
217
+ )
218
+ return True
225
219
 
226
220
  def assign_symbols(
227
- self,
228
- strategy: base_strategy.Strategy,
229
- symbols: Iterable[str],
230
- ) -> tuple[list[str], list[str]]:
221
+ self, strategy_instance: base_strategy.Strategy, symbols: Iterable[str]
222
+ ):
231
223
  """
232
- Assign symbols to a strategy with exclusivity enforcement.
224
+ Assign a list of symbols to a strategy.
233
225
 
234
- Returns:
235
- tuple[list[str], list[str]]: (accepted, conflicts)
226
+ Args:
227
+ strategy_instance (base_strategy.Strategy): Strategy instance to assign
228
+ symbols to.
229
+ symbols (Iterable[str]): List of symbols to assign.
236
230
  """
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] = []
231
+ # IF STRATEGY IS REGISTERED, MARK IT FOR REMOVAL AND PUBLISH STOP TRADING EVENT
232
+ # ------------------------------------------------------------------------------
247
233
  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}"
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())
260
247
  )
261
- return accepted, conflicts
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
262
277
 
263
278
  def unassign_symbols(
264
279
  self, strategy: base_strategy.Strategy, symbols: Iterable[str]
265
280
  ) -> list[str]:
266
- """
267
- Release symbol ownership from a strategy.
268
-
269
- Returns:
270
- list[str]: Symbols actually unassigned.
271
- """
272
281
  if not isinstance(strategy, base_strategy.Strategy):
273
282
  console.logger.error(
274
283
  "unassign_symbols: strategy must inherit from Strategy"
@@ -296,23 +305,38 @@ class Portfolio:
296
305
  with self._lock:
297
306
  return self._symbol_owner.get(symbol)
298
307
 
299
- def release_symbols_from_strategy(
300
- self, strategy: base_strategy.Strategy, symbols: Iterable[str]
301
- ) -> list[str]:
308
+ def on_symbol_release(self, event: events.Base.Event) -> None:
302
309
  """
303
- Release symbols from the given strategy.
310
+ Handle symbol release events. Ignores unrelated event types.
304
311
 
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.
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.
308
315
  """
309
- removed = self.unassign_symbols(strategy, symbols)
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])
310
331
  if not removed:
311
- return removed
312
- # Auto-deregister if pending removal and no more owned symbols
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
313
337
  pending = False
314
338
  with self._lock:
315
- pending = strategy in self._removal_pending
339
+ pending = strategy in self.strategy_removal_pending
316
340
  has_owned_left = any(
317
341
  owner is strategy for owner in self._symbol_owner.values()
318
342
  )
@@ -322,27 +346,14 @@ class Portfolio:
322
346
  with self._lock:
323
347
  if strategy in self._strategies:
324
348
  self._strategies.remove(strategy)
325
- self._removal_pending.discard(strategy)
349
+ self.strategy_removal_pending.discard(strategy)
326
350
  console.logger.info(
327
- f"Strategy {getattr(strategy, 'name', type(strategy).__name__)} removed: all symbols released and flat"
351
+ f"Strategy {getattr(strategy, 'name', type(strategy).__name__)} "
352
+ f"removed: all symbols released and flat"
328
353
  )
329
354
  except Exception:
330
355
  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
- )
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
+ )
@@ -86,13 +86,16 @@ 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]
90
91
 
91
92
  R5 --> E1
92
93
  R5 --> E2
94
+ R5 --> E3
93
95
 
94
96
  style E1 fill:#6F42C1,fill-opacity:0.3
95
97
  style E2 fill:#6F42C1,fill-opacity:0.3
98
+ style E3 fill:#6F42C1,fill-opacity:0.3
96
99
 
97
100
  subgraph Market ["Market Update Event Messages"]
98
101
  R1
@@ -159,10 +162,12 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
159
162
  R5
160
163
  E1
161
164
  E2
165
+ E3
162
166
 
163
167
  subgraph StrategyNamespace ["events.Strategy Namespace"]
164
168
  E1
165
169
  E2
170
+ E3
166
171
  end
167
172
 
168
173
  end
@@ -778,6 +783,29 @@ class Strategy:
778
783
  super().__post_init__()
779
784
  _Validate.symbol(self.symbol, "Strategy.SymbolRelease")
780
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
+
781
809
  @dataclasses.dataclass(kw_only=True, frozen=True)
782
810
  class StopTrading(Base.Strategy):
783
811
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.14.1
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
@@ -4,7 +4,7 @@ onesecondtrader/brokers/base_broker.py,sha256=PtLyFEXY5VisnFqJabOkRGEsSS05SUSTc7
4
4
  onesecondtrader/brokers/simulated_broker.py,sha256=ptbDkGG7NDKpqPn5ZkthALI2p533J9twS9hDQCaMeOY,242
5
5
  onesecondtrader/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  onesecondtrader/core/models.py,sha256=fPI9gpgAhd2JREoo77jwf2x-QZTrSLg8_SWYKLSqwGQ,4721
7
- onesecondtrader/core/portfolio.py,sha256=pysRGNZmjUf2kholg-2s1yW85GcT5dtj4vUW-bLMM9s,14851
7
+ onesecondtrader/core/portfolio.py,sha256=7MOASfqz6qykMSadu2cfYAzLTZrtOw5q6b04hk4tt-0,15950
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=ggP5wDTqeFxLijTukEoDEMnH4VZsVaYjHny-XXKsnls,27377
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.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,,