onesecondtrader 0.49.0__tar.gz → 0.51.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.49.0 → onesecondtrader-0.51.0}/PKG-INFO +1 -1
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/pyproject.toml +1 -1
- onesecondtrader-0.51.0/src/onesecondtrader/brokers/__init__.py +11 -0
- onesecondtrader-0.51.0/src/onesecondtrader/brokers/base.py +110 -0
- onesecondtrader-0.51.0/src/onesecondtrader/brokers/simulated.py +494 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/orders/base.py +4 -4
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/orders/expirations.py +2 -2
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/orders/fills.py +1 -1
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/requests/base.py +1 -1
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/requests/order_submission.py +1 -1
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/responses/base.py +4 -4
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/responses/cancellations.py +5 -5
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/responses/modifications.py +5 -5
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/responses/orders.py +5 -5
- onesecondtrader-0.51.0/src/onesecondtrader/messaging/__init__.py +11 -0
- onesecondtrader-0.51.0/src/onesecondtrader/messaging/eventbus.py +96 -0
- onesecondtrader-0.51.0/src/onesecondtrader/messaging/subscriber.py +153 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/models/rejection_reasons.py +5 -5
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/LICENSE +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/README.md +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/__init__.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/__init__.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/base.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/market/__init__.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/market/bar_processed.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/market/bar_received.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/orders/__init__.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/requests/__init__.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/requests/order_cancellation.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/requests/order_modification.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/responses/__init__.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/indicators/__init__.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/indicators/base.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/indicators/market_fields.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/indicators/moving_averages.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/models/__init__.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/models/bar_fields.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/models/bar_period.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/models/order_types.py +0 -0
- {onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/models/trade_sides.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onesecondtrader
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.51.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.51.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,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provides interfaces for order execution via a simulated broker and adapters to real venues.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from onesecondtrader.brokers.base import BrokerBase
|
|
6
|
+
from onesecondtrader.brokers.simulated import SimulatedBroker
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BrokerBase",
|
|
10
|
+
"SimulatedBroker",
|
|
11
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
|
|
3
|
+
from onesecondtrader import events, messaging
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BrokerBase(messaging.Subscriber):
|
|
7
|
+
"""
|
|
8
|
+
Abstract base class for broker components.
|
|
9
|
+
|
|
10
|
+
A broker component receives order-related request events from the event bus and translates them into actions against an external execution venue or simulated environment.
|
|
11
|
+
Responses to these requests are published back onto the event bus.
|
|
12
|
+
|
|
13
|
+
This class defines the event-handling interface and subscription logic common to all broker implementations.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initialize the broker and subscribe to order request events.
|
|
19
|
+
|
|
20
|
+
Parameters:
|
|
21
|
+
event_bus:
|
|
22
|
+
Event bus used for receiving order requests and publishing response events.
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(event_bus)
|
|
25
|
+
|
|
26
|
+
self._subscribe(
|
|
27
|
+
events.requests.OrderSubmissionRequest,
|
|
28
|
+
events.requests.OrderCancellationRequest,
|
|
29
|
+
events.requests.OrderModificationRequest,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@abc.abstractmethod
|
|
33
|
+
def connect(self) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Establish a connection to the external broker API.
|
|
36
|
+
For simulated broker, this method is a no-op.
|
|
37
|
+
|
|
38
|
+
Implementations are responsible for initializing any external resources required to submit, modify, or cancel orders.
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def disconnect(self) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Disconnect the external broker API and stop event processing.
|
|
45
|
+
For simulated broker, this method is a no-op.
|
|
46
|
+
|
|
47
|
+
This method shuts down the subscriber and releases associated resources.
|
|
48
|
+
"""
|
|
49
|
+
self.shutdown()
|
|
50
|
+
|
|
51
|
+
def _on_event(self, event: events.EventBase) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Dispatch incoming order-related events to the appropriate handler.
|
|
54
|
+
|
|
55
|
+
Parameters:
|
|
56
|
+
event:
|
|
57
|
+
Incoming event received from the event bus.
|
|
58
|
+
"""
|
|
59
|
+
match event:
|
|
60
|
+
case events.requests.OrderSubmissionRequest() as submit_order:
|
|
61
|
+
self._on_submit_order(submit_order)
|
|
62
|
+
case events.requests.OrderCancellationRequest() as cancel_order:
|
|
63
|
+
self._on_cancel_order(cancel_order)
|
|
64
|
+
case events.requests.OrderModificationRequest() as modify_order:
|
|
65
|
+
self._on_modify_order(modify_order)
|
|
66
|
+
case _:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
@abc.abstractmethod
|
|
70
|
+
def _on_submit_order(self, event: events.requests.OrderSubmissionRequest) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Handle an order submission request.
|
|
73
|
+
|
|
74
|
+
Parameters:
|
|
75
|
+
event:
|
|
76
|
+
Order submission request event.
|
|
77
|
+
"""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
@abc.abstractmethod
|
|
81
|
+
def _on_cancel_order(self, event: events.requests.OrderCancellationRequest) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Handle an order cancellation request.
|
|
84
|
+
|
|
85
|
+
Parameters:
|
|
86
|
+
event:
|
|
87
|
+
Order cancellation request event.
|
|
88
|
+
"""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@abc.abstractmethod
|
|
92
|
+
def _on_modify_order(self, event: events.requests.OrderModificationRequest) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Handle an order modification request.
|
|
95
|
+
|
|
96
|
+
Parameters:
|
|
97
|
+
event:
|
|
98
|
+
Order modification request event.
|
|
99
|
+
"""
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def _respond(self, response_event: events.responses.ResponseBase) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Publish a response event to the event bus.
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
response_event:
|
|
108
|
+
Response event generated by the broker.
|
|
109
|
+
"""
|
|
110
|
+
self._publish(response_event)
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from onesecondtrader import events, messaging, models
|
|
7
|
+
from onesecondtrader.brokers.base import BrokerBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass
|
|
11
|
+
class _PendingOrder:
|
|
12
|
+
"""
|
|
13
|
+
Internal order state tracked by the simulated broker.
|
|
14
|
+
|
|
15
|
+
This structure represents broker-side pending order state and is distinct from order request events.
|
|
16
|
+
It is used to evaluate trigger conditions against incoming market bars and to generate fills when conditions are met.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
order_id: uuid.UUID
|
|
20
|
+
symbol: str
|
|
21
|
+
order_type: models.OrderType
|
|
22
|
+
side: models.TradeSide
|
|
23
|
+
quantity: float
|
|
24
|
+
limit_price: float | None = None
|
|
25
|
+
stop_price: float | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SimulatedBroker(BrokerBase):
|
|
29
|
+
"""
|
|
30
|
+
Event-driven simulated broker for backtesting.
|
|
31
|
+
|
|
32
|
+
The broker subscribes to order request events and market bar events.
|
|
33
|
+
Order requests are validated and accepted or rejected immediately.
|
|
34
|
+
Accepted orders are stored as pending broker-side state and evaluated against each incoming
|
|
35
|
+
bar.
|
|
36
|
+
When an order triggers, a fill event is published with a deterministic fill price model based on the bar's OHLC values.
|
|
37
|
+
|
|
38
|
+
The broker publishes response events using the event timestamp to preserve simulated time consistency.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
commission_per_unit: float = 0.0
|
|
42
|
+
minimum_commission_per_order: float = 0.0
|
|
43
|
+
|
|
44
|
+
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
45
|
+
"""
|
|
46
|
+
parameters:
|
|
47
|
+
event_bus:
|
|
48
|
+
Event bus used to receive order requests and market bars, and to publish broker responses and fills.
|
|
49
|
+
"""
|
|
50
|
+
self._pending_market_orders: dict[uuid.UUID, _PendingOrder] = {}
|
|
51
|
+
self._pending_limit_orders: dict[uuid.UUID, _PendingOrder] = {}
|
|
52
|
+
self._pending_stop_orders: dict[uuid.UUID, _PendingOrder] = {}
|
|
53
|
+
self._pending_stop_limit_orders: dict[uuid.UUID, _PendingOrder] = {}
|
|
54
|
+
|
|
55
|
+
super().__init__(event_bus)
|
|
56
|
+
self._subscribe(events.market.BarReceived)
|
|
57
|
+
|
|
58
|
+
def connect(self) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Establish broker readiness.
|
|
61
|
+
|
|
62
|
+
The simulated broker has no external connectivity requirements.
|
|
63
|
+
This method is a no-op and exists to satisfy the broker interface.
|
|
64
|
+
"""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def _on_event(self, event: events.EventBase) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Dispatch incoming events.
|
|
70
|
+
|
|
71
|
+
Market bar events are routed to bar processing.
|
|
72
|
+
All other events are delegated to the broker base class for order request handling.
|
|
73
|
+
|
|
74
|
+
parameters:
|
|
75
|
+
event:
|
|
76
|
+
Incoming event received from the event bus.
|
|
77
|
+
"""
|
|
78
|
+
match event:
|
|
79
|
+
case events.market.BarReceived() as bar:
|
|
80
|
+
self._on_bar(bar)
|
|
81
|
+
case _:
|
|
82
|
+
super()._on_event(event)
|
|
83
|
+
|
|
84
|
+
def _on_bar(self, event: events.market.BarReceived) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Process an incoming market bar.
|
|
87
|
+
|
|
88
|
+
Pending orders are evaluated against the bar in a fixed sequence to provide deterministic behavior.
|
|
89
|
+
Crucially, limit orders are processed after stop limit orders to ensure that limit orders created by stop limit orders are evaluated against the same bar.
|
|
90
|
+
|
|
91
|
+
parameters:
|
|
92
|
+
event:
|
|
93
|
+
Market bar used to trigger and price simulated fills.
|
|
94
|
+
"""
|
|
95
|
+
self._process_market_orders(event)
|
|
96
|
+
self._process_stop_orders(event)
|
|
97
|
+
self._process_stop_limit_orders(event)
|
|
98
|
+
self._process_limit_orders(event)
|
|
99
|
+
|
|
100
|
+
def _process_market_orders(self, event: events.market.BarReceived) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Fill pending market orders for the bar symbol.
|
|
103
|
+
|
|
104
|
+
Market orders are filled at the bar open price on the next received bar for the matching symbol.
|
|
105
|
+
|
|
106
|
+
parameters:
|
|
107
|
+
event:
|
|
108
|
+
Market bar providing the simulated fill price and timestamps.
|
|
109
|
+
"""
|
|
110
|
+
for order_id, order in list(self._pending_market_orders.items()):
|
|
111
|
+
if order.symbol != event.symbol:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
self._publish(
|
|
115
|
+
events.orders.FillEvent(
|
|
116
|
+
ts_event_ns=event.ts_event_ns,
|
|
117
|
+
ts_broker_ns=event.ts_event_ns,
|
|
118
|
+
associated_order_id=order.order_id,
|
|
119
|
+
symbol=order.symbol,
|
|
120
|
+
side=order.side,
|
|
121
|
+
quantity_filled=order.quantity,
|
|
122
|
+
fill_price=event.open,
|
|
123
|
+
commission=max(
|
|
124
|
+
order.quantity * self.commission_per_unit,
|
|
125
|
+
self.minimum_commission_per_order,
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
del self._pending_market_orders[order_id]
|
|
130
|
+
|
|
131
|
+
def _process_stop_orders(self, event: events.market.BarReceived) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Evaluate and fill pending stop orders for the bar symbol.
|
|
134
|
+
|
|
135
|
+
Stop orders trigger when the bar crosses the stop level.
|
|
136
|
+
The fill price is modeled as the worse of the stop price and the bar open in the direction of the trade.
|
|
137
|
+
|
|
138
|
+
parameters:
|
|
139
|
+
event:
|
|
140
|
+
Market bar used to evaluate triggers and determine fill prices.
|
|
141
|
+
"""
|
|
142
|
+
for order_id, order in list(self._pending_stop_orders.items()):
|
|
143
|
+
if order.symbol != event.symbol:
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# This is for mypy, it has already been validated on submission
|
|
147
|
+
assert order.stop_price is not None
|
|
148
|
+
|
|
149
|
+
triggered = False
|
|
150
|
+
match order.side:
|
|
151
|
+
case models.TradeSide.BUY:
|
|
152
|
+
triggered = event.high >= order.stop_price
|
|
153
|
+
case models.TradeSide.SELL:
|
|
154
|
+
triggered = event.low <= order.stop_price
|
|
155
|
+
|
|
156
|
+
if not triggered:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
fill_price = 0.0
|
|
160
|
+
match order.side:
|
|
161
|
+
case models.TradeSide.BUY:
|
|
162
|
+
fill_price = max(order.stop_price, event.open)
|
|
163
|
+
case models.TradeSide.SELL:
|
|
164
|
+
fill_price = min(order.stop_price, event.open)
|
|
165
|
+
|
|
166
|
+
self._publish(
|
|
167
|
+
events.orders.FillEvent(
|
|
168
|
+
ts_event_ns=event.ts_event_ns,
|
|
169
|
+
ts_broker_ns=event.ts_event_ns,
|
|
170
|
+
associated_order_id=order.order_id,
|
|
171
|
+
symbol=order.symbol,
|
|
172
|
+
side=order.side,
|
|
173
|
+
quantity_filled=order.quantity,
|
|
174
|
+
fill_price=fill_price,
|
|
175
|
+
commission=max(
|
|
176
|
+
order.quantity * self.commission_per_unit,
|
|
177
|
+
self.minimum_commission_per_order,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
del self._pending_stop_orders[order_id]
|
|
182
|
+
|
|
183
|
+
def _process_stop_limit_orders(self, event: events.market.BarReceived) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Evaluate pending stop-limit orders for the bar symbol.
|
|
186
|
+
|
|
187
|
+
Stop-limit orders trigger on stop conditions.
|
|
188
|
+
When triggered, they are converted into pending limit orders at the same identifier.
|
|
189
|
+
|
|
190
|
+
parameters:
|
|
191
|
+
event:
|
|
192
|
+
Market bar used to evaluate stop triggers.
|
|
193
|
+
"""
|
|
194
|
+
for order_id, order in list(self._pending_stop_limit_orders.items()):
|
|
195
|
+
if order.symbol != event.symbol:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
assert order.stop_price is not None
|
|
199
|
+
|
|
200
|
+
triggered = False
|
|
201
|
+
match order.side:
|
|
202
|
+
case models.TradeSide.BUY:
|
|
203
|
+
triggered = event.high >= order.stop_price
|
|
204
|
+
case models.TradeSide.SELL:
|
|
205
|
+
triggered = event.low <= order.stop_price
|
|
206
|
+
|
|
207
|
+
if not triggered:
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
limit_order = dataclasses.replace(order, order_type=models.OrderType.LIMIT)
|
|
211
|
+
self._pending_limit_orders[order_id] = limit_order
|
|
212
|
+
del self._pending_stop_limit_orders[order_id]
|
|
213
|
+
|
|
214
|
+
def _process_limit_orders(self, event: events.market.BarReceived) -> None:
|
|
215
|
+
"""
|
|
216
|
+
Evaluate and fill pending limit orders for the bar symbol.
|
|
217
|
+
|
|
218
|
+
Limit orders trigger when the bar crosses the limit level.
|
|
219
|
+
The fill price is modeled as the better of the limit price and the bar open in the direction of the trade.
|
|
220
|
+
|
|
221
|
+
parameters:
|
|
222
|
+
event:
|
|
223
|
+
Market bar used to evaluate triggers and determine fill prices.
|
|
224
|
+
"""
|
|
225
|
+
for order_id, order in list(self._pending_limit_orders.items()):
|
|
226
|
+
if order.symbol != event.symbol:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
assert order.limit_price is not None
|
|
230
|
+
|
|
231
|
+
triggered = False
|
|
232
|
+
match order.side:
|
|
233
|
+
case models.TradeSide.BUY:
|
|
234
|
+
triggered = event.low <= order.limit_price
|
|
235
|
+
case models.TradeSide.SELL:
|
|
236
|
+
triggered = event.high >= order.limit_price
|
|
237
|
+
|
|
238
|
+
if not triggered:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
fill_price = 0.0
|
|
242
|
+
match order.side:
|
|
243
|
+
case models.TradeSide.BUY:
|
|
244
|
+
fill_price = min(order.limit_price, event.open)
|
|
245
|
+
case models.TradeSide.SELL:
|
|
246
|
+
fill_price = max(order.limit_price, event.open)
|
|
247
|
+
|
|
248
|
+
self._publish(
|
|
249
|
+
events.orders.FillEvent(
|
|
250
|
+
ts_event_ns=event.ts_event_ns,
|
|
251
|
+
ts_broker_ns=event.ts_event_ns,
|
|
252
|
+
associated_order_id=order.order_id,
|
|
253
|
+
symbol=order.symbol,
|
|
254
|
+
side=order.side,
|
|
255
|
+
quantity_filled=order.quantity,
|
|
256
|
+
fill_price=fill_price,
|
|
257
|
+
commission=max(
|
|
258
|
+
order.quantity * self.commission_per_unit,
|
|
259
|
+
self.minimum_commission_per_order,
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
del self._pending_limit_orders[order_id]
|
|
264
|
+
|
|
265
|
+
def _reject_if_invalid_submission(
|
|
266
|
+
self, event: events.requests.OrderSubmissionRequest
|
|
267
|
+
) -> bool:
|
|
268
|
+
"""
|
|
269
|
+
Validate an order submission request.
|
|
270
|
+
|
|
271
|
+
Invalid submissions are rejected immediately by publishing an `OrderRejected` response event.
|
|
272
|
+
|
|
273
|
+
parameters:
|
|
274
|
+
event:
|
|
275
|
+
Order submission request event to validate.
|
|
276
|
+
|
|
277
|
+
returns:
|
|
278
|
+
True if the submission is invalid and was rejected, otherwise False.
|
|
279
|
+
"""
|
|
280
|
+
is_invalid = event.quantity <= 0
|
|
281
|
+
|
|
282
|
+
match event.order_type:
|
|
283
|
+
case models.OrderType.LIMIT:
|
|
284
|
+
is_invalid = (
|
|
285
|
+
is_invalid or event.limit_price is None or event.limit_price <= 0
|
|
286
|
+
)
|
|
287
|
+
case models.OrderType.STOP:
|
|
288
|
+
is_invalid = (
|
|
289
|
+
is_invalid or event.stop_price is None or event.stop_price <= 0
|
|
290
|
+
)
|
|
291
|
+
case models.OrderType.STOP_LIMIT:
|
|
292
|
+
is_invalid = is_invalid or (
|
|
293
|
+
event.limit_price is None
|
|
294
|
+
or event.limit_price <= 0
|
|
295
|
+
or event.stop_price is None
|
|
296
|
+
or event.stop_price <= 0
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if is_invalid:
|
|
300
|
+
self._publish(
|
|
301
|
+
events.responses.OrderRejected(
|
|
302
|
+
ts_event_ns=event.ts_event_ns,
|
|
303
|
+
ts_broker_ns=event.ts_event_ns,
|
|
304
|
+
associated_order_id=event.system_order_id,
|
|
305
|
+
rejection_reason=models.OrderRejectionReason.UNKNOWN,
|
|
306
|
+
rejection_message="Unknown",
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return is_invalid
|
|
311
|
+
|
|
312
|
+
def _on_submit_order(self, event: events.requests.OrderSubmissionRequest) -> None:
|
|
313
|
+
"""
|
|
314
|
+
Handle an order submission request.
|
|
315
|
+
|
|
316
|
+
Valid orders are stored as pending broker-side state and acknowledged via an `OrderAccepted` response event.
|
|
317
|
+
|
|
318
|
+
parameters:
|
|
319
|
+
event:
|
|
320
|
+
Order submission request event.
|
|
321
|
+
"""
|
|
322
|
+
if self._reject_if_invalid_submission(event):
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
order = _PendingOrder(
|
|
326
|
+
order_id=event.system_order_id,
|
|
327
|
+
symbol=event.symbol,
|
|
328
|
+
order_type=event.order_type,
|
|
329
|
+
side=event.side,
|
|
330
|
+
quantity=event.quantity,
|
|
331
|
+
limit_price=event.limit_price,
|
|
332
|
+
stop_price=event.stop_price,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
match order.order_type:
|
|
336
|
+
case models.OrderType.MARKET:
|
|
337
|
+
self._pending_market_orders[order.order_id] = order
|
|
338
|
+
case models.OrderType.LIMIT:
|
|
339
|
+
self._pending_limit_orders[order.order_id] = order
|
|
340
|
+
case models.OrderType.STOP:
|
|
341
|
+
self._pending_stop_orders[order.order_id] = order
|
|
342
|
+
case models.OrderType.STOP_LIMIT:
|
|
343
|
+
self._pending_stop_limit_orders[order.order_id] = order
|
|
344
|
+
|
|
345
|
+
self._publish(
|
|
346
|
+
events.responses.OrderAccepted(
|
|
347
|
+
ts_event_ns=event.ts_event_ns,
|
|
348
|
+
ts_broker_ns=event.ts_event_ns,
|
|
349
|
+
associated_order_id=order.order_id,
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def _on_cancel_order(self, event: events.requests.OrderCancellationRequest) -> None:
|
|
354
|
+
"""
|
|
355
|
+
Handle an order cancellation request.
|
|
356
|
+
|
|
357
|
+
If the referenced order is pending, it is removed and acknowledged via `CancellationAccepted`.
|
|
358
|
+
Otherwise, `CancellationRejected` is published.
|
|
359
|
+
|
|
360
|
+
parameters:
|
|
361
|
+
event:
|
|
362
|
+
Order cancellation request event.
|
|
363
|
+
"""
|
|
364
|
+
order_id = event.system_order_id
|
|
365
|
+
|
|
366
|
+
removed = False
|
|
367
|
+
for pending_orders in (
|
|
368
|
+
self._pending_market_orders,
|
|
369
|
+
self._pending_limit_orders,
|
|
370
|
+
self._pending_stop_orders,
|
|
371
|
+
self._pending_stop_limit_orders,
|
|
372
|
+
):
|
|
373
|
+
if order_id in pending_orders:
|
|
374
|
+
del pending_orders[order_id]
|
|
375
|
+
removed = True
|
|
376
|
+
break
|
|
377
|
+
|
|
378
|
+
if removed:
|
|
379
|
+
self._publish(
|
|
380
|
+
events.responses.CancellationAccepted(
|
|
381
|
+
ts_event_ns=event.ts_event_ns,
|
|
382
|
+
ts_broker_ns=event.ts_event_ns,
|
|
383
|
+
associated_order_id=order_id,
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
else:
|
|
387
|
+
self._publish(
|
|
388
|
+
events.responses.CancellationRejected(
|
|
389
|
+
ts_event_ns=event.ts_event_ns,
|
|
390
|
+
ts_broker_ns=event.ts_event_ns,
|
|
391
|
+
associated_order_id=order_id,
|
|
392
|
+
rejection_reason=models.CancellationRejectionReason.UNKNOWN,
|
|
393
|
+
rejection_message="Unknown",
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def _reject_if_invalid_modification(
|
|
398
|
+
self, event: events.requests.OrderModificationRequest
|
|
399
|
+
) -> bool:
|
|
400
|
+
"""
|
|
401
|
+
Validate an order modification request.
|
|
402
|
+
|
|
403
|
+
Invalid modifications are rejected immediately by publishing a `ModificationRejected` response event.
|
|
404
|
+
|
|
405
|
+
parameters:
|
|
406
|
+
event:
|
|
407
|
+
Order modification request event to validate.
|
|
408
|
+
|
|
409
|
+
returns:
|
|
410
|
+
True if the modification is invalid and was rejected, otherwise False.
|
|
411
|
+
"""
|
|
412
|
+
is_invalid = (
|
|
413
|
+
(event.quantity is not None and event.quantity <= 0)
|
|
414
|
+
or (event.limit_price is not None and event.limit_price <= 0)
|
|
415
|
+
or (event.stop_price is not None and event.stop_price <= 0)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if is_invalid:
|
|
419
|
+
self._publish(
|
|
420
|
+
events.responses.ModificationRejected(
|
|
421
|
+
ts_event_ns=event.ts_event_ns,
|
|
422
|
+
ts_broker_ns=event.ts_event_ns,
|
|
423
|
+
associated_order_id=event.system_order_id,
|
|
424
|
+
rejection_reason=models.ModificationRejectionReason.UNKNOWN,
|
|
425
|
+
rejection_message="Unknown",
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return is_invalid
|
|
430
|
+
|
|
431
|
+
def _on_modify_order(self, event: events.requests.OrderModificationRequest) -> None:
|
|
432
|
+
"""
|
|
433
|
+
Handle an order modification request.
|
|
434
|
+
|
|
435
|
+
If the referenced order is pending, its fields are updated and acknowledged via `ModificationAccepted`.
|
|
436
|
+
Otherwise, `ModificationRejected` is published.
|
|
437
|
+
|
|
438
|
+
parameters:
|
|
439
|
+
event:
|
|
440
|
+
Order modification request event.
|
|
441
|
+
"""
|
|
442
|
+
if self._reject_if_invalid_modification(event):
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
order_id = event.system_order_id
|
|
446
|
+
|
|
447
|
+
for pending_orders in (
|
|
448
|
+
self._pending_market_orders,
|
|
449
|
+
self._pending_limit_orders,
|
|
450
|
+
self._pending_stop_orders,
|
|
451
|
+
self._pending_stop_limit_orders,
|
|
452
|
+
):
|
|
453
|
+
if order_id in pending_orders:
|
|
454
|
+
order = pending_orders[order_id]
|
|
455
|
+
|
|
456
|
+
new_quantity = (
|
|
457
|
+
event.quantity if event.quantity is not None else order.quantity
|
|
458
|
+
)
|
|
459
|
+
new_limit_price = (
|
|
460
|
+
event.limit_price
|
|
461
|
+
if event.limit_price is not None
|
|
462
|
+
else order.limit_price
|
|
463
|
+
)
|
|
464
|
+
new_stop_price = (
|
|
465
|
+
event.stop_price
|
|
466
|
+
if event.stop_price is not None
|
|
467
|
+
else order.stop_price
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
pending_orders[order_id] = dataclasses.replace(
|
|
471
|
+
order,
|
|
472
|
+
quantity=new_quantity,
|
|
473
|
+
limit_price=new_limit_price,
|
|
474
|
+
stop_price=new_stop_price,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
self._publish(
|
|
478
|
+
events.responses.ModificationAccepted(
|
|
479
|
+
ts_event_ns=event.ts_event_ns,
|
|
480
|
+
ts_broker_ns=event.ts_event_ns,
|
|
481
|
+
associated_order_id=order_id,
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
self._publish(
|
|
487
|
+
events.responses.ModificationRejected(
|
|
488
|
+
ts_event_ns=event.ts_event_ns,
|
|
489
|
+
ts_broker_ns=event.ts_event_ns,
|
|
490
|
+
associated_order_id=order_id,
|
|
491
|
+
rejection_reason=models.ModificationRejectionReason.UNKNOWN,
|
|
492
|
+
rejection_message="Unknown",
|
|
493
|
+
)
|
|
494
|
+
)
|
|
@@ -9,17 +9,17 @@ from onesecondtrader import events
|
|
|
9
9
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
10
10
|
class OrderBase(events.EventBase):
|
|
11
11
|
"""
|
|
12
|
-
Base class for
|
|
12
|
+
Base class for brokers-originated order events.
|
|
13
13
|
|
|
14
|
-
Order events are
|
|
14
|
+
Order events are brokers-originated facts about the state or execution of an order.
|
|
15
15
|
Each order event is correlated to a system order identifier via `associated_order_id`.
|
|
16
16
|
|
|
17
17
|
| Field | Type | Semantics |
|
|
18
18
|
|-----------------------|-----------------|---------------------------------------------------------------------------------------|
|
|
19
19
|
| `ts_event_ns` | `int` | Time at which the response event was observed by the system, as UTC epoch nanoseconds.|
|
|
20
20
|
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
21
|
-
| `ts_broker_ns` | `int` | Time reported by the
|
|
22
|
-
| `associated_order_id` | `uuid.UUID`. | Identifier of the order associated with the
|
|
21
|
+
| `ts_broker_ns` | `int` | Time reported by the brokers for the response, as UTC epoch nanoseconds. |
|
|
22
|
+
| `associated_order_id` | `uuid.UUID`. | Identifier of the order associated with the brokers response. |
|
|
23
23
|
| `broker_order_id` | `str` or `None` | Broker-assigned identifier of the order, if reported. |
|
|
24
24
|
| `symbol` | `str` | Identifier of the traded instrument. |
|
|
25
25
|
"""
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/orders/expirations.py
RENAMED
|
@@ -8,13 +8,13 @@ from onesecondtrader.events.orders.base import OrderBase
|
|
|
8
8
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
9
9
|
class OrderExpired(OrderBase):
|
|
10
10
|
"""
|
|
11
|
-
Event indicating that the order is no longer active at the venue due to expiration according to
|
|
11
|
+
Event indicating that the order is no longer active at the venue due to expiration according to brokers- or venue-specific rules (e.g. time-in-force constraints).
|
|
12
12
|
|
|
13
13
|
| Field | Type | Semantics |
|
|
14
14
|
|-----------------------|-----------------|------------------------------------------------------------------------------------|
|
|
15
15
|
| `ts_event_ns` | `int` | Time at which the expiration was observed by the system, as UTC epoch nanoseconds. |
|
|
16
16
|
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
17
|
-
| `ts_broker_ns` | `int` | Time reported by the
|
|
17
|
+
| `ts_broker_ns` | `int` | Time reported by the brokers for the expiration, as UTC epoch nanoseconds. |
|
|
18
18
|
| `associated_order_id` | `uuid.UUID` | Identifier of the expired order. |
|
|
19
19
|
| `broker_order_id` | `str` or `None` | Broker-assigned identifier of the expired order, if reported. |
|
|
20
20
|
| `symbol` | `str` | Identifier of the traded instrument. |
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/orders/fills.py
RENAMED
|
@@ -19,7 +19,7 @@ class FillEvent(OrderBase):
|
|
|
19
19
|
|-----------------------|---------------------|---------------------------------------------------------------------------------|
|
|
20
20
|
| `ts_event_ns` | `int` | Time at which the fill was observed by the system, as UTC epoch nanoseconds. |
|
|
21
21
|
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
22
|
-
| `ts_broker_ns` | `int` | Time reported by the
|
|
22
|
+
| `ts_broker_ns` | `int` | Time reported by the brokers for the fill, as UTC epoch nanoseconds. |
|
|
23
23
|
| `associated_order_id` | `uuid.UUID` | Identifier of the order associated with the fill. |
|
|
24
24
|
| `broker_order_id` | `str` or `None` | Broker-assigned identifier of the order associated with the fill, if available. |
|
|
25
25
|
| `symbol` | `str` | Identifier of the traded instrument. |
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/requests/base.py
RENAMED
|
@@ -11,7 +11,7 @@ class RequestBase(events.EventBase):
|
|
|
11
11
|
"""
|
|
12
12
|
Base class for request events.
|
|
13
13
|
|
|
14
|
-
This class defines attributes common to all requests issued to a
|
|
14
|
+
This class defines attributes common to all requests issued to a brokers.
|
|
15
15
|
|
|
16
16
|
| Field | Type | Semantics |
|
|
17
17
|
|-------------------|-------------|----------------------------------------------------------------------------|
|
|
@@ -10,7 +10,7 @@ from onesecondtrader.events.requests.base import RequestBase
|
|
|
10
10
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
11
11
|
class OrderSubmissionRequest(RequestBase):
|
|
12
12
|
"""
|
|
13
|
-
Event representing a request to submit a new order to a
|
|
13
|
+
Event representing a request to submit a new order to a brokers.
|
|
14
14
|
|
|
15
15
|
The `system_order_id` is a unique identifier assigned by the system to the order submission request by default at object creation.
|
|
16
16
|
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/responses/base.py
RENAMED
|
@@ -10,16 +10,16 @@ from onesecondtrader import events
|
|
|
10
10
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
11
11
|
class ResponseBase(events.EventBase):
|
|
12
12
|
"""
|
|
13
|
-
Base class for
|
|
13
|
+
Base class for brokers response events.
|
|
14
14
|
|
|
15
|
-
This class defines attributes common to all responses received from a
|
|
15
|
+
This class defines attributes common to all responses received from a brokers in reaction to previously issued requests.
|
|
16
16
|
|
|
17
17
|
| Field | Type | Semantics |
|
|
18
18
|
|-----------------------|-------------|---------------------------------------------------------------------------------------|
|
|
19
19
|
| `ts_event_ns` | `int` | Time at which the response event was observed by the system, as UTC epoch nanoseconds.|
|
|
20
20
|
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
21
|
-
| `ts_broker_ns` | `int` | Time reported by the
|
|
22
|
-
| `associated_order_id` | `uuid.UUID` | Identifier of the order associated with the
|
|
21
|
+
| `ts_broker_ns` | `int` | Time reported by the brokers for the response, as UTC epoch nanoseconds. |
|
|
22
|
+
| `associated_order_id` | `uuid.UUID` | Identifier of the order associated with the brokers response. |
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
ts_broker_ns: int
|
|
@@ -9,13 +9,13 @@ from onesecondtrader.events.responses.base import ResponseBase
|
|
|
9
9
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
10
10
|
class CancellationAccepted(ResponseBase):
|
|
11
11
|
"""
|
|
12
|
-
Event indicating that the order cancellation has been acknowledged by the
|
|
12
|
+
Event indicating that the order cancellation has been acknowledged by the brokers and the order is no longer active at the execution venue.
|
|
13
13
|
|
|
14
14
|
| Field | Type | Semantics |
|
|
15
15
|
|-----------------------|-----------------|----------------------------------------------------------------------------------------|
|
|
16
16
|
| `ts_event_ns` | `int` | Time at which the cancellation was observed by the system, as UTC epoch nanoseconds. |
|
|
17
17
|
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
18
|
-
| `ts_broker_ns` | `int` | Time reported by the
|
|
18
|
+
| `ts_broker_ns` | `int` | Time reported by the brokers for the cancellation, as UTC epoch nanoseconds. |
|
|
19
19
|
| `associated_order_id` | `uuid.UUID` | Identifier of the cancelled order. |
|
|
20
20
|
| `broker_order_id` | `str` or `None` | Broker-assigned identifier of the cancelled order, if reported. |
|
|
21
21
|
"""
|
|
@@ -26,16 +26,16 @@ class CancellationAccepted(ResponseBase):
|
|
|
26
26
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
27
27
|
class CancellationRejected(ResponseBase):
|
|
28
28
|
"""
|
|
29
|
-
Event indicating that the order cancellation has been rejected by the
|
|
29
|
+
Event indicating that the order cancellation has been rejected by the brokers.
|
|
30
30
|
|
|
31
31
|
| Field | Type | Semantics |
|
|
32
32
|
|-----------------------|--------------------------------------|------------------------------------------------------------------------------------|
|
|
33
33
|
| `ts_event_ns` | `int` | Time at which the rejection was observed by the system, as UTC epoch nanoseconds. |
|
|
34
34
|
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
35
|
-
| `ts_broker_ns` | `int` | Time reported by the
|
|
35
|
+
| `ts_broker_ns` | `int` | Time reported by the brokers for the rejection, as UTC epoch nanoseconds. |
|
|
36
36
|
| `associated_order_id` | `uuid.UUID` | Identifier of the order associated with the rejected cancellation. |
|
|
37
37
|
| `rejection_reason` | `models.CancellationRejectionReason` | Canonical classification of the cancellation rejection cause. |
|
|
38
|
-
| `rejection_message` | `str` | Human-readable explanation provided by the
|
|
38
|
+
| `rejection_message` | `str` | Human-readable explanation provided by the brokers. |
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
rejection_reason: models.CancellationRejectionReason
|
|
@@ -10,13 +10,13 @@ from onesecondtrader.events.responses.base import ResponseBase
|
|
|
10
10
|
class ModificationAccepted(ResponseBase):
|
|
11
11
|
"""
|
|
12
12
|
Event indicating that the requested modification has been acknowledged by
|
|
13
|
-
the
|
|
13
|
+
the brokers and that the updated order parameters are active at the execution venue.
|
|
14
14
|
|
|
15
15
|
| Field | Type | Semantics |
|
|
16
16
|
|-----------------------|-----------------|----------------------------------------------------------------------------------------|
|
|
17
17
|
| `ts_event_ns` | `int` | Time at which the acceptance was observed by the system, as UTC epoch nanoseconds. |
|
|
18
18
|
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
19
|
-
| `ts_broker_ns` | `int` | Time reported by the
|
|
19
|
+
| `ts_broker_ns` | `int` | Time reported by the brokers for the modification acceptance, as UTC epoch nanoseconds. |
|
|
20
20
|
| `associated_order_id` | `uuid.UUID` | Identifier of the modified order. |
|
|
21
21
|
| `broker_order_id` | `str` or `None` | Broker-assigned identifier of the order after modification, if reported. |
|
|
22
22
|
"""
|
|
@@ -27,16 +27,16 @@ class ModificationAccepted(ResponseBase):
|
|
|
27
27
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
28
28
|
class ModificationRejected(ResponseBase):
|
|
29
29
|
"""
|
|
30
|
-
Event indicating that the requested modification has been rejected by the
|
|
30
|
+
Event indicating that the requested modification has been rejected by the brokers.
|
|
31
31
|
|
|
32
32
|
| Field | Type | Semantics |
|
|
33
33
|
|-----------------------|--------------------------------------|------------------------------------------------------------------------------------|
|
|
34
34
|
| `ts_event_ns` | `int` | Time at which the rejection was observed by the system, as UTC epoch nanoseconds. |
|
|
35
35
|
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
36
|
-
| `ts_broker_ns` | `int` | Time reported by the
|
|
36
|
+
| `ts_broker_ns` | `int` | Time reported by the brokers for the rejection, as UTC epoch nanoseconds. |
|
|
37
37
|
| `associated_order_id` | `uuid.UUID` | Identifier of the order associated with the rejected modification. |
|
|
38
38
|
| `rejection_reason` | `models.ModificationRejectionReason` | Canonical classification of the modification rejection cause. |
|
|
39
|
-
| `rejection_message` | `str` | Human-readable explanation provided by the
|
|
39
|
+
| `rejection_message` | `str` | Human-readable explanation provided by the brokers. |
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
42
|
rejection_reason: models.ModificationRejectionReason
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/responses/orders.py
RENAMED
|
@@ -9,13 +9,13 @@ from onesecondtrader.events.responses.base import ResponseBase
|
|
|
9
9
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
10
10
|
class OrderAccepted(ResponseBase):
|
|
11
11
|
"""
|
|
12
|
-
Event indicating that the order has been accepted by the
|
|
12
|
+
Event indicating that the order has been accepted by the brokers and is active at the execution venue.
|
|
13
13
|
|
|
14
14
|
| Field | Type | Semantics |
|
|
15
15
|
|-----------------------|-----------------|------------------------------------------------------------------------------------|
|
|
16
16
|
| `ts_event_ns` | `int` | Time at which the acceptance was observed by the system, as UTC epoch nanoseconds. |
|
|
17
17
|
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
18
|
-
| `ts_broker_ns` | `int` | Time reported by the
|
|
18
|
+
| `ts_broker_ns` | `int` | Time reported by the brokers for the acceptance, as UTC epoch nanoseconds. |
|
|
19
19
|
| `associated_order_id` | `uuid.UUID` | Identifier of the accepted order. |
|
|
20
20
|
| `broker_order_id` | `str` or `None` | Broker-assigned identifier of the accepted order. |
|
|
21
21
|
"""
|
|
@@ -26,16 +26,16 @@ class OrderAccepted(ResponseBase):
|
|
|
26
26
|
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
|
27
27
|
class OrderRejected(ResponseBase):
|
|
28
28
|
"""
|
|
29
|
-
Event indicating that the order has been rejected by the
|
|
29
|
+
Event indicating that the order has been rejected by the brokers.
|
|
30
30
|
|
|
31
31
|
| Field | Type | Semantics |
|
|
32
32
|
|-----------------------|-------------------------------|------------------------------------------------------------------------------------|
|
|
33
33
|
| `ts_event_ns` | `int` | Time at which the rejection was observed by the system, as UTC epoch nanoseconds. |
|
|
34
34
|
| `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
|
|
35
|
-
| `ts_broker_ns` | `int` | Time reported by the
|
|
35
|
+
| `ts_broker_ns` | `int` | Time reported by the brokers for the rejection, as UTC epoch nanoseconds. |
|
|
36
36
|
| `associated_order_id` | `uuid.UUID` | Identifier of the rejected order. |
|
|
37
37
|
| `rejection_reason` | `models.OrderRejectionReason` | Canonical classification of the rejection cause. |
|
|
38
|
-
| `rejection_message` | `str` | Human-readable explanation provided by the
|
|
38
|
+
| `rejection_message` | `str` | Human-readable explanation provided by the brokers. |
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
rejection_reason: models.OrderRejectionReason
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import collections
|
|
4
|
+
import threading
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from onesecondtrader import events
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from .subscriber import Subscriber
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EventBus:
|
|
14
|
+
"""
|
|
15
|
+
Event dispatch mechanism for propagating event objects to subscribers.
|
|
16
|
+
|
|
17
|
+
The event bus maintains subscriptions between subscribers and concrete event types.
|
|
18
|
+
Events published to the bus are synchronously delivered to all subscribers registered for the exact event type.
|
|
19
|
+
|
|
20
|
+
Subscription management and event publication are thread-safe.
|
|
21
|
+
Event delivery itself occurs outside the internal lock.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Initialize an empty event bus.
|
|
27
|
+
|
|
28
|
+
The bus starts with no registered subscribers and no active subscriptions.
|
|
29
|
+
"""
|
|
30
|
+
self._per_event_subscriptions: collections.defaultdict[
|
|
31
|
+
type[events.EventBase], set[Subscriber]
|
|
32
|
+
] = collections.defaultdict(set)
|
|
33
|
+
self._subscribers: set[Subscriber] = set()
|
|
34
|
+
self._lock: threading.Lock = threading.Lock()
|
|
35
|
+
|
|
36
|
+
def subscribe(
|
|
37
|
+
self,
|
|
38
|
+
subscriber: Subscriber,
|
|
39
|
+
event_type: type[events.EventBase],
|
|
40
|
+
) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Register a subscriber for a specific event type.
|
|
43
|
+
|
|
44
|
+
The subscriber will receive all future events whose concrete type matches `event_type`.
|
|
45
|
+
|
|
46
|
+
Parameters:
|
|
47
|
+
subscriber:
|
|
48
|
+
Object receiving published events.
|
|
49
|
+
event_type:
|
|
50
|
+
Concrete event class the subscriber is interested in.
|
|
51
|
+
"""
|
|
52
|
+
with self._lock:
|
|
53
|
+
self._subscribers.add(subscriber)
|
|
54
|
+
self._per_event_subscriptions[event_type].add(subscriber)
|
|
55
|
+
|
|
56
|
+
def unsubscribe(self, subscriber: Subscriber) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Remove a subscriber from all event subscriptions.
|
|
59
|
+
|
|
60
|
+
After unsubscription, the subscriber will no longer receive any events published on this bus.
|
|
61
|
+
|
|
62
|
+
Parameters:
|
|
63
|
+
subscriber:
|
|
64
|
+
Subscriber to remove.
|
|
65
|
+
"""
|
|
66
|
+
with self._lock:
|
|
67
|
+
for set_of_event_subscribers in self._per_event_subscriptions.values():
|
|
68
|
+
set_of_event_subscribers.discard(subscriber)
|
|
69
|
+
self._subscribers.discard(subscriber)
|
|
70
|
+
|
|
71
|
+
def publish(self, event: events.EventBase) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Publish an event to all subscribed listeners.
|
|
74
|
+
|
|
75
|
+
Subscribers are matched strictly by the concrete type of the event.
|
|
76
|
+
Parent classes and inheritance relationships are not considered.
|
|
77
|
+
|
|
78
|
+
Parameters:
|
|
79
|
+
event:
|
|
80
|
+
Event instance to dispatch.
|
|
81
|
+
"""
|
|
82
|
+
with self._lock:
|
|
83
|
+
subscribers = self._per_event_subscriptions[type(event)].copy()
|
|
84
|
+
for subscriber in subscribers:
|
|
85
|
+
subscriber.receive(event)
|
|
86
|
+
|
|
87
|
+
def wait_until_system_idle(self) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Block until all subscribers report an idle state.
|
|
90
|
+
|
|
91
|
+
This method delegates to each subscriber's `wait_until_idle` method and returns only after all subscribers have completed any pending work.
|
|
92
|
+
"""
|
|
93
|
+
with self._lock:
|
|
94
|
+
subscribers = self._subscribers.copy()
|
|
95
|
+
for subscriber in subscribers:
|
|
96
|
+
subscriber.wait_until_idle()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import queue
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from onesecondtrader import events, messaging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Subscriber(abc.ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for event bus subscribers.
|
|
11
|
+
|
|
12
|
+
A subscriber receives events from an event bus and processes them asynchronously in a dedicated worker thread.
|
|
13
|
+
Incoming events are queued and handled sequentially.
|
|
14
|
+
|
|
15
|
+
Subclasses implement `_on_event` to define event-specific behavior.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Initialize the subscriber and start its event-processing thread.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
event_bus:
|
|
24
|
+
Event bus used for subscribing to and publishing events.
|
|
25
|
+
"""
|
|
26
|
+
self._event_bus = event_bus
|
|
27
|
+
self._queue: queue.Queue[events.EventBase | None] = queue.Queue()
|
|
28
|
+
|
|
29
|
+
self._running: threading.Event = threading.Event()
|
|
30
|
+
self._running.set()
|
|
31
|
+
|
|
32
|
+
self._thread = threading.Thread(
|
|
33
|
+
target=self._event_loop, name=self.__class__.__name__
|
|
34
|
+
)
|
|
35
|
+
self._thread.start()
|
|
36
|
+
|
|
37
|
+
def receive(self, event: events.EventBase) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Receive an event from the event bus.
|
|
40
|
+
|
|
41
|
+
The event is enqueued for asynchronous processing if the subscriber is running.
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
event:
|
|
45
|
+
Event instance delivered by the event bus.
|
|
46
|
+
"""
|
|
47
|
+
if self._running.is_set():
|
|
48
|
+
self._queue.put(event)
|
|
49
|
+
|
|
50
|
+
def wait_until_idle(self) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Block until all queued events have been processed.
|
|
53
|
+
|
|
54
|
+
If the subscriber is not running, this method returns immediately.
|
|
55
|
+
"""
|
|
56
|
+
if not self._running.is_set():
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
self._queue.join()
|
|
60
|
+
|
|
61
|
+
def shutdown(self) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Shut down the subscriber and stop event processing.
|
|
64
|
+
|
|
65
|
+
The subscriber is unsubscribed from the event bus, its worker thread is signaled to terminate, and all pending events are processed before shutdown completes.
|
|
66
|
+
"""
|
|
67
|
+
if not self._running.is_set():
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
self._event_bus.unsubscribe(self)
|
|
71
|
+
self._running.clear()
|
|
72
|
+
self._queue.put(None)
|
|
73
|
+
|
|
74
|
+
if threading.current_thread() is not self._thread:
|
|
75
|
+
self._thread.join()
|
|
76
|
+
|
|
77
|
+
def _subscribe(self, *event_types: type[events.EventBase]) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Subscribe this subscriber to one or more event types.
|
|
80
|
+
|
|
81
|
+
Parameters:
|
|
82
|
+
*event_types:
|
|
83
|
+
Concrete event classes to subscribe to.
|
|
84
|
+
"""
|
|
85
|
+
for event_type in event_types:
|
|
86
|
+
self._event_bus.subscribe(self, event_type)
|
|
87
|
+
|
|
88
|
+
def _publish(self, event: events.EventBase) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Publish an event to the event bus.
|
|
91
|
+
|
|
92
|
+
Parameters:
|
|
93
|
+
event:
|
|
94
|
+
Event instance to publish.
|
|
95
|
+
"""
|
|
96
|
+
self._event_bus.publish(event)
|
|
97
|
+
|
|
98
|
+
def _event_loop(self) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Internal worker loop for processing queued events.
|
|
101
|
+
|
|
102
|
+
This method runs in a dedicated thread and should not be called directly.
|
|
103
|
+
"""
|
|
104
|
+
while True:
|
|
105
|
+
event = self._queue.get()
|
|
106
|
+
|
|
107
|
+
if event is None:
|
|
108
|
+
self._queue.task_done()
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
self._on_event(event)
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
self._on_exception(exc)
|
|
115
|
+
finally:
|
|
116
|
+
self._queue.task_done()
|
|
117
|
+
|
|
118
|
+
self._cleanup()
|
|
119
|
+
|
|
120
|
+
def _on_exception(self, exc: Exception) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Handle an exception raised during event processing.
|
|
123
|
+
|
|
124
|
+
Subclasses may override this method to implement logging or recovery behavior.
|
|
125
|
+
The default implementation ignores the exception.
|
|
126
|
+
|
|
127
|
+
Parameters:
|
|
128
|
+
exc:
|
|
129
|
+
Exception raised while processing an event.
|
|
130
|
+
"""
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
def _cleanup(self) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Perform cleanup after the event loop terminates.
|
|
136
|
+
|
|
137
|
+
Subclasses may override this method to release resources or emit shutdown notifications.
|
|
138
|
+
"""
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
@abc.abstractmethod
|
|
142
|
+
def _on_event(self, event: events.EventBase) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Handle a single event.
|
|
145
|
+
|
|
146
|
+
This method is invoked sequentially for each event received by the subscriber.
|
|
147
|
+
Implementations must not block indefinitely, as `wait_until_idle` relies on timely completion.
|
|
148
|
+
|
|
149
|
+
Parameters:
|
|
150
|
+
event:
|
|
151
|
+
Event instance to handle.
|
|
152
|
+
"""
|
|
153
|
+
...
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/models/rejection_reasons.py
RENAMED
|
@@ -8,7 +8,7 @@ class OrderRejectionReason(enum.Enum):
|
|
|
8
8
|
Enumeration of canonical order rejection reasons.
|
|
9
9
|
|
|
10
10
|
This enumeration defines the system-level classification of order rejection causes.
|
|
11
|
-
It provides a stable,
|
|
11
|
+
It provides a stable, brokers-agnostic taxonomy for programmatic handling of rejected orders.
|
|
12
12
|
|
|
13
13
|
| Value | Semantics |
|
|
14
14
|
|-----------|---------------------------------------------------------------------------|
|
|
@@ -22,8 +22,8 @@ class ModificationRejectionReason(enum.Enum):
|
|
|
22
22
|
"""
|
|
23
23
|
Enumeration of canonical order modification rejection reasons.
|
|
24
24
|
|
|
25
|
-
This enumeration defines the system-level classification of reasons for which an order modification request may be rejected by a
|
|
26
|
-
It provides a stable,
|
|
25
|
+
This enumeration defines the system-level classification of reasons for which an order modification request may be rejected by a brokers.
|
|
26
|
+
It provides a stable, brokers-agnostic taxonomy intended for programmatic handling and observability of modification rejections.
|
|
27
27
|
|
|
28
28
|
| Value | Semantics |
|
|
29
29
|
|-----------|----------------------------------------------------------------------------------|
|
|
@@ -37,8 +37,8 @@ class CancellationRejectionReason(enum.Enum):
|
|
|
37
37
|
"""
|
|
38
38
|
Enumeration of canonical order cancellation rejection reasons.
|
|
39
39
|
|
|
40
|
-
This enumeration defines the system-level classification of reasons for which an order cancellation request may be rejected by a
|
|
41
|
-
It provides a stable,
|
|
40
|
+
This enumeration defines the system-level classification of reasons for which an order cancellation request may be rejected by a brokers.
|
|
41
|
+
It provides a stable, brokers-agnostic taxonomy intended for programmatic handling and observability of cancellation rejections.
|
|
42
42
|
|
|
43
43
|
| Value | Semantics |
|
|
44
44
|
|-----------|----------------------------------------------------------------------------------|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/market/__init__.py
RENAMED
|
File without changes
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/market/bar_processed.py
RENAMED
|
File without changes
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/market/bar_received.py
RENAMED
|
File without changes
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/orders/__init__.py
RENAMED
|
File without changes
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/requests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/events/responses/__init__.py
RENAMED
|
File without changes
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/indicators/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/indicators/market_fields.py
RENAMED
|
File without changes
|
{onesecondtrader-0.49.0 → onesecondtrader-0.51.0}/src/onesecondtrader/indicators/moving_averages.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|