onesecondtrader 0.53.0__tar.gz → 0.55.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.53.0 → onesecondtrader-0.55.0}/PKG-INFO +1 -1
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/pyproject.toml +1 -1
- onesecondtrader-0.55.0/src/onesecondtrader/datafeeds/__init__.py +11 -0
- onesecondtrader-0.55.0/src/onesecondtrader/datafeeds/base.py +90 -0
- onesecondtrader-0.55.0/src/onesecondtrader/datafeeds/simulated.py +209 -0
- onesecondtrader-0.55.0/src/onesecondtrader/events/requests/order_submission.py +39 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/__init__.py +2 -0
- onesecondtrader-0.55.0/src/onesecondtrader/models/action_types.py +34 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/secmaster/utils.py +33 -0
- onesecondtrader-0.55.0/src/onesecondtrader/strategies/__init__.py +11 -0
- onesecondtrader-0.55.0/src/onesecondtrader/strategies/base.py +539 -0
- onesecondtrader-0.55.0/src/onesecondtrader/strategies/examples.py +48 -0
- onesecondtrader-0.53.0/src/onesecondtrader/events/requests/order_submission.py +0 -35
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/LICENSE +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/README.md +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/brokers/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/brokers/base.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/brokers/simulated.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/base.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/market/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/market/bar_processed.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/market/bar_received.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/base.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/expirations.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/fills.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/requests/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/requests/base.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/requests/order_cancellation.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/requests/order_modification.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/base.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/cancellations.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/modifications.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/orders.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/base.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/market_fields.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/moving_averages.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/messaging/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/messaging/eventbus.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/messaging/subscriber.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/bar_fields.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/bar_period.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/order_types.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/rejection_reasons.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/trade_sides.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/secmaster/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/secmaster/schema_versions/__init__.py +0 -0
- {onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/secmaster/schema_versions/secmaster_schema_v1.sql +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onesecondtrader
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.55.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.55.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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
|
|
5
|
+
from onesecondtrader import events, messaging, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DatafeedBase(abc.ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for market data feed implementations.
|
|
11
|
+
|
|
12
|
+
A data feed is responsible for connecting to an external data source, managing symbol and bar-period subscriptions, and publishing market data events onto the system event bus.
|
|
13
|
+
|
|
14
|
+
Concrete subclasses implement the mechanics of connectivity, subscription handling, and lifecycle management for a specific data source.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Initialize the data feed with an event bus.
|
|
20
|
+
|
|
21
|
+
parameters:
|
|
22
|
+
event_bus:
|
|
23
|
+
Event bus used to publish market data events produced by this data feed.
|
|
24
|
+
"""
|
|
25
|
+
self._event_bus = event_bus
|
|
26
|
+
|
|
27
|
+
def _publish(self, event: events.EventBase) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Publish a market data event to the event bus.
|
|
30
|
+
|
|
31
|
+
This method is intended for use by subclasses to forward incoming data from the external source into the internal event-driven system.
|
|
32
|
+
|
|
33
|
+
parameters:
|
|
34
|
+
event:
|
|
35
|
+
Event instance to be published.
|
|
36
|
+
"""
|
|
37
|
+
self._event_bus.publish(event)
|
|
38
|
+
|
|
39
|
+
@abc.abstractmethod
|
|
40
|
+
def connect(self) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Establish a connection to the underlying data source.
|
|
43
|
+
|
|
44
|
+
Implementations should perform any required setup, authentication, or resource allocation needed before subscriptions can be registered.
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abc.abstractmethod
|
|
49
|
+
def disconnect(self) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Terminate the connection to the underlying data source.
|
|
52
|
+
|
|
53
|
+
Implementations should release resources and ensure that no further events are published after disconnection.
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abc.abstractmethod
|
|
58
|
+
def subscribe(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Subscribe to market data for one or more symbols at a given bar period.
|
|
61
|
+
|
|
62
|
+
parameters:
|
|
63
|
+
symbols:
|
|
64
|
+
Instrument symbols to subscribe to, interpreted according to the conventions of the underlying data source.
|
|
65
|
+
bar_period:
|
|
66
|
+
Bar aggregation period specifying the granularity of market data.
|
|
67
|
+
"""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@abc.abstractmethod
|
|
71
|
+
def unsubscribe(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Cancel existing subscriptions for one or more symbols at a given bar period.
|
|
74
|
+
|
|
75
|
+
parameters:
|
|
76
|
+
symbols:
|
|
77
|
+
Instrument symbols for which subscriptions should be removed.
|
|
78
|
+
bar_period:
|
|
79
|
+
Bar aggregation period associated with the subscriptions.
|
|
80
|
+
"""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def wait_until_complete(self) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Block until the data feed has completed all pending work.
|
|
86
|
+
|
|
87
|
+
This method may be overridden by subclasses that perform asynchronous ingestion or background processing.
|
|
88
|
+
The default implementation does nothing.
|
|
89
|
+
"""
|
|
90
|
+
pass
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import os
|
|
5
|
+
import sqlite3
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
from onesecondtrader import events, messaging, models
|
|
9
|
+
from onesecondtrader.datafeeds.base import DatafeedBase
|
|
10
|
+
|
|
11
|
+
_RTYPE_MAP = {
|
|
12
|
+
models.BarPeriod.SECOND: 32,
|
|
13
|
+
models.BarPeriod.MINUTE: 33,
|
|
14
|
+
models.BarPeriod.HOUR: 34,
|
|
15
|
+
models.BarPeriod.DAY: 35,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_RTYPE_TO_BAR_PERIOD = {v: k for k, v in _RTYPE_MAP.items()}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SimulatedDatafeed(DatafeedBase):
|
|
22
|
+
"""
|
|
23
|
+
Simulated market data feed backed by a secmaster SQLite database.
|
|
24
|
+
|
|
25
|
+
This datafeed replays historical OHLCV bars from a secmaster database, resolving symbols
|
|
26
|
+
via time-bounded symbology mappings. Bars are delivered in timestamp order, with all bars
|
|
27
|
+
sharing the same timestamp published before calling `wait_until_system_idle`.
|
|
28
|
+
|
|
29
|
+
Subclasses must set `publisher_name`, `dataset`, and `symbol_type` as class attributes to
|
|
30
|
+
scope the feed to a specific data source. The database must contain publishers with numeric
|
|
31
|
+
`source_instrument_id` values; symbol-only publishers (e.g., yfinance) are not supported.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
db_path: str = ""
|
|
35
|
+
publisher_name: str = ""
|
|
36
|
+
dataset: str = ""
|
|
37
|
+
symbol_type: str = ""
|
|
38
|
+
price_scale: float = 1e9
|
|
39
|
+
start_ts: int | None = None
|
|
40
|
+
end_ts: int | None = None
|
|
41
|
+
|
|
42
|
+
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Parameters:
|
|
45
|
+
event_bus:
|
|
46
|
+
Event bus used to publish bar events and synchronize with subscribers.
|
|
47
|
+
"""
|
|
48
|
+
super().__init__(event_bus)
|
|
49
|
+
self._db_path = self.db_path or os.environ.get(
|
|
50
|
+
"SECMASTER_DB_PATH", "secmaster.db"
|
|
51
|
+
)
|
|
52
|
+
if not self.publisher_name:
|
|
53
|
+
raise ValueError("publisher_name is required")
|
|
54
|
+
if not self.dataset:
|
|
55
|
+
raise ValueError("dataset is required")
|
|
56
|
+
if not self.symbol_type:
|
|
57
|
+
raise ValueError("symbol_type is required")
|
|
58
|
+
self._subscriptions: set[tuple[str, models.BarPeriod]] = set()
|
|
59
|
+
self._subscriptions_lock = threading.Lock()
|
|
60
|
+
self._connection: sqlite3.Connection | None = None
|
|
61
|
+
self._thread: threading.Thread | None = None
|
|
62
|
+
self._stop_event = threading.Event()
|
|
63
|
+
self._publisher_id: int | None = None
|
|
64
|
+
|
|
65
|
+
def connect(self) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Open a connection to the secmaster database and resolve the publisher.
|
|
68
|
+
|
|
69
|
+
If already connected, this method returns immediately.
|
|
70
|
+
"""
|
|
71
|
+
if self._connection:
|
|
72
|
+
return
|
|
73
|
+
self._connection = sqlite3.connect(self._db_path, check_same_thread=False)
|
|
74
|
+
self._connection.execute("PRAGMA foreign_keys = ON")
|
|
75
|
+
self._connection.execute("PRAGMA journal_mode = WAL")
|
|
76
|
+
row = self._connection.execute(
|
|
77
|
+
"SELECT publisher_id FROM publishers WHERE name = ? AND dataset = ?",
|
|
78
|
+
(self.publisher_name, self.dataset),
|
|
79
|
+
).fetchone()
|
|
80
|
+
if row is None:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Publisher not found: {self.publisher_name}/{self.dataset}"
|
|
83
|
+
)
|
|
84
|
+
self._publisher_id = row[0]
|
|
85
|
+
|
|
86
|
+
def disconnect(self) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Close the database connection and stop any active streaming.
|
|
89
|
+
|
|
90
|
+
If not connected, this method returns immediately.
|
|
91
|
+
"""
|
|
92
|
+
if not self._connection:
|
|
93
|
+
return
|
|
94
|
+
self._stop_event.set()
|
|
95
|
+
if self._thread and self._thread.is_alive():
|
|
96
|
+
self._thread.join()
|
|
97
|
+
self._connection.close()
|
|
98
|
+
self._connection = None
|
|
99
|
+
self._publisher_id = None
|
|
100
|
+
|
|
101
|
+
def subscribe(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Register symbols for bar delivery at the specified period.
|
|
104
|
+
|
|
105
|
+
Parameters:
|
|
106
|
+
symbols:
|
|
107
|
+
List of ticker symbols to subscribe.
|
|
108
|
+
bar_period:
|
|
109
|
+
Bar aggregation period for the subscription.
|
|
110
|
+
"""
|
|
111
|
+
with self._subscriptions_lock:
|
|
112
|
+
self._subscriptions.update((s, bar_period) for s in symbols)
|
|
113
|
+
|
|
114
|
+
def unsubscribe(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Remove symbols from bar delivery at the specified period.
|
|
117
|
+
|
|
118
|
+
Parameters:
|
|
119
|
+
symbols:
|
|
120
|
+
List of ticker symbols to unsubscribe.
|
|
121
|
+
bar_period:
|
|
122
|
+
Bar aggregation period for the subscription.
|
|
123
|
+
"""
|
|
124
|
+
with self._subscriptions_lock:
|
|
125
|
+
self._subscriptions.difference_update((s, bar_period) for s in symbols)
|
|
126
|
+
|
|
127
|
+
def wait_until_complete(self) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Stream all subscribed bars and block until delivery is complete.
|
|
130
|
+
|
|
131
|
+
Bars are published in timestamp order. After each timestamp batch, the method
|
|
132
|
+
waits for all event bus subscribers to become idle before proceeding.
|
|
133
|
+
"""
|
|
134
|
+
with self._subscriptions_lock:
|
|
135
|
+
has_subscriptions = bool(self._subscriptions)
|
|
136
|
+
if not has_subscriptions:
|
|
137
|
+
return
|
|
138
|
+
if self._thread is None or not self._thread.is_alive():
|
|
139
|
+
self._stop_event.clear()
|
|
140
|
+
self._thread = threading.Thread(
|
|
141
|
+
target=self._stream,
|
|
142
|
+
name=self.__class__.__name__,
|
|
143
|
+
daemon=False,
|
|
144
|
+
)
|
|
145
|
+
self._thread.start()
|
|
146
|
+
self._thread.join()
|
|
147
|
+
|
|
148
|
+
def _stream(self) -> None:
|
|
149
|
+
if not self._connection or self._publisher_id is None:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
with self._subscriptions_lock:
|
|
153
|
+
subscriptions = list(self._subscriptions)
|
|
154
|
+
if not subscriptions:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
symbols = list({symbol for symbol, _ in subscriptions})
|
|
158
|
+
rtypes = list({_RTYPE_MAP[bp] for _, bp in subscriptions})
|
|
159
|
+
subscription_set = {(symbol, _RTYPE_MAP[bp]) for symbol, bp in subscriptions}
|
|
160
|
+
|
|
161
|
+
params: list = [self._publisher_id, self.symbol_type]
|
|
162
|
+
params.extend(symbols)
|
|
163
|
+
params.extend(rtypes)
|
|
164
|
+
if self.start_ts is not None:
|
|
165
|
+
params.append(self.start_ts)
|
|
166
|
+
if self.end_ts is not None:
|
|
167
|
+
params.append(self.end_ts)
|
|
168
|
+
|
|
169
|
+
query = f"""
|
|
170
|
+
SELECT s.symbol, o.rtype, o.ts_event, o.open, o.high, o.low, o.close, o.volume
|
|
171
|
+
FROM ohlcv o
|
|
172
|
+
JOIN instruments i ON i.instrument_id = o.instrument_id
|
|
173
|
+
JOIN symbology s
|
|
174
|
+
ON s.publisher_ref = i.publisher_ref
|
|
175
|
+
AND s.source_instrument_id = i.source_instrument_id
|
|
176
|
+
AND date(o.ts_event / 1000000000, 'unixepoch') >= s.start_date
|
|
177
|
+
AND date(o.ts_event / 1000000000, 'unixepoch') <= s.end_date
|
|
178
|
+
WHERE i.publisher_ref = ?
|
|
179
|
+
AND s.symbol_type = ?
|
|
180
|
+
AND s.symbol IN ({",".join("?" * len(symbols))})
|
|
181
|
+
AND o.rtype IN ({",".join("?" * len(rtypes))})
|
|
182
|
+
{"AND o.ts_event >= ?" if self.start_ts is not None else ""}
|
|
183
|
+
{"AND o.ts_event <= ?" if self.end_ts is not None else ""}
|
|
184
|
+
ORDER BY o.ts_event, s.symbol
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
rows = self._connection.execute(query, params)
|
|
188
|
+
|
|
189
|
+
def to_bar(row):
|
|
190
|
+
symbol, rtype, ts_event, open_, high, low, close, volume = row
|
|
191
|
+
if (symbol, rtype) not in subscription_set:
|
|
192
|
+
return None
|
|
193
|
+
return events.market.BarReceived(
|
|
194
|
+
ts_event_ns=ts_event,
|
|
195
|
+
symbol=symbol,
|
|
196
|
+
bar_period=_RTYPE_TO_BAR_PERIOD[rtype],
|
|
197
|
+
open=open_ / self.price_scale,
|
|
198
|
+
high=high / self.price_scale,
|
|
199
|
+
low=low / self.price_scale,
|
|
200
|
+
close=close / self.price_scale,
|
|
201
|
+
volume=volume,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
for _, group in itertools.groupby(rows, key=lambda r: r[2]):
|
|
205
|
+
if self._stop_event.is_set():
|
|
206
|
+
return
|
|
207
|
+
for bar in filter(None, map(to_bar, group)):
|
|
208
|
+
self._publish(bar)
|
|
209
|
+
self._event_bus.wait_until_system_idle()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from onesecondtrader import models
|
|
7
|
+
from onesecondtrader.events.requests.base import RequestBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
11
|
+
class OrderSubmissionRequest(RequestBase):
|
|
12
|
+
"""
|
|
13
|
+
Event representing a request to submit a new order to a brokers.
|
|
14
|
+
|
|
15
|
+
The `system_order_id` is a unique identifier assigned by the system to the order submission request by default at object creation.
|
|
16
|
+
|
|
17
|
+
| Field | Type | Semantics |
|
|
18
|
+
|-------------------|----------------------------|----------------------------------------------------------------------------|
|
|
19
|
+
| `ts_event_ns` | `int` | Time at which the submission request was issued, as UTC epoch nanoseconds. |
|
|
20
|
+
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
21
|
+
| `system_order_id` | `uuid.UUID` | System-assigned unique identifier for the order submission. |
|
|
22
|
+
| `symbol` | `str` | Identifier of the traded instrument. |
|
|
23
|
+
| `order_type` | `models.OrderType` | Execution constraint of the order. |
|
|
24
|
+
| `side` | `models.TradeSide` | Direction of the trade. |
|
|
25
|
+
| `quantity` | `float` | Requested order quantity. |
|
|
26
|
+
| `limit_price` | `float` or `None` | Limit price, if applicable to the order type. |
|
|
27
|
+
| `stop_price` | `float` or `None` | Stop price, if applicable to the order type. |
|
|
28
|
+
| `action` | `models.ActionType` or `None` | Intent of the order from the strategy's perspective (e.g., entry, exit). |
|
|
29
|
+
| `signal` | `str` or `None` | Optional signal name or identifier that triggered this order. |
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
system_order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
33
|
+
order_type: models.OrderType
|
|
34
|
+
side: models.TradeSide
|
|
35
|
+
quantity: float
|
|
36
|
+
limit_price: float | None = None
|
|
37
|
+
stop_price: float | None = None
|
|
38
|
+
action: models.ActionType | None = None
|
|
39
|
+
signal: str | None = None
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Defines the fundamental domain concepts used throughout the trading system.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from .action_types import ActionType
|
|
5
6
|
from .bar_fields import BarField
|
|
6
7
|
from .bar_period import BarPeriod
|
|
7
8
|
from .order_types import OrderType
|
|
@@ -13,6 +14,7 @@ from .rejection_reasons import (
|
|
|
13
14
|
from .trade_sides import TradeSide
|
|
14
15
|
|
|
15
16
|
__all__ = [
|
|
17
|
+
"ActionType",
|
|
16
18
|
"BarField",
|
|
17
19
|
"BarPeriod",
|
|
18
20
|
"OrderType",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ActionType(enum.Enum):
|
|
7
|
+
"""
|
|
8
|
+
Enumeration of trading action types.
|
|
9
|
+
|
|
10
|
+
`ActionType` specifies the intent or purpose of an order from the strategy's perspective,
|
|
11
|
+
describing what the order is meant to accomplish in terms of position management.
|
|
12
|
+
|
|
13
|
+
| Value | Semantics |
|
|
14
|
+
|---------------|----------------------------------------------------------------------------|
|
|
15
|
+
| `ENTRY` | Opens a new position (direction-agnostic). |
|
|
16
|
+
| `ENTRY_LONG` | Opens a new long position. |
|
|
17
|
+
| `ENTRY_SHORT` | Opens a new short position. |
|
|
18
|
+
| `EXIT` | Closes an existing position (direction-agnostic). |
|
|
19
|
+
| `EXIT_LONG` | Closes an existing long position. |
|
|
20
|
+
| `EXIT_SHORT` | Closes an existing short position. |
|
|
21
|
+
| `ADD` | Increases the size of an existing position. |
|
|
22
|
+
| `REDUCE` | Decreases the size of an existing position without fully closing it. |
|
|
23
|
+
| `REVERSE` | Closes the current position and opens a new one in the opposite direction. |
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
ENTRY = enum.auto()
|
|
27
|
+
ENTRY_LONG = enum.auto()
|
|
28
|
+
ENTRY_SHORT = enum.auto()
|
|
29
|
+
EXIT = enum.auto()
|
|
30
|
+
EXIT_LONG = enum.auto()
|
|
31
|
+
EXIT_SHORT = enum.auto()
|
|
32
|
+
ADD = enum.auto()
|
|
33
|
+
REDUCE = enum.auto()
|
|
34
|
+
REVERSE = enum.auto()
|
|
@@ -559,6 +559,8 @@ def _ingest_symbology(
|
|
|
559
559
|
batch,
|
|
560
560
|
)
|
|
561
561
|
|
|
562
|
+
_validate_no_overlapping_symbology(con, publisher_id, symbol_type)
|
|
563
|
+
|
|
562
564
|
logger.info(
|
|
563
565
|
"Completed symbology ingest from %s (%d mappings)", json_path.name, count
|
|
564
566
|
)
|
|
@@ -566,6 +568,37 @@ def _ingest_symbology(
|
|
|
566
568
|
return count
|
|
567
569
|
|
|
568
570
|
|
|
571
|
+
def _validate_no_overlapping_symbology(
|
|
572
|
+
con: sqlite3.Connection,
|
|
573
|
+
publisher_id: int,
|
|
574
|
+
symbol_type: str,
|
|
575
|
+
) -> None:
|
|
576
|
+
query = """
|
|
577
|
+
WITH ordered AS (
|
|
578
|
+
SELECT
|
|
579
|
+
symbol,
|
|
580
|
+
start_date,
|
|
581
|
+
end_date,
|
|
582
|
+
LEAD(start_date) OVER (
|
|
583
|
+
PARTITION BY symbol ORDER BY start_date
|
|
584
|
+
) AS next_start
|
|
585
|
+
FROM symbology
|
|
586
|
+
WHERE publisher_ref = ? AND symbol_type = ?
|
|
587
|
+
)
|
|
588
|
+
SELECT symbol, start_date, end_date, next_start
|
|
589
|
+
FROM ordered
|
|
590
|
+
WHERE next_start IS NOT NULL AND end_date > next_start
|
|
591
|
+
LIMIT 1
|
|
592
|
+
"""
|
|
593
|
+
row = con.execute(query, (publisher_id, symbol_type)).fetchone()
|
|
594
|
+
if row:
|
|
595
|
+
symbol, start, end, next_start = row
|
|
596
|
+
raise ValueError(
|
|
597
|
+
f"Overlapping symbology detected for symbol={symbol!r}: "
|
|
598
|
+
f"segment [{start}, {end}] overlaps with next segment starting {next_start}"
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
569
602
|
def _enable_bulk_loading(con: sqlite3.Connection) -> None:
|
|
570
603
|
con.execute("PRAGMA journal_mode = WAL")
|
|
571
604
|
con.execute("PRAGMA synchronous = NORMAL")
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import dataclasses
|
|
5
|
+
import enum
|
|
6
|
+
import uuid
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from onesecondtrader import events, indicators, messaging, models
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass
|
|
15
|
+
class ParamSpec:
|
|
16
|
+
"""
|
|
17
|
+
Specification for a strategy parameter.
|
|
18
|
+
|
|
19
|
+
Defines the default value and optional constraints for a configurable strategy parameter.
|
|
20
|
+
Used to declare tunable parameters that can be overridden at strategy instantiation.
|
|
21
|
+
|
|
22
|
+
| Field | Type | Semantics |
|
|
23
|
+
|-----------|-------------------------------------------|--------------------------------------------------------------|
|
|
24
|
+
| `default` | `int`, `float`, `str`, `bool`, or `Enum` | Default value of the parameter. |
|
|
25
|
+
| `min` | `int`, `float`, or `None` | Minimum allowed value, if applicable. |
|
|
26
|
+
| `max` | `int`, `float`, or `None` | Maximum allowed value, if applicable. |
|
|
27
|
+
| `step` | `int`, `float`, or `None` | Step size for parameter sweeps, if applicable. |
|
|
28
|
+
| `choices` | `list` or `None` | Explicit list of allowed values, if applicable. |
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
default: int | float | str | bool | enum.Enum
|
|
32
|
+
min: int | float | None = None
|
|
33
|
+
max: int | float | None = None
|
|
34
|
+
step: int | float | None = None
|
|
35
|
+
choices: list | None = None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def resolved_choices(self) -> list | None:
|
|
39
|
+
"""
|
|
40
|
+
Return the effective list of allowed values for this parameter.
|
|
41
|
+
|
|
42
|
+
If `choices` is explicitly set, returns that list.
|
|
43
|
+
If `default` is an enum member, returns all members of that enum type.
|
|
44
|
+
Otherwise, returns `None`.
|
|
45
|
+
"""
|
|
46
|
+
if self.choices is not None:
|
|
47
|
+
return self.choices
|
|
48
|
+
if isinstance(self.default, enum.Enum):
|
|
49
|
+
return list(type(self.default))
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclasses.dataclass
|
|
54
|
+
class OrderRecord:
|
|
55
|
+
"""
|
|
56
|
+
Internal record of an order submitted by a strategy.
|
|
57
|
+
|
|
58
|
+
Tracks the state of an order from submission through fill or cancellation.
|
|
59
|
+
|
|
60
|
+
| Field | Type | Semantics |
|
|
61
|
+
|-------------------|--------------------|-----------------------------------------------------|
|
|
62
|
+
| `order_id` | `uuid.UUID` | System-assigned unique identifier for the order. |
|
|
63
|
+
| `symbol` | `str` | Identifier of the traded instrument. |
|
|
64
|
+
| `order_type` | `models.OrderType` | Execution constraint of the order. |
|
|
65
|
+
| `side` | `models.TradeSide` | Direction of the trade. |
|
|
66
|
+
| `quantity` | `float` | Requested order quantity. |
|
|
67
|
+
| `limit_price` | `float` or `None` | Limit price, if applicable to the order type. |
|
|
68
|
+
| `stop_price` | `float` or `None` | Stop price, if applicable to the order type. |
|
|
69
|
+
| `signal` | `str` or `None` | Optional signal name associated with the order. |
|
|
70
|
+
| `filled_quantity` | `float` | Cumulative quantity filled for this order. |
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
order_id: uuid.UUID
|
|
74
|
+
symbol: str
|
|
75
|
+
order_type: models.OrderType
|
|
76
|
+
side: models.TradeSide
|
|
77
|
+
quantity: float
|
|
78
|
+
limit_price: float | None = None
|
|
79
|
+
stop_price: float | None = None
|
|
80
|
+
signal: str | None = None
|
|
81
|
+
filled_quantity: float = 0.0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclasses.dataclass
|
|
85
|
+
class FillRecord:
|
|
86
|
+
"""
|
|
87
|
+
Internal record of a fill received by a strategy.
|
|
88
|
+
|
|
89
|
+
Captures execution details for a single fill event.
|
|
90
|
+
|
|
91
|
+
| Field | Type | Semantics |
|
|
92
|
+
|--------------|--------------------|-----------------------------------------------------------|
|
|
93
|
+
| `fill_id` | `uuid.UUID` | System-assigned unique identifier for the fill. |
|
|
94
|
+
| `order_id` | `uuid.UUID` | Identifier of the order associated with the fill. |
|
|
95
|
+
| `symbol` | `str` | Identifier of the traded instrument. |
|
|
96
|
+
| `side` | `models.TradeSide` | Trade direction of the executed quantity. |
|
|
97
|
+
| `quantity` | `float` | Quantity executed in this fill. |
|
|
98
|
+
| `price` | `float` | Execution price of the fill. |
|
|
99
|
+
| `commission` | `float` | Commission or fee associated with the fill. |
|
|
100
|
+
| `ts_event` | `pd.Timestamp` | Timestamp at which the fill was observed by the strategy. |
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
fill_id: uuid.UUID
|
|
104
|
+
order_id: uuid.UUID
|
|
105
|
+
symbol: str
|
|
106
|
+
side: models.TradeSide
|
|
107
|
+
quantity: float
|
|
108
|
+
price: float
|
|
109
|
+
commission: float
|
|
110
|
+
ts_event: pd.Timestamp
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class StrategyBase(messaging.Subscriber, abc.ABC):
|
|
114
|
+
"""
|
|
115
|
+
Abstract base class for trading strategies.
|
|
116
|
+
|
|
117
|
+
A strategy subscribes to market data and order events, maintains position state,
|
|
118
|
+
and submits orders through the event bus. Subclasses implement `on_bar` to define
|
|
119
|
+
trading logic and optionally override `setup` to register indicators.
|
|
120
|
+
|
|
121
|
+
Class Attributes:
|
|
122
|
+
name:
|
|
123
|
+
Human-readable name of the strategy.
|
|
124
|
+
symbols:
|
|
125
|
+
List of instrument symbols the strategy trades.
|
|
126
|
+
parameters:
|
|
127
|
+
Dictionary mapping parameter names to their specifications.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
name: str = ""
|
|
131
|
+
symbols: list[str] = []
|
|
132
|
+
parameters: dict[str, ParamSpec] = {}
|
|
133
|
+
|
|
134
|
+
def __init__(self, event_bus: messaging.EventBus, **overrides) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Initialize the strategy and start event processing.
|
|
137
|
+
|
|
138
|
+
Parameters:
|
|
139
|
+
event_bus:
|
|
140
|
+
Event bus used for subscribing to and publishing events.
|
|
141
|
+
**overrides:
|
|
142
|
+
Parameter values to override defaults defined in `parameters`.
|
|
143
|
+
"""
|
|
144
|
+
super().__init__(event_bus)
|
|
145
|
+
|
|
146
|
+
for name, spec in self.parameters.items():
|
|
147
|
+
value = overrides.get(name, spec.default)
|
|
148
|
+
setattr(self, name, value)
|
|
149
|
+
|
|
150
|
+
self._subscribe(
|
|
151
|
+
events.market.BarReceived,
|
|
152
|
+
events.responses.OrderAccepted,
|
|
153
|
+
events.responses.ModificationAccepted,
|
|
154
|
+
events.responses.CancellationAccepted,
|
|
155
|
+
events.responses.OrderRejected,
|
|
156
|
+
events.responses.ModificationRejected,
|
|
157
|
+
events.responses.CancellationRejected,
|
|
158
|
+
events.orders.FillEvent,
|
|
159
|
+
events.orders.OrderExpired,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self._current_symbol: str = ""
|
|
163
|
+
self._current_ts: pd.Timestamp = pd.Timestamp.now(tz="UTC")
|
|
164
|
+
self._indicators: list[indicators.IndicatorBase] = []
|
|
165
|
+
|
|
166
|
+
self._fills: dict[str, list[FillRecord]] = {}
|
|
167
|
+
self._positions: dict[str, float] = {}
|
|
168
|
+
self._avg_prices: dict[str, float] = {}
|
|
169
|
+
self._pending_orders: dict[uuid.UUID, OrderRecord] = {}
|
|
170
|
+
self._submitted_orders: dict[uuid.UUID, OrderRecord] = {}
|
|
171
|
+
self._submitted_modifications: dict[uuid.UUID, OrderRecord] = {}
|
|
172
|
+
self._submitted_cancellations: dict[uuid.UUID, OrderRecord] = {}
|
|
173
|
+
|
|
174
|
+
# OHLCV as indicators for history access: self.bar.close.history
|
|
175
|
+
self.bar = SimpleNamespace(
|
|
176
|
+
open=self.add_indicator(indicators.Open()),
|
|
177
|
+
high=self.add_indicator(indicators.High()),
|
|
178
|
+
low=self.add_indicator(indicators.Low()),
|
|
179
|
+
close=self.add_indicator(indicators.Close()),
|
|
180
|
+
volume=self.add_indicator(indicators.Volume()),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Hook for subclasses to register indicators without overriding __init__
|
|
184
|
+
self.setup()
|
|
185
|
+
|
|
186
|
+
def add_indicator(self, ind: indicators.IndicatorBase) -> indicators.IndicatorBase:
|
|
187
|
+
"""
|
|
188
|
+
Register an indicator with the strategy.
|
|
189
|
+
|
|
190
|
+
Registered indicators are automatically updated on each bar event.
|
|
191
|
+
|
|
192
|
+
Parameters:
|
|
193
|
+
ind:
|
|
194
|
+
Indicator instance to register.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
The registered indicator instance.
|
|
198
|
+
"""
|
|
199
|
+
self._indicators.append(ind)
|
|
200
|
+
return ind
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def position(self) -> float:
|
|
204
|
+
"""
|
|
205
|
+
Return the current position for the active symbol.
|
|
206
|
+
|
|
207
|
+
The active symbol is set by the most recently processed bar event.
|
|
208
|
+
"""
|
|
209
|
+
return self._positions.get(self._current_symbol, 0.0)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def avg_price(self) -> float:
|
|
213
|
+
"""
|
|
214
|
+
Return the average entry price for the current position on the active symbol.
|
|
215
|
+
|
|
216
|
+
Returns zero if there is no open position.
|
|
217
|
+
"""
|
|
218
|
+
return self._avg_prices.get(self._current_symbol, 0.0)
|
|
219
|
+
|
|
220
|
+
def submit_order(
|
|
221
|
+
self,
|
|
222
|
+
order_type: models.OrderType,
|
|
223
|
+
side: models.TradeSide,
|
|
224
|
+
quantity: float,
|
|
225
|
+
limit_price: float | None = None,
|
|
226
|
+
stop_price: float | None = None,
|
|
227
|
+
action: models.ActionType | None = None,
|
|
228
|
+
signal: str | None = None,
|
|
229
|
+
) -> uuid.UUID:
|
|
230
|
+
"""
|
|
231
|
+
Submit a new order for the active symbol.
|
|
232
|
+
|
|
233
|
+
Parameters:
|
|
234
|
+
order_type:
|
|
235
|
+
Execution constraint of the order.
|
|
236
|
+
side:
|
|
237
|
+
Direction of the trade.
|
|
238
|
+
quantity:
|
|
239
|
+
Requested order quantity.
|
|
240
|
+
limit_price:
|
|
241
|
+
Limit price, if applicable to the order type.
|
|
242
|
+
stop_price:
|
|
243
|
+
Stop price, if applicable to the order type.
|
|
244
|
+
action:
|
|
245
|
+
Intent of the order from the strategy's perspective (e.g., entry, exit).
|
|
246
|
+
signal:
|
|
247
|
+
Optional signal name associated with the order.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
System-assigned unique identifier for the submitted order.
|
|
251
|
+
"""
|
|
252
|
+
order_id = uuid.uuid4()
|
|
253
|
+
|
|
254
|
+
event = events.requests.OrderSubmissionRequest(
|
|
255
|
+
ts_event_ns=int(self._current_ts.value),
|
|
256
|
+
system_order_id=order_id,
|
|
257
|
+
symbol=self._current_symbol,
|
|
258
|
+
order_type=order_type,
|
|
259
|
+
side=side,
|
|
260
|
+
quantity=quantity,
|
|
261
|
+
limit_price=limit_price,
|
|
262
|
+
stop_price=stop_price,
|
|
263
|
+
action=action,
|
|
264
|
+
signal=signal,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
order = OrderRecord(
|
|
268
|
+
order_id=order_id,
|
|
269
|
+
symbol=self._current_symbol,
|
|
270
|
+
order_type=order_type,
|
|
271
|
+
side=side,
|
|
272
|
+
quantity=quantity,
|
|
273
|
+
limit_price=limit_price,
|
|
274
|
+
stop_price=stop_price,
|
|
275
|
+
signal=signal,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
self._submitted_orders[order_id] = order
|
|
279
|
+
self._publish(event)
|
|
280
|
+
return order_id
|
|
281
|
+
|
|
282
|
+
def submit_modification(
|
|
283
|
+
self,
|
|
284
|
+
order_id: uuid.UUID,
|
|
285
|
+
quantity: float | None = None,
|
|
286
|
+
limit_price: float | None = None,
|
|
287
|
+
stop_price: float | None = None,
|
|
288
|
+
) -> bool:
|
|
289
|
+
"""
|
|
290
|
+
Submit a modification request for a pending order.
|
|
291
|
+
|
|
292
|
+
Parameters:
|
|
293
|
+
order_id:
|
|
294
|
+
Identifier of the order to modify.
|
|
295
|
+
quantity:
|
|
296
|
+
Updated order quantity, or `None` to keep unchanged.
|
|
297
|
+
limit_price:
|
|
298
|
+
Updated limit price, or `None` to keep unchanged.
|
|
299
|
+
stop_price:
|
|
300
|
+
Updated stop price, or `None` to keep unchanged.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
`True` if the modification request was submitted, `False` if the order was not found.
|
|
304
|
+
"""
|
|
305
|
+
original_order = self._pending_orders.get(order_id)
|
|
306
|
+
if original_order is None:
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
event = events.requests.OrderModificationRequest(
|
|
310
|
+
ts_event_ns=int(self._current_ts.value),
|
|
311
|
+
system_order_id=order_id,
|
|
312
|
+
symbol=original_order.symbol,
|
|
313
|
+
quantity=quantity,
|
|
314
|
+
limit_price=limit_price,
|
|
315
|
+
stop_price=stop_price,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
modified_order = OrderRecord(
|
|
319
|
+
order_id=order_id,
|
|
320
|
+
symbol=original_order.symbol,
|
|
321
|
+
order_type=original_order.order_type,
|
|
322
|
+
side=original_order.side,
|
|
323
|
+
quantity=quantity if quantity is not None else original_order.quantity,
|
|
324
|
+
limit_price=(
|
|
325
|
+
limit_price if limit_price is not None else original_order.limit_price
|
|
326
|
+
),
|
|
327
|
+
stop_price=(
|
|
328
|
+
stop_price if stop_price is not None else original_order.stop_price
|
|
329
|
+
),
|
|
330
|
+
signal=original_order.signal,
|
|
331
|
+
filled_quantity=original_order.filled_quantity,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
self._submitted_modifications[order_id] = modified_order
|
|
335
|
+
self._publish(event)
|
|
336
|
+
return True
|
|
337
|
+
|
|
338
|
+
def submit_cancellation(self, order_id: uuid.UUID) -> bool:
|
|
339
|
+
"""
|
|
340
|
+
Submit a cancellation request for a pending order.
|
|
341
|
+
|
|
342
|
+
Parameters:
|
|
343
|
+
order_id:
|
|
344
|
+
Identifier of the order to cancel.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
`True` if the cancellation request was submitted, `False` if the order was not found.
|
|
348
|
+
"""
|
|
349
|
+
original_order = self._pending_orders.get(order_id)
|
|
350
|
+
if original_order is None:
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
event = events.requests.OrderCancellationRequest(
|
|
354
|
+
ts_event_ns=int(self._current_ts.value),
|
|
355
|
+
system_order_id=order_id,
|
|
356
|
+
symbol=original_order.symbol,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
self._submitted_cancellations[order_id] = original_order
|
|
360
|
+
self._publish(event)
|
|
361
|
+
return True
|
|
362
|
+
|
|
363
|
+
def _on_event(self, event: events.EventBase) -> None:
|
|
364
|
+
match event:
|
|
365
|
+
case events.market.BarReceived() as bar_event:
|
|
366
|
+
self._on_bar_received(bar_event)
|
|
367
|
+
case events.responses.OrderAccepted() as accepted:
|
|
368
|
+
self._on_order_submission_accepted(accepted)
|
|
369
|
+
case events.responses.ModificationAccepted() as accepted:
|
|
370
|
+
self._on_order_modification_accepted(accepted)
|
|
371
|
+
case events.responses.CancellationAccepted() as accepted:
|
|
372
|
+
self._on_order_cancellation_accepted(accepted)
|
|
373
|
+
case events.responses.OrderRejected() as rejected:
|
|
374
|
+
self._on_order_submission_rejected(rejected)
|
|
375
|
+
case events.responses.ModificationRejected() as rejected:
|
|
376
|
+
self._on_order_modification_rejected(rejected)
|
|
377
|
+
case events.responses.CancellationRejected() as rejected:
|
|
378
|
+
self._on_order_cancellation_rejected(rejected)
|
|
379
|
+
case events.orders.FillEvent() as filled:
|
|
380
|
+
self._on_order_filled(filled)
|
|
381
|
+
case events.orders.OrderExpired() as expired:
|
|
382
|
+
self._on_order_expired(expired)
|
|
383
|
+
case _:
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
def _on_bar_received(self, event: events.market.BarReceived) -> None:
|
|
387
|
+
if event.symbol not in self.symbols:
|
|
388
|
+
return
|
|
389
|
+
if event.bar_period != self.bar_period: # type: ignore[attr-defined]
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
self._current_symbol = event.symbol
|
|
393
|
+
self._current_ts = pd.Timestamp(event.ts_event_ns, tz="UTC")
|
|
394
|
+
|
|
395
|
+
for ind in self._indicators:
|
|
396
|
+
ind.update(event)
|
|
397
|
+
|
|
398
|
+
self._emit_processed_bar(event)
|
|
399
|
+
self.on_bar(event)
|
|
400
|
+
|
|
401
|
+
def _emit_processed_bar(self, event: events.market.BarReceived) -> None:
|
|
402
|
+
ohlcv_names = {"OPEN", "HIGH", "LOW", "CLOSE", "VOLUME"}
|
|
403
|
+
|
|
404
|
+
indicator_values = {
|
|
405
|
+
f"{ind.plot_at:02d}_{ind.name}": ind.latest(event.symbol)
|
|
406
|
+
for ind in self._indicators
|
|
407
|
+
if ind.name not in ohlcv_names
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
processed_bar = events.market.BarProcessed(
|
|
411
|
+
ts_event_ns=event.ts_event_ns,
|
|
412
|
+
symbol=event.symbol,
|
|
413
|
+
bar_period=event.bar_period,
|
|
414
|
+
open=event.open,
|
|
415
|
+
high=event.high,
|
|
416
|
+
low=event.low,
|
|
417
|
+
close=event.close,
|
|
418
|
+
volume=event.volume,
|
|
419
|
+
indicators=indicator_values,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
self._publish(processed_bar)
|
|
423
|
+
|
|
424
|
+
def _on_order_submission_accepted(
|
|
425
|
+
self, event: events.responses.OrderAccepted
|
|
426
|
+
) -> None:
|
|
427
|
+
order = self._submitted_orders.pop(event.associated_order_id, None)
|
|
428
|
+
if order is not None:
|
|
429
|
+
self._pending_orders[event.associated_order_id] = order
|
|
430
|
+
|
|
431
|
+
def _on_order_modification_accepted(
|
|
432
|
+
self, event: events.responses.ModificationAccepted
|
|
433
|
+
) -> None:
|
|
434
|
+
modified_order = self._submitted_modifications.pop(
|
|
435
|
+
event.associated_order_id, None
|
|
436
|
+
)
|
|
437
|
+
if modified_order is not None:
|
|
438
|
+
self._pending_orders[event.associated_order_id] = modified_order
|
|
439
|
+
|
|
440
|
+
def _on_order_cancellation_accepted(
|
|
441
|
+
self, event: events.responses.CancellationAccepted
|
|
442
|
+
) -> None:
|
|
443
|
+
self._submitted_cancellations.pop(event.associated_order_id, None)
|
|
444
|
+
self._pending_orders.pop(event.associated_order_id, None)
|
|
445
|
+
|
|
446
|
+
def _on_order_submission_rejected(
|
|
447
|
+
self, event: events.responses.OrderRejected
|
|
448
|
+
) -> None:
|
|
449
|
+
self._submitted_orders.pop(event.associated_order_id, None)
|
|
450
|
+
|
|
451
|
+
def _on_order_modification_rejected(
|
|
452
|
+
self, event: events.responses.ModificationRejected
|
|
453
|
+
) -> None:
|
|
454
|
+
self._submitted_modifications.pop(event.associated_order_id, None)
|
|
455
|
+
|
|
456
|
+
def _on_order_cancellation_rejected(
|
|
457
|
+
self, event: events.responses.CancellationRejected
|
|
458
|
+
) -> None:
|
|
459
|
+
self._submitted_cancellations.pop(event.associated_order_id, None)
|
|
460
|
+
|
|
461
|
+
def _on_order_filled(self, event: events.orders.FillEvent) -> None:
|
|
462
|
+
order = self._pending_orders.get(event.associated_order_id)
|
|
463
|
+
if order:
|
|
464
|
+
order.filled_quantity += event.quantity_filled
|
|
465
|
+
if order.filled_quantity >= order.quantity:
|
|
466
|
+
self._pending_orders.pop(event.associated_order_id)
|
|
467
|
+
|
|
468
|
+
fill = FillRecord(
|
|
469
|
+
fill_id=event.fill_id,
|
|
470
|
+
order_id=event.associated_order_id,
|
|
471
|
+
symbol=event.symbol,
|
|
472
|
+
side=event.side,
|
|
473
|
+
quantity=event.quantity_filled,
|
|
474
|
+
price=event.fill_price,
|
|
475
|
+
commission=event.commission,
|
|
476
|
+
ts_event=pd.Timestamp(event.ts_event_ns, tz="UTC"),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
self._fills.setdefault(event.symbol, []).append(fill)
|
|
480
|
+
self._update_position(event)
|
|
481
|
+
|
|
482
|
+
def _update_position(self, event: events.orders.FillEvent) -> None:
|
|
483
|
+
symbol = event.symbol
|
|
484
|
+
fill_qty = event.quantity_filled
|
|
485
|
+
fill_price = event.fill_price
|
|
486
|
+
|
|
487
|
+
signed_qty = 0.0
|
|
488
|
+
match event.side:
|
|
489
|
+
case models.TradeSide.BUY:
|
|
490
|
+
signed_qty = fill_qty
|
|
491
|
+
case models.TradeSide.SELL:
|
|
492
|
+
signed_qty = -fill_qty
|
|
493
|
+
|
|
494
|
+
old_pos = self._positions.get(symbol, 0.0)
|
|
495
|
+
old_avg = self._avg_prices.get(symbol, 0.0)
|
|
496
|
+
new_pos = old_pos + signed_qty
|
|
497
|
+
|
|
498
|
+
if new_pos == 0.0:
|
|
499
|
+
new_avg = 0.0
|
|
500
|
+
elif old_pos == 0.0:
|
|
501
|
+
new_avg = fill_price
|
|
502
|
+
elif (old_pos > 0 and signed_qty > 0) or (old_pos < 0 and signed_qty < 0):
|
|
503
|
+
new_avg = (old_avg * abs(old_pos) + fill_price * abs(signed_qty)) / abs(
|
|
504
|
+
new_pos
|
|
505
|
+
)
|
|
506
|
+
else:
|
|
507
|
+
if abs(new_pos) <= abs(old_pos):
|
|
508
|
+
new_avg = old_avg
|
|
509
|
+
else:
|
|
510
|
+
new_avg = fill_price
|
|
511
|
+
|
|
512
|
+
self._positions[symbol] = new_pos
|
|
513
|
+
self._avg_prices[symbol] = new_avg
|
|
514
|
+
|
|
515
|
+
def _on_order_expired(self, event: events.orders.OrderExpired) -> None:
|
|
516
|
+
self._pending_orders.pop(event.associated_order_id, None)
|
|
517
|
+
|
|
518
|
+
def setup(self) -> None:
|
|
519
|
+
"""
|
|
520
|
+
Hook for subclasses to register indicators and perform initialization.
|
|
521
|
+
|
|
522
|
+
Called at the end of `__init__`. Override this method to register indicators
|
|
523
|
+
using `add_indicator` without needing to override `__init__`.
|
|
524
|
+
"""
|
|
525
|
+
pass
|
|
526
|
+
|
|
527
|
+
@abc.abstractmethod
|
|
528
|
+
def on_bar(self, event: events.market.BarReceived) -> None:
|
|
529
|
+
"""
|
|
530
|
+
Handle a bar event for a subscribed symbol.
|
|
531
|
+
|
|
532
|
+
Called after all registered indicators have been updated. Subclasses implement
|
|
533
|
+
this method to define trading logic.
|
|
534
|
+
|
|
535
|
+
Parameters:
|
|
536
|
+
event:
|
|
537
|
+
Bar event containing OHLCV data for the current bar.
|
|
538
|
+
"""
|
|
539
|
+
pass
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from onesecondtrader import events, indicators, models
|
|
2
|
+
from .base import StrategyBase, ParamSpec
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SMACrossover(StrategyBase):
|
|
6
|
+
name = "SMA Crossover"
|
|
7
|
+
parameters = {
|
|
8
|
+
"bar_period": ParamSpec(default=models.BarPeriod.SECOND),
|
|
9
|
+
"fast_period": ParamSpec(default=20, min=5, max=100, step=1),
|
|
10
|
+
"slow_period": ParamSpec(default=100, min=10, max=500, step=1),
|
|
11
|
+
"quantity": ParamSpec(default=1.0, min=0.1, max=100.0, step=0.1),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def setup(self) -> None:
|
|
15
|
+
self.fast_sma = self.add_indicator(
|
|
16
|
+
indicators.SimpleMovingAverage(period=self.fast_period) # type: ignore[attr-defined]
|
|
17
|
+
)
|
|
18
|
+
self.slow_sma = self.add_indicator(
|
|
19
|
+
indicators.SimpleMovingAverage(period=self.slow_period) # type: ignore[attr-defined]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def on_bar(self, event: events.market.BarReceived) -> None:
|
|
23
|
+
sym = event.symbol
|
|
24
|
+
if (
|
|
25
|
+
self.fast_sma[sym, -2] <= self.slow_sma[sym, -2]
|
|
26
|
+
and self.fast_sma.latest(sym) > self.slow_sma.latest(sym)
|
|
27
|
+
and self.position <= 0
|
|
28
|
+
):
|
|
29
|
+
self.submit_order(
|
|
30
|
+
models.OrderType.MARKET,
|
|
31
|
+
models.TradeSide.BUY,
|
|
32
|
+
self.quantity, # type: ignore[attr-defined]
|
|
33
|
+
action=models.ActionType.ENTRY,
|
|
34
|
+
signal="sma_crossover_up",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
self.fast_sma[sym, -2] >= self.slow_sma[sym, -2]
|
|
39
|
+
and self.fast_sma.latest(sym) < self.slow_sma.latest(sym)
|
|
40
|
+
and self.position >= 0
|
|
41
|
+
):
|
|
42
|
+
self.submit_order(
|
|
43
|
+
models.OrderType.MARKET,
|
|
44
|
+
models.TradeSide.SELL,
|
|
45
|
+
self.quantity, # type: ignore[attr-defined]
|
|
46
|
+
action=models.ActionType.EXIT,
|
|
47
|
+
signal="sma_crossover_down",
|
|
48
|
+
)
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import dataclasses
|
|
4
|
-
import uuid
|
|
5
|
-
|
|
6
|
-
from onesecondtrader import models
|
|
7
|
-
from onesecondtrader.events.requests.base import RequestBase
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
11
|
-
class OrderSubmissionRequest(RequestBase):
|
|
12
|
-
"""
|
|
13
|
-
Event representing a request to submit a new order to a brokers.
|
|
14
|
-
|
|
15
|
-
The `system_order_id` is a unique identifier assigned by the system to the order submission request by default at object creation.
|
|
16
|
-
|
|
17
|
-
| Field | Type | Semantics |
|
|
18
|
-
|-------------------|--------------------------|----------------------------------------------------------------------------|
|
|
19
|
-
| `ts_event_ns` | `int` | Time at which the submission request was issued, as UTC epoch nanoseconds. |
|
|
20
|
-
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
21
|
-
| `system_order_id` | `uuid.UUID` | System-assigned unique identifier for the order submission. |
|
|
22
|
-
| `symbol` | `str` | Identifier of the traded instrument. |
|
|
23
|
-
| `order_type` | `models.OrderType` | Execution constraint of the order. |
|
|
24
|
-
| `side` | `models.TradeSide` | Direction of the trade. |
|
|
25
|
-
| `quantity` | `float` | Requested order quantity. |
|
|
26
|
-
| `limit_price` | `float` or `None` | Limit price, if applicable to the order type. |
|
|
27
|
-
| `stop_price` | `float` or `None` | Stop price, if applicable to the order type. |
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
system_order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
31
|
-
order_type: models.OrderType
|
|
32
|
-
side: models.TradeSide
|
|
33
|
-
quantity: float
|
|
34
|
-
limit_price: float | None = None
|
|
35
|
-
stop_price: float | None = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/market/__init__.py
RENAMED
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/market/bar_processed.py
RENAMED
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/market/bar_received.py
RENAMED
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/expirations.py
RENAMED
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/fills.py
RENAMED
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/requests/__init__.py
RENAMED
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/requests/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/__init__.py
RENAMED
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/orders.py
RENAMED
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/market_fields.py
RENAMED
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/moving_averages.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/messaging/subscriber.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.53.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/rejection_reasons.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|