onesecondtrader 0.14.1__tar.gz → 0.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/PKG-INFO +1 -1
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/pyproject.toml +1 -1
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/core/models.py +16 -0
- onesecondtrader-0.15.0/src/onesecondtrader/core/portfolio.py +177 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/messaging/events.py +59 -5
- onesecondtrader-0.14.1/src/onesecondtrader/core/portfolio.py +0 -348
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/LICENSE +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/README.md +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/__init__.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/brokers/__init__.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/brokers/base_broker.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/brokers/simulated_broker.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/core/__init__.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/core/py.typed +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/datafeeds/__init__.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/datafeeds/base_datafeed.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/datafeeds/csv_datafeed.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/indicators/__init__.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/indicators/base_indicator.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/indicators/moving_averages.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/messaging/__init__.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/messaging/eventbus.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/monitoring/__init__.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/monitoring/console.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/monitoring/py.typed +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/py.typed +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/strategies/__init__.py +0 -0
- {onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/strategies/base_strategy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onesecondtrader
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "onesecondtrader"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.15.0"
|
|
4
4
|
description = "The Trading Infrastructure Toolkit for Python. Research, simulate, and deploy algorithmic trading strategies — all in one place."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Nils P. Kujath",email = "63961429+NilsKujath@users.noreply.github.com"}
|
|
@@ -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()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
from onesecondtrader import messaging
|
|
4
|
+
from onesecondtrader.messaging import events
|
|
5
|
+
from onesecondtrader.strategies import base_strategy
|
|
6
|
+
from onesecondtrader.monitoring import console
|
|
7
|
+
from onesecondtrader.core import models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Portfolio:
|
|
11
|
+
def __init__(self, event_bus: messaging.EventBus | None = None):
|
|
12
|
+
"""
|
|
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.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
event_bus (messaging.EventBus | None): Event bus to use; defaults to
|
|
20
|
+
messaging.system_event_bus when None.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
self._lock (threading.Lock): Lock for thread-safe operations.
|
|
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.
|
|
28
|
+
"""
|
|
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
|
|
35
|
+
self.event_bus: messaging.EventBus = (
|
|
36
|
+
event_bus if event_bus else messaging.system_event_bus
|
|
37
|
+
)
|
|
38
|
+
self.event_bus.subscribe(events.Strategy.SymbolRelease, self.on_symbol_release)
|
|
39
|
+
|
|
40
|
+
# ------------------------------------------------------------------------------
|
|
41
|
+
# INITIALIZE SYMBOLS TO STRATEGY REGISTRY
|
|
42
|
+
self.symbols_to_strategy: dict[str, base_strategy.Strategy] = {}
|
|
43
|
+
|
|
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.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
event (messaging.events.Base.Event): Symbol release event.
|
|
51
|
+
"""
|
|
52
|
+
# ------------------------------------------------------------------------------
|
|
53
|
+
# IGNORE UNRELATED EVENT TYPES
|
|
54
|
+
if not isinstance(event, events.Strategy.SymbolRelease):
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------------------
|
|
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__)}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def assign_symbols(
|
|
74
|
+
self, strategy_instance: base_strategy.Strategy, symbols: list[str]
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""
|
|
77
|
+
Assign a list of symbols to a strategy if no conflicts exist and notify the
|
|
78
|
+
strategy of the assignment.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
strategy_instance (base_strategy.Strategy): Strategy instance to assign
|
|
82
|
+
symbols to.
|
|
83
|
+
symbols (list[str]): List of symbols to assign.
|
|
84
|
+
"""
|
|
85
|
+
# ------------------------------------------------------------------------------
|
|
86
|
+
# VALIDATE THAT INSTANCE IS A SUBCLASS OF base_strategy.Strategy
|
|
87
|
+
if not isinstance(strategy_instance, base_strategy.Strategy):
|
|
88
|
+
console.logger.error("assign_symbols: strategy must inherit from Strategy")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------------------
|
|
92
|
+
# CHECK FOR CONFLICTS
|
|
93
|
+
non_conflicting: list[str] = []
|
|
94
|
+
conflicting: list[str] = []
|
|
95
|
+
with self._lock:
|
|
96
|
+
for symbol in symbols:
|
|
97
|
+
owner = self.symbols_to_strategy.get(symbol)
|
|
98
|
+
if owner is None:
|
|
99
|
+
non_conflicting.append(symbol)
|
|
100
|
+
else:
|
|
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
|
|
125
|
+
|
|
126
|
+
def unassign_symbols(
|
|
127
|
+
self,
|
|
128
|
+
symbols: list[str],
|
|
129
|
+
shutdown_mode: models.SymbolShutdownMode = models.SymbolShutdownMode.SOFT,
|
|
130
|
+
) -> bool:
|
|
131
|
+
"""
|
|
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.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
symbols (list[str]): List of symbols to unassign.
|
|
143
|
+
shutdown_mode (models.SymbolShutdownMode): Shutdown mode to use. Defaults
|
|
144
|
+
to `models.SymbolShutdownMode.SOFT`.
|
|
145
|
+
"""
|
|
146
|
+
# ------------------------------------------------------------------------------
|
|
147
|
+
# CHECK THAT SYMBOLS ARE REGISTERED
|
|
148
|
+
conflicting: list[str] = []
|
|
149
|
+
with self._lock:
|
|
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
|
+
)
|
|
159
|
+
return False
|
|
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,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
console.logger.info(
|
|
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}"
|
|
176
|
+
)
|
|
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.
|
|
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
|
-
|
|
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,348 +0,0 @@
|
|
|
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
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/brokers/base_broker.py
RENAMED
|
File without changes
|
{onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/brokers/simulated_broker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/datafeeds/base_datafeed.py
RENAMED
|
File without changes
|
{onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/datafeeds/csv_datafeed.py
RENAMED
|
File without changes
|
{onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/indicators/__init__.py
RENAMED
|
File without changes
|
{onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/indicators/base_indicator.py
RENAMED
|
File without changes
|
{onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/indicators/moving_averages.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/monitoring/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/strategies/__init__.py
RENAMED
|
File without changes
|
{onesecondtrader-0.14.1 → onesecondtrader-0.15.0}/src/onesecondtrader/strategies/base_strategy.py
RENAMED
|
File without changes
|