onesecondtrader 0.55.0__py3-none-any.whl → 0.56.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.
- onesecondtrader/orchestrator/__init__.py +8 -0
- onesecondtrader/orchestrator/orchestrator.py +161 -0
- onesecondtrader/orchestrator/run_recorder.py +759 -0
- onesecondtrader/orchestrator/runs_schema.sql +500 -0
- {onesecondtrader-0.55.0.dist-info → onesecondtrader-0.56.0.dist-info}/METADATA +1 -1
- {onesecondtrader-0.55.0.dist-info → onesecondtrader-0.56.0.dist-info}/RECORD +8 -4
- {onesecondtrader-0.55.0.dist-info → onesecondtrader-0.56.0.dist-info}/WHEEL +0 -0
- {onesecondtrader-0.55.0.dist-info → onesecondtrader-0.56.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
import dotenv
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from onesecondtrader import messaging, models
|
|
9
|
+
from onesecondtrader.brokers.base import BrokerBase
|
|
10
|
+
from onesecondtrader.datafeeds.base import DatafeedBase
|
|
11
|
+
from onesecondtrader.strategies.base import StrategyBase
|
|
12
|
+
from .run_recorder import RunRecorder
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Orchestrator:
|
|
16
|
+
"""
|
|
17
|
+
Orchestrates the execution of a trading run.
|
|
18
|
+
|
|
19
|
+
The orchestrator instantiates strategies, broker, datafeed, and recorder components,
|
|
20
|
+
connects them via an event bus, and coordinates the run lifecycle from start to shutdown.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
db_path: str = "runs.db"
|
|
24
|
+
mode: str = "backtest"
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
strategies: list[type[StrategyBase]],
|
|
29
|
+
broker: type[BrokerBase],
|
|
30
|
+
datafeed: type[DatafeedBase],
|
|
31
|
+
) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Initialize the orchestrator with component classes.
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
strategies:
|
|
37
|
+
List of strategy classes to instantiate for the run.
|
|
38
|
+
broker:
|
|
39
|
+
Broker class to instantiate for order execution.
|
|
40
|
+
datafeed:
|
|
41
|
+
Datafeed class to instantiate for market data delivery.
|
|
42
|
+
"""
|
|
43
|
+
dotenv.load_dotenv()
|
|
44
|
+
self._strategy_classes = strategies
|
|
45
|
+
self._broker_class = broker
|
|
46
|
+
self._datafeed_class = datafeed
|
|
47
|
+
self._event_bus: messaging.EventBus | None = None
|
|
48
|
+
self._strategies: list[StrategyBase] = []
|
|
49
|
+
self._broker: BrokerBase | None = None
|
|
50
|
+
self._datafeed: DatafeedBase | None = None
|
|
51
|
+
self._recorder: RunRecorder | None = None
|
|
52
|
+
|
|
53
|
+
def run(self) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Execute the trading run.
|
|
56
|
+
|
|
57
|
+
Creates all components, connects them, subscribes to symbols, waits for
|
|
58
|
+
the datafeed to complete, and then shuts down all components.
|
|
59
|
+
"""
|
|
60
|
+
run_id = self._generate_run_id()
|
|
61
|
+
|
|
62
|
+
self._event_bus = messaging.EventBus()
|
|
63
|
+
|
|
64
|
+
self._recorder = self._create_recorder(run_id)
|
|
65
|
+
self._broker = self._broker_class(self._event_bus)
|
|
66
|
+
self._strategies = [s(self._event_bus) for s in self._strategy_classes]
|
|
67
|
+
self._datafeed = self._datafeed_class(self._event_bus)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
self._broker.connect()
|
|
71
|
+
self._datafeed.connect()
|
|
72
|
+
self._subscribe_symbols()
|
|
73
|
+
self._datafeed.wait_until_complete()
|
|
74
|
+
self._event_bus.wait_until_system_idle()
|
|
75
|
+
self._recorder.update_run_status(
|
|
76
|
+
"completed", pd.Timestamp.now(tz="UTC").value
|
|
77
|
+
)
|
|
78
|
+
except Exception:
|
|
79
|
+
if self._recorder:
|
|
80
|
+
self._recorder.update_run_status(
|
|
81
|
+
"failed", pd.Timestamp.now(tz="UTC").value
|
|
82
|
+
)
|
|
83
|
+
raise
|
|
84
|
+
finally:
|
|
85
|
+
self._shutdown()
|
|
86
|
+
|
|
87
|
+
def _generate_run_id(self) -> str:
|
|
88
|
+
"""
|
|
89
|
+
Generate a unique run identifier.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A string combining the current UTC timestamp and strategy names.
|
|
93
|
+
"""
|
|
94
|
+
timestamp = pd.Timestamp.now(tz="UTC").strftime("%Y-%m-%d_%H-%M-%S")
|
|
95
|
+
strategy_names = "_".join(s.name for s in self._strategy_classes)
|
|
96
|
+
return f"{timestamp}_{strategy_names}"
|
|
97
|
+
|
|
98
|
+
def _collect_symbols(self) -> list[str]:
|
|
99
|
+
"""
|
|
100
|
+
Collect all unique symbols from the strategy classes.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
A deduplicated list of symbols from all strategies.
|
|
104
|
+
"""
|
|
105
|
+
symbols = []
|
|
106
|
+
for strategy_class in self._strategy_classes:
|
|
107
|
+
symbols.extend(strategy_class.symbols)
|
|
108
|
+
return list(set(symbols))
|
|
109
|
+
|
|
110
|
+
def _create_recorder(self, run_id: str) -> RunRecorder:
|
|
111
|
+
"""
|
|
112
|
+
Create and return a RunRecorder instance for this run.
|
|
113
|
+
|
|
114
|
+
Parameters:
|
|
115
|
+
run_id:
|
|
116
|
+
Unique identifier for this run.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
A configured RunRecorder instance.
|
|
120
|
+
"""
|
|
121
|
+
assert self._event_bus is not None
|
|
122
|
+
config = {
|
|
123
|
+
"mode": self.mode,
|
|
124
|
+
"symbols": self._collect_symbols(),
|
|
125
|
+
"strategies": [s.name for s in self._strategy_classes],
|
|
126
|
+
}
|
|
127
|
+
return RunRecorder(
|
|
128
|
+
event_bus=self._event_bus,
|
|
129
|
+
db_path=pathlib.Path(self.db_path),
|
|
130
|
+
run_id=run_id,
|
|
131
|
+
name="_".join(s.name for s in self._strategy_classes),
|
|
132
|
+
config=config,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _subscribe_symbols(self) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Subscribe the datafeed to symbols for each strategy's bar period.
|
|
138
|
+
"""
|
|
139
|
+
assert self._datafeed is not None
|
|
140
|
+
subscriptions: dict[models.BarPeriod, list[str]] = {}
|
|
141
|
+
for strategy_class in self._strategy_classes:
|
|
142
|
+
bar_period_value = strategy_class.parameters["bar_period"].default
|
|
143
|
+
assert isinstance(bar_period_value, models.BarPeriod)
|
|
144
|
+
if bar_period_value not in subscriptions:
|
|
145
|
+
subscriptions[bar_period_value] = []
|
|
146
|
+
subscriptions[bar_period_value].extend(strategy_class.symbols)
|
|
147
|
+
for bar_period, symbols in subscriptions.items():
|
|
148
|
+
self._datafeed.subscribe(list(set(symbols)), bar_period)
|
|
149
|
+
|
|
150
|
+
def _shutdown(self) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Shut down all components in the correct order.
|
|
153
|
+
"""
|
|
154
|
+
if self._datafeed:
|
|
155
|
+
self._datafeed.disconnect()
|
|
156
|
+
if self._broker:
|
|
157
|
+
self._broker.disconnect()
|
|
158
|
+
for strategy in self._strategies:
|
|
159
|
+
strategy.shutdown()
|
|
160
|
+
if self._recorder:
|
|
161
|
+
self._recorder.shutdown()
|