onesecondtrader 0.14.0__py3-none-any.whl → 0.14.1__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.
- onesecondtrader/brokers/__init__.py +0 -0
- onesecondtrader/brokers/base_broker.py +99 -0
- onesecondtrader/brokers/simulated_broker.py +10 -0
- onesecondtrader/core/portfolio.py +348 -0
- onesecondtrader/messaging/__init__.py +1 -1
- onesecondtrader/messaging/events.py +57 -17
- onesecondtrader/strategies/__init__.py +0 -0
- onesecondtrader/strategies/base_strategy.py +46 -0
- {onesecondtrader-0.14.0.dist-info → onesecondtrader-0.14.1.dist-info}/METADATA +1 -1
- {onesecondtrader-0.14.0.dist-info → onesecondtrader-0.14.1.dist-info}/RECORD +12 -6
- {onesecondtrader-0.14.0.dist-info → onesecondtrader-0.14.1.dist-info}/WHEEL +0 -0
- {onesecondtrader-0.14.0.dist-info → onesecondtrader-0.14.1.dist-info}/licenses/LICENSE +0 -0
|
File without changes
|
|
@@ -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,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the Portfolio class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from onesecondtrader import messaging
|
|
8
|
+
from onesecondtrader.messaging import events
|
|
9
|
+
from onesecondtrader.monitoring import console
|
|
10
|
+
from onesecondtrader.brokers import base_broker
|
|
11
|
+
from onesecondtrader.strategies import base_strategy
|
|
12
|
+
from onesecondtrader.core.models import StrategyShutdownMode
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Portfolio:
|
|
16
|
+
"""
|
|
17
|
+
The Portfolio class orchestrates the trading infrastructure's components.
|
|
18
|
+
It manages the broker connection, market data reception, and strategy execution.
|
|
19
|
+
|
|
20
|
+
Multiple instances of the same Strategy class can be registered concurrently.
|
|
21
|
+
Symbol ownership is exclusive and enforced by the portfolio: each symbol may be
|
|
22
|
+
owned by at most one strategy instance at a time. Use `add_strategy()` with a list
|
|
23
|
+
of symbols or `assign_symbols(...)` with a specific strategy and a list of symbols
|
|
24
|
+
to claim symbols; `owner_of(symbol)` returns the current owner.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
event_bus: messaging.EventBus | None = None,
|
|
30
|
+
broker_class: type[base_broker.BaseBroker] | None = None,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the Portfolio class, subscribe to events, and connect to the broker.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
event_bus (EventBus | None): Event bus to use; defaults to
|
|
37
|
+
system_event_bus when None.
|
|
38
|
+
broker_class (type[base_broker.BaseBroker] | None): Broker class to
|
|
39
|
+
instantiate and connect. Must be a subclass of BaseBroker.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
self.event_bus (eventbus.EventBus): Event bus used for communication between
|
|
43
|
+
the trading infrastructure's components.
|
|
44
|
+
self._lock (threading.Lock): Lock for thread-safe operations.
|
|
45
|
+
self._strategies (set[base_strategy.Strategy]): Registered strategy
|
|
46
|
+
instances.
|
|
47
|
+
self._symbol_owner (dict[str, base_strategy.Strategy]): Exclusive symbol
|
|
48
|
+
ownership map; each symbol is owned by at most one strategy instance at
|
|
49
|
+
a time.
|
|
50
|
+
self._removal_pending (set[base_strategy.Strategy]): Set of strategies that
|
|
51
|
+
are still active but marked for removal once all symbols are released.
|
|
52
|
+
self.broker (base_broker.BaseBroker | None): Instantiated broker; may be
|
|
53
|
+
disconnected if connect failed.
|
|
54
|
+
"""
|
|
55
|
+
# INITIALIZE EVENT BUS
|
|
56
|
+
# ------------------------------------------------------------------------------
|
|
57
|
+
self.event_bus: messaging.EventBus = (
|
|
58
|
+
event_bus if event_bus else messaging.system_event_bus
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# SUBSCRIBE HANDLER METHODS TO EVENTS VIA event_bus.subscribe
|
|
62
|
+
# ------------------------------------------------------------------------------
|
|
63
|
+
self.event_bus.subscribe(events.Strategy.SymbolRelease, self.on_symbol_release)
|
|
64
|
+
|
|
65
|
+
# INITIALIZE LOCK FOR THREAD-SAFE OPERATIONS WITHIN THE PORTFOLIO
|
|
66
|
+
# ------------------------------------------------------------------------------
|
|
67
|
+
self._lock = threading.Lock()
|
|
68
|
+
|
|
69
|
+
# KEEP TRACK OF STRATEGIES AND SYMBOL OWNERSHIP
|
|
70
|
+
# ------------------------------------------------------------------------------
|
|
71
|
+
self._strategies: set[base_strategy.Strategy] = set()
|
|
72
|
+
self._symbol_owner: dict[str, base_strategy.Strategy] = {}
|
|
73
|
+
self._removal_pending: set[base_strategy.Strategy] = set()
|
|
74
|
+
|
|
75
|
+
# INITIALIZE BROKER
|
|
76
|
+
# ------------------------------------------------------------------------------
|
|
77
|
+
self.broker: base_broker.BaseBroker | None = None
|
|
78
|
+
if broker_class is None or not issubclass(broker_class, base_broker.BaseBroker):
|
|
79
|
+
broker_name = (
|
|
80
|
+
getattr(broker_class, "__name__", str(broker_class))
|
|
81
|
+
if broker_class
|
|
82
|
+
else None
|
|
83
|
+
)
|
|
84
|
+
console.logger.error(
|
|
85
|
+
"Portfolio requires a valid broker_class (subclass of BaseBroker), "
|
|
86
|
+
f"got {broker_name}"
|
|
87
|
+
)
|
|
88
|
+
return
|
|
89
|
+
try:
|
|
90
|
+
self.broker = broker_class(self.event_bus)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
console.logger.error(
|
|
93
|
+
f"Failed to instantiate broker "
|
|
94
|
+
f"{getattr(broker_class, '__name__', str(broker_class))}: {e}"
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# CONNECT TO BROKER
|
|
99
|
+
# ------------------------------------------------------------------------------
|
|
100
|
+
try:
|
|
101
|
+
connected = self.broker.connect()
|
|
102
|
+
if not connected:
|
|
103
|
+
console.logger.error(
|
|
104
|
+
f"Failed to connect broker {type(self.broker).__name__}"
|
|
105
|
+
)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
console.logger.error(f"Broker connect failed: {e}")
|
|
108
|
+
|
|
109
|
+
def add_strategy(
|
|
110
|
+
self,
|
|
111
|
+
strategy_instance: base_strategy.Strategy,
|
|
112
|
+
symbols: Iterable[str] | None = None,
|
|
113
|
+
) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
Register a Strategy instance and optionally assign a list of symbols to it.
|
|
116
|
+
|
|
117
|
+
If symbols are provided, potential conflicts are checked first under a lock.
|
|
118
|
+
If any conflicts exist, no symbols are assigned; a warning is logged listing
|
|
119
|
+
both non_conflicting and conflicting symbols and instructions to use
|
|
120
|
+
assign_symbols(...) are provided.
|
|
121
|
+
If no conflicts exist, all provided symbols are claimed by the strategy.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
strategy_instance (base_strategy.Strategy): Strategy instance to register.
|
|
125
|
+
symbols (Iterable[str] | None): Optional list of symbols to assign to the
|
|
126
|
+
strategy.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
bool: True if the strategy was registered, False otherwise.
|
|
130
|
+
"""
|
|
131
|
+
# VALIDATE THAT INSTANCE IS A SUBCLASS OF base_strategy.Strategy
|
|
132
|
+
# ------------------------------------------------------------------------------
|
|
133
|
+
if not isinstance(strategy_instance, base_strategy.Strategy):
|
|
134
|
+
console.logger.error("add_strategy: strategy must inherit from Strategy")
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
# ADD STRATEGY INSTANCE TO REGISTRY IF NOT ALREADY REGISTERED
|
|
138
|
+
# ------------------------------------------------------------------------------
|
|
139
|
+
with self._lock:
|
|
140
|
+
if strategy_instance in self._strategies:
|
|
141
|
+
console.logger.warning("add_strategy: strategy already registered")
|
|
142
|
+
return False
|
|
143
|
+
self._strategies.add(strategy_instance)
|
|
144
|
+
|
|
145
|
+
# ASSIGN SYMBOLS IF PROVIDED AND NO CONFLICTS EXIST, ELSE LOG WARNING
|
|
146
|
+
# ------------------------------------------------------------------------------
|
|
147
|
+
if symbols is not None:
|
|
148
|
+
# Create an ordered list of unique, non-empty, trimmed symbols
|
|
149
|
+
symbols_list = list(
|
|
150
|
+
dict.fromkeys(s.strip() for s in symbols if s and s.strip())
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Check for conflicts, claim symbols for strategy if no conflicts arise
|
|
154
|
+
if symbols_list:
|
|
155
|
+
non_conflicting: list[str] = []
|
|
156
|
+
conflicting: list[str] = []
|
|
157
|
+
with self._lock:
|
|
158
|
+
for sym in symbols_list:
|
|
159
|
+
owner = self._symbol_owner.get(sym)
|
|
160
|
+
if owner is None or owner is strategy_instance:
|
|
161
|
+
non_conflicting.append(sym)
|
|
162
|
+
else:
|
|
163
|
+
conflicting.append(sym)
|
|
164
|
+
if conflicting:
|
|
165
|
+
console.logger.warning(
|
|
166
|
+
"add_strategy: symbols not assigned due to conflicts; "
|
|
167
|
+
"use Portfolio.assign_symbols(...) after resolving. "
|
|
168
|
+
f"non_conflicting={non_conflicting}, conflicts={conflicting}"
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
self.assign_symbols(strategy_instance, symbols_list)
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
def remove_strategy(
|
|
175
|
+
self,
|
|
176
|
+
strategy: base_strategy.Strategy,
|
|
177
|
+
shutdown_mode: StrategyShutdownMode = StrategyShutdownMode.SOFT,
|
|
178
|
+
) -> bool:
|
|
179
|
+
"""
|
|
180
|
+
Mark a strategy for removal and request it to close its positions in the manner
|
|
181
|
+
dictated via the `shutdown_mode` argument (default to soft shutdown, i.e. wait
|
|
182
|
+
for open positions to close naturally and release symbols once they are flat).
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
strategy (base_strategy.Strategy): Strategy instance to remove.
|
|
186
|
+
shutdown_mode (StrategyShutdownMode): Shutdown mode to use. Defaults to
|
|
187
|
+
StrategyShutdownMode.SOFT.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
# IF STRATEGY IS REGISTERED, MARK IT FOR REMOVAL
|
|
191
|
+
# ------------------------------------------------------------------------------
|
|
192
|
+
with self._lock:
|
|
193
|
+
if strategy not in self._strategies:
|
|
194
|
+
console.logger.warning("remove_strategy: strategy not registered")
|
|
195
|
+
return False
|
|
196
|
+
self._removal_pending.add(strategy)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
strategy.request_close(shutdown_mode)
|
|
200
|
+
except Exception:
|
|
201
|
+
console.logger.warning(
|
|
202
|
+
"remove_strategy: strategy does not support request_close; proceeding to flatness check"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
if bool(strategy.is_flat()):
|
|
207
|
+
# If the strategy is already flat and owns no symbols, deregister now
|
|
208
|
+
with self._lock:
|
|
209
|
+
has_owned_left = any(
|
|
210
|
+
owner is strategy for owner in self._symbol_owner.values()
|
|
211
|
+
)
|
|
212
|
+
if not has_owned_left:
|
|
213
|
+
if strategy in self._strategies:
|
|
214
|
+
self._strategies.remove(strategy)
|
|
215
|
+
self._removal_pending.discard(strategy)
|
|
216
|
+
console.logger.info(
|
|
217
|
+
f"Strategy {getattr(strategy, 'name', type(strategy).__name__)} removed: flat and no symbols owned"
|
|
218
|
+
)
|
|
219
|
+
return True
|
|
220
|
+
except Exception:
|
|
221
|
+
console.logger.warning(
|
|
222
|
+
"remove_strategy: strategy does not implement is_flat; will wait for symbol releases"
|
|
223
|
+
)
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
def assign_symbols(
|
|
227
|
+
self,
|
|
228
|
+
strategy: base_strategy.Strategy,
|
|
229
|
+
symbols: Iterable[str],
|
|
230
|
+
) -> tuple[list[str], list[str]]:
|
|
231
|
+
"""
|
|
232
|
+
Assign symbols to a strategy with exclusivity enforcement.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
tuple[list[str], list[str]]: (accepted, conflicts)
|
|
236
|
+
"""
|
|
237
|
+
if not isinstance(strategy, base_strategy.Strategy):
|
|
238
|
+
console.logger.error("assign_symbols: strategy must inherit from Strategy")
|
|
239
|
+
return [], list(symbols)
|
|
240
|
+
symbols_list = list(
|
|
241
|
+
dict.fromkeys(s.strip() for s in symbols if s and s.strip())
|
|
242
|
+
)
|
|
243
|
+
if not symbols_list:
|
|
244
|
+
return [], []
|
|
245
|
+
accepted: list[str] = []
|
|
246
|
+
conflicts: list[str] = []
|
|
247
|
+
with self._lock:
|
|
248
|
+
for sym in symbols_list:
|
|
249
|
+
current = self._symbol_owner.get(sym)
|
|
250
|
+
if current is None or current is strategy:
|
|
251
|
+
self._symbol_owner[sym] = strategy
|
|
252
|
+
accepted.append(sym)
|
|
253
|
+
else:
|
|
254
|
+
conflicts.append(sym)
|
|
255
|
+
if accepted:
|
|
256
|
+
strategy.add_symbols(accepted)
|
|
257
|
+
if conflicts:
|
|
258
|
+
console.logger.warning(
|
|
259
|
+
f"assign_symbols: conflicts for {len(conflicts)} symbol(s): {conflicts}"
|
|
260
|
+
)
|
|
261
|
+
return accepted, conflicts
|
|
262
|
+
|
|
263
|
+
def unassign_symbols(
|
|
264
|
+
self, strategy: base_strategy.Strategy, symbols: Iterable[str]
|
|
265
|
+
) -> list[str]:
|
|
266
|
+
"""
|
|
267
|
+
Release symbol ownership from a strategy.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
list[str]: Symbols actually unassigned.
|
|
271
|
+
"""
|
|
272
|
+
if not isinstance(strategy, base_strategy.Strategy):
|
|
273
|
+
console.logger.error(
|
|
274
|
+
"unassign_symbols: strategy must inherit from Strategy"
|
|
275
|
+
)
|
|
276
|
+
return []
|
|
277
|
+
symbols_list = list(
|
|
278
|
+
dict.fromkeys(s.strip() for s in symbols if s and s.strip())
|
|
279
|
+
)
|
|
280
|
+
if not symbols_list:
|
|
281
|
+
return []
|
|
282
|
+
removed: list[str] = []
|
|
283
|
+
with self._lock:
|
|
284
|
+
for sym in symbols_list:
|
|
285
|
+
if self._symbol_owner.get(sym) is strategy:
|
|
286
|
+
del self._symbol_owner[sym]
|
|
287
|
+
removed.append(sym)
|
|
288
|
+
if removed:
|
|
289
|
+
strategy.remove_symbols(removed)
|
|
290
|
+
return removed
|
|
291
|
+
|
|
292
|
+
def owner_of(self, symbol: str) -> base_strategy.Strategy | None:
|
|
293
|
+
"""
|
|
294
|
+
Return the owning strategy for a symbol or None if unowned.
|
|
295
|
+
"""
|
|
296
|
+
with self._lock:
|
|
297
|
+
return self._symbol_owner.get(symbol)
|
|
298
|
+
|
|
299
|
+
def release_symbols_from_strategy(
|
|
300
|
+
self, strategy: base_strategy.Strategy, symbols: Iterable[str]
|
|
301
|
+
) -> list[str]:
|
|
302
|
+
"""
|
|
303
|
+
Release symbols from the given strategy.
|
|
304
|
+
|
|
305
|
+
If the strategy was previously marked for removal and ends up with no owned
|
|
306
|
+
symbols after this call, and the strategy is flat, it will be automatically
|
|
307
|
+
deregistered.
|
|
308
|
+
"""
|
|
309
|
+
removed = self.unassign_symbols(strategy, symbols)
|
|
310
|
+
if not removed:
|
|
311
|
+
return removed
|
|
312
|
+
# Auto-deregister if pending removal and no more owned symbols
|
|
313
|
+
pending = False
|
|
314
|
+
with self._lock:
|
|
315
|
+
pending = strategy in self._removal_pending
|
|
316
|
+
has_owned_left = any(
|
|
317
|
+
owner is strategy for owner in self._symbol_owner.values()
|
|
318
|
+
)
|
|
319
|
+
if pending and not has_owned_left:
|
|
320
|
+
try:
|
|
321
|
+
if bool(strategy.is_flat()):
|
|
322
|
+
with self._lock:
|
|
323
|
+
if strategy in self._strategies:
|
|
324
|
+
self._strategies.remove(strategy)
|
|
325
|
+
self._removal_pending.discard(strategy)
|
|
326
|
+
console.logger.info(
|
|
327
|
+
f"Strategy {getattr(strategy, 'name', type(strategy).__name__)} removed: all symbols released and flat"
|
|
328
|
+
)
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
return removed
|
|
332
|
+
|
|
333
|
+
def on_symbol_release(self, event: events.Base.Event) -> None:
|
|
334
|
+
"""
|
|
335
|
+
Handle symbol release events. Ignores unrelated event types.
|
|
336
|
+
"""
|
|
337
|
+
if not isinstance(event, events.Strategy.SymbolRelease):
|
|
338
|
+
return
|
|
339
|
+
strategy = event.strategy
|
|
340
|
+
with self._lock:
|
|
341
|
+
if strategy not in self._strategies:
|
|
342
|
+
console.logger.warning("on_symbol_release: strategy not registered")
|
|
343
|
+
return
|
|
344
|
+
removed = self.release_symbols_from_strategy(strategy, [event.symbol])
|
|
345
|
+
if not removed:
|
|
346
|
+
console.logger.warning(
|
|
347
|
+
f"on_symbol_release: symbol {event.symbol} not owned by {getattr(event.strategy, 'name', type(event.strategy).__name__)}"
|
|
348
|
+
)
|
|
@@ -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
|
|
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.
|
|
26
|
+
R5[events.Base.Strategy]
|
|
27
27
|
|
|
28
28
|
R --> R1
|
|
29
29
|
R --> R2
|
|
@@ -85,11 +85,14 @@ 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.
|
|
88
|
+
E1[events.Strategy.SymbolRelease]
|
|
89
|
+
E2[events.Strategy.StopTrading]
|
|
89
90
|
|
|
90
91
|
R5 --> E1
|
|
92
|
+
R5 --> E2
|
|
91
93
|
|
|
92
94
|
style E1 fill:#6F42C1,fill-opacity:0.3
|
|
95
|
+
style E2 fill:#6F42C1,fill-opacity:0.3
|
|
93
96
|
|
|
94
97
|
subgraph Market ["Market Update Event Messages"]
|
|
95
98
|
R1
|
|
@@ -152,12 +155,14 @@ Dataclass field validation logic is grouped under the `_Validate` namespace.
|
|
|
152
155
|
|
|
153
156
|
end
|
|
154
157
|
|
|
155
|
-
subgraph
|
|
158
|
+
subgraph Strategy ["Strategy Coord. Event Messages"]
|
|
156
159
|
R5
|
|
157
160
|
E1
|
|
161
|
+
E2
|
|
158
162
|
|
|
159
|
-
subgraph
|
|
163
|
+
subgraph StrategyNamespace ["events.Strategy Namespace"]
|
|
160
164
|
E1
|
|
165
|
+
E2
|
|
161
166
|
end
|
|
162
167
|
|
|
163
168
|
end
|
|
@@ -170,6 +175,7 @@ import re
|
|
|
170
175
|
import uuid
|
|
171
176
|
from onesecondtrader.core import models
|
|
172
177
|
from onesecondtrader.monitoring import console
|
|
178
|
+
from onesecondtrader.strategies import base_strategy
|
|
173
179
|
|
|
174
180
|
|
|
175
181
|
class Base:
|
|
@@ -297,7 +303,7 @@ class Base:
|
|
|
297
303
|
)
|
|
298
304
|
_Validate.quantity(self.quantity, f"Order {self.order_id}")
|
|
299
305
|
|
|
300
|
-
if self.time_in_force
|
|
306
|
+
if self.time_in_force is models.TimeInForce.GTD:
|
|
301
307
|
if self.order_expiration is None:
|
|
302
308
|
console.logger.error(
|
|
303
309
|
f"Order {self.order_id}: GTD order missing expiration "
|
|
@@ -363,9 +369,9 @@ class Base:
|
|
|
363
369
|
return super().__new__(cls)
|
|
364
370
|
|
|
365
371
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
366
|
-
class
|
|
372
|
+
class Strategy(Event):
|
|
367
373
|
"""
|
|
368
|
-
Base event message dataclass for
|
|
374
|
+
Base event message dataclass for strategy coordination messages.
|
|
369
375
|
This dataclass cannot be instantiated directly.
|
|
370
376
|
|
|
371
377
|
Attributes:
|
|
@@ -376,14 +382,22 @@ class Base:
|
|
|
376
382
|
ts_event: pd.Timestamp = dataclasses.field(
|
|
377
383
|
default_factory=lambda: pd.Timestamp.now(tz="UTC")
|
|
378
384
|
)
|
|
385
|
+
strategy: base_strategy.Strategy
|
|
379
386
|
|
|
380
387
|
def __new__(cls, *args, **kwargs):
|
|
381
|
-
if cls is Base.
|
|
388
|
+
if cls is Base.Strategy:
|
|
382
389
|
console.logger.error(
|
|
383
390
|
f"Cannot instantiate abstract class '{cls.__name__}' directly"
|
|
384
391
|
)
|
|
385
392
|
return super().__new__(cls)
|
|
386
393
|
|
|
394
|
+
def __post_init__(self) -> None:
|
|
395
|
+
super().__post_init__()
|
|
396
|
+
if not isinstance(self.strategy, base_strategy.Strategy):
|
|
397
|
+
console.logger.error(
|
|
398
|
+
f"{type(self).__name__}: strategy must inherit from Strategy"
|
|
399
|
+
)
|
|
400
|
+
|
|
387
401
|
|
|
388
402
|
class Market:
|
|
389
403
|
"""
|
|
@@ -413,7 +427,7 @@ class Market:
|
|
|
413
427
|
... volume=10000,
|
|
414
428
|
... ),
|
|
415
429
|
... )
|
|
416
|
-
|
|
430
|
+
|
|
417
431
|
"""
|
|
418
432
|
|
|
419
433
|
bar: models.Bar
|
|
@@ -660,7 +674,7 @@ class Response:
|
|
|
660
674
|
|
|
661
675
|
gross_value = self.filled_at_price * self.quantity_filled
|
|
662
676
|
|
|
663
|
-
if self.side
|
|
677
|
+
if self.side is models.Side.BUY:
|
|
664
678
|
net_value = gross_value + self.commission_and_fees
|
|
665
679
|
else:
|
|
666
680
|
net_value = gross_value - self.commission_and_fees
|
|
@@ -738,23 +752,49 @@ class System:
|
|
|
738
752
|
pass
|
|
739
753
|
|
|
740
754
|
|
|
741
|
-
class
|
|
755
|
+
class Strategy:
|
|
742
756
|
"""
|
|
743
|
-
Namespace for
|
|
757
|
+
Namespace for strategy coordination event messages.
|
|
744
758
|
"""
|
|
745
759
|
|
|
746
760
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
747
|
-
class SymbolRelease(Base.
|
|
761
|
+
class SymbolRelease(Base.Strategy):
|
|
748
762
|
"""
|
|
749
763
|
Event to indicate a strategy releases ownership of a symbol.
|
|
764
|
+
|
|
765
|
+
Attributes:
|
|
766
|
+
symbol (str): Symbol released.
|
|
767
|
+
|
|
768
|
+
Examples:
|
|
769
|
+
>>> from onesecondtrader.messaging import events
|
|
770
|
+
>>> event = events.Strategy.SymbolRelease(
|
|
771
|
+
... symbol="AAPL",
|
|
772
|
+
... )
|
|
750
773
|
"""
|
|
751
774
|
|
|
752
775
|
symbol: str
|
|
753
|
-
strategy_name: str
|
|
754
776
|
|
|
755
777
|
def __post_init__(self) -> None:
|
|
756
778
|
super().__post_init__()
|
|
757
|
-
_Validate.symbol(self.symbol, "
|
|
779
|
+
_Validate.symbol(self.symbol, "Strategy.SymbolRelease")
|
|
780
|
+
|
|
781
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
782
|
+
class StopTrading(Base.Strategy):
|
|
783
|
+
"""
|
|
784
|
+
Event to indicate a strategy should stop trading.
|
|
785
|
+
|
|
786
|
+
Attributes:
|
|
787
|
+
shutdown_mode (models.StrategyShutdownMode): Shutdown mode to use.
|
|
788
|
+
|
|
789
|
+
Examples:
|
|
790
|
+
>>> from onesecondtrader.messaging import events
|
|
791
|
+
>>> event = events.Strategy.StopTrading(
|
|
792
|
+
... strategy=my_strategy,
|
|
793
|
+
... shutdown_mode=models.StrategyShutdownMode.SOFT,
|
|
794
|
+
... )
|
|
795
|
+
"""
|
|
796
|
+
|
|
797
|
+
shutdown_mode: models.StrategyShutdownMode
|
|
758
798
|
|
|
759
799
|
|
|
760
800
|
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)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onesecondtrader
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.1
|
|
4
4
|
Summary: The Trading Infrastructure Toolkit for Python. Research, simulate, and deploy algorithmic trading strategies — all in one place.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Nils P. Kujath
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
onesecondtrader/__init__.py,sha256=TNqlT20sH46-J7F6giBxwWYG1-wFZZt7toDbZeQK6KQ,210
|
|
2
|
+
onesecondtrader/brokers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
onesecondtrader/brokers/base_broker.py,sha256=PtLyFEXY5VisnFqJabOkRGEsSS05SUSTc7JIAzk-OA8,2948
|
|
4
|
+
onesecondtrader/brokers/simulated_broker.py,sha256=ptbDkGG7NDKpqPn5ZkthALI2p533J9twS9hDQCaMeOY,242
|
|
2
5
|
onesecondtrader/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
6
|
onesecondtrader/core/models.py,sha256=fPI9gpgAhd2JREoo77jwf2x-QZTrSLg8_SWYKLSqwGQ,4721
|
|
7
|
+
onesecondtrader/core/portfolio.py,sha256=pysRGNZmjUf2kholg-2s1yW85GcT5dtj4vUW-bLMM9s,14851
|
|
4
8
|
onesecondtrader/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
9
|
onesecondtrader/datafeeds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
10
|
onesecondtrader/datafeeds/base_datafeed.py,sha256=WViw7tzsVoZku-V-DxbqKSjNPkvaiA8G-J5Rs9eKKn8,1299
|
|
@@ -8,14 +12,16 @@ onesecondtrader/datafeeds/csv_datafeed.py,sha256=WMoZpoian_93CdAzo36hJoF15T0ywRA
|
|
|
8
12
|
onesecondtrader/indicators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
13
|
onesecondtrader/indicators/base_indicator.py,sha256=eGv5_WYOSsuLXX8MbnyE3_Y8owH-2bpUT_GczOXDHVE,4359
|
|
10
14
|
onesecondtrader/indicators/moving_averages.py,sha256=ddZy640Z2aVgeiZ4SFRWsHDFaOBCW7u3mqBmc1wZrmQ,4678
|
|
11
|
-
onesecondtrader/messaging/__init__.py,sha256=
|
|
15
|
+
onesecondtrader/messaging/__init__.py,sha256=8LMFnw7KsnctDxyC8ZybDHgcdMB8fSy56Fad9Ozj6Bw,243
|
|
12
16
|
onesecondtrader/messaging/eventbus.py,sha256=sEp5ebYNRHiqTRXaTqytZ2PV2wKDXj5NlWNi1OKn2_4,19447
|
|
13
|
-
onesecondtrader/messaging/events.py,sha256=
|
|
17
|
+
onesecondtrader/messaging/events.py,sha256=duC1nFPwJubT0t8DDSorIPPQeib7UFQTCMRvz98QeY0,26500
|
|
14
18
|
onesecondtrader/monitoring/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
19
|
onesecondtrader/monitoring/console.py,sha256=1mrojXkyL4ro7ebkvDMGNQiCL-93WEylRuwnfmEKzVs,299
|
|
16
20
|
onesecondtrader/monitoring/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
21
|
onesecondtrader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
onesecondtrader
|
|
19
|
-
onesecondtrader
|
|
20
|
-
onesecondtrader-0.14.
|
|
21
|
-
onesecondtrader-0.14.
|
|
22
|
+
onesecondtrader/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
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,,
|
|
File without changes
|
|
File without changes
|