onesecondtrader 0.22.0__tar.gz → 0.33.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.22.0 → onesecondtrader-0.33.0}/PKG-INFO +1 -1
- {onesecondtrader-0.22.0 → onesecondtrader-0.33.0}/pyproject.toml +1 -1
- onesecondtrader-0.33.0/src/onesecondtrader/__init__.py +49 -0
- onesecondtrader-0.33.0/src/onesecondtrader/brokers/__init__.py +7 -0
- onesecondtrader-0.33.0/src/onesecondtrader/brokers/base.py +39 -0
- onesecondtrader-0.33.0/src/onesecondtrader/brokers/simulated.py +346 -0
- onesecondtrader-0.33.0/src/onesecondtrader/events/__init__.py +33 -0
- onesecondtrader-0.33.0/src/onesecondtrader/events/bases.py +29 -0
- onesecondtrader-0.33.0/src/onesecondtrader/events/market.py +22 -0
- onesecondtrader-0.33.0/src/onesecondtrader/events/requests.py +31 -0
- onesecondtrader-0.33.0/src/onesecondtrader/events/responses.py +54 -0
- onesecondtrader-0.33.0/src/onesecondtrader/indicators/__init__.py +13 -0
- onesecondtrader-0.33.0/src/onesecondtrader/indicators/averages.py +56 -0
- onesecondtrader-0.33.0/src/onesecondtrader/indicators/bar.py +47 -0
- onesecondtrader-0.33.0/src/onesecondtrader/indicators/base.py +60 -0
- onesecondtrader-0.33.0/src/onesecondtrader/messaging/__init__.py +7 -0
- onesecondtrader-0.33.0/src/onesecondtrader/messaging/eventbus.py +47 -0
- onesecondtrader-0.33.0/src/onesecondtrader/messaging/subscriber.py +69 -0
- onesecondtrader-0.33.0/src/onesecondtrader/models/__init__.py +12 -0
- onesecondtrader-0.33.0/src/onesecondtrader/models/data.py +18 -0
- onesecondtrader-0.33.0/src/onesecondtrader/models/orders.py +15 -0
- onesecondtrader-0.33.0/src/onesecondtrader/models/records.py +32 -0
- onesecondtrader-0.33.0/src/onesecondtrader/strategies/__init__.py +7 -0
- onesecondtrader-0.33.0/src/onesecondtrader/strategies/base.py +318 -0
- onesecondtrader-0.33.0/src/onesecondtrader/strategies/sma_crossover.py +35 -0
- onesecondtrader-0.22.0/src/onesecondtrader/__init__.py +0 -0
- onesecondtrader-0.22.0/src/onesecondtrader/brokers.py +0 -92
- onesecondtrader-0.22.0/src/onesecondtrader/core.py +0 -260
- onesecondtrader-0.22.0/src/onesecondtrader/datafeeds.py +0 -173
- onesecondtrader-0.22.0/src/onesecondtrader/indicators.py +0 -106
- {onesecondtrader-0.22.0 → onesecondtrader-0.33.0}/LICENSE +0 -0
- {onesecondtrader-0.22.0 → onesecondtrader-0.33.0}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onesecondtrader
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.33.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.33.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,49 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"BarPeriod",
|
|
3
|
+
"BarProcessed",
|
|
4
|
+
"BarReceived",
|
|
5
|
+
"BrokerBase",
|
|
6
|
+
"Close",
|
|
7
|
+
"FillRecord",
|
|
8
|
+
"High",
|
|
9
|
+
"Indicator",
|
|
10
|
+
"InputSource",
|
|
11
|
+
"Low",
|
|
12
|
+
"Open",
|
|
13
|
+
"OrderFilled",
|
|
14
|
+
"OrderRecord",
|
|
15
|
+
"OrderSide",
|
|
16
|
+
"OrderSubmission",
|
|
17
|
+
"OrderType",
|
|
18
|
+
"SimpleMovingAverage",
|
|
19
|
+
"SimulatedBroker",
|
|
20
|
+
"SMACrossover",
|
|
21
|
+
"StrategyBase",
|
|
22
|
+
"Volume",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
from onesecondtrader.brokers import BrokerBase, SimulatedBroker
|
|
26
|
+
from onesecondtrader.events import (
|
|
27
|
+
BarProcessed,
|
|
28
|
+
BarReceived,
|
|
29
|
+
OrderFilled,
|
|
30
|
+
OrderSubmission,
|
|
31
|
+
)
|
|
32
|
+
from onesecondtrader.indicators import (
|
|
33
|
+
Close,
|
|
34
|
+
High,
|
|
35
|
+
Indicator,
|
|
36
|
+
Low,
|
|
37
|
+
Open,
|
|
38
|
+
SimpleMovingAverage,
|
|
39
|
+
Volume,
|
|
40
|
+
)
|
|
41
|
+
from onesecondtrader.models import (
|
|
42
|
+
BarPeriod,
|
|
43
|
+
FillRecord,
|
|
44
|
+
InputSource,
|
|
45
|
+
OrderRecord,
|
|
46
|
+
OrderSide,
|
|
47
|
+
OrderType,
|
|
48
|
+
)
|
|
49
|
+
from onesecondtrader.strategies import SMACrossover, StrategyBase
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
|
|
3
|
+
from onesecondtrader import events, messaging
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BrokerBase(messaging.Subscriber):
|
|
7
|
+
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
8
|
+
super().__init__(event_bus)
|
|
9
|
+
self._subscribe(
|
|
10
|
+
events.requests.OrderSubmission,
|
|
11
|
+
events.requests.OrderCancellation,
|
|
12
|
+
events.requests.OrderModification,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def _on_event(self, event: events.bases.EventBase) -> None:
|
|
16
|
+
match event:
|
|
17
|
+
case events.requests.OrderSubmission() as submit_order:
|
|
18
|
+
self._on_submit_order(submit_order)
|
|
19
|
+
case events.requests.OrderCancellation() as cancel_order:
|
|
20
|
+
self._on_cancel_order(cancel_order)
|
|
21
|
+
case events.requests.OrderModification() as modify_order:
|
|
22
|
+
self._on_modify_order(modify_order)
|
|
23
|
+
case _:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
def _on_submit_order(self, event: events.requests.OrderSubmission) -> None:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abc.abstractmethod
|
|
31
|
+
def _on_cancel_order(self, event: events.requests.OrderCancellation) -> None:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abc.abstractmethod
|
|
35
|
+
def _on_modify_order(self, event: events.requests.OrderModification) -> None:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def _respond(self, response_event: events.bases.BrokerResponseEvent) -> None:
|
|
39
|
+
self._publish(response_event)
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from onesecondtrader import events, messaging, models
|
|
7
|
+
from .base import BrokerBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass
|
|
11
|
+
class _PendingOrder:
|
|
12
|
+
# Order state tracked by the broker, distinct from the OrderSubmission event
|
|
13
|
+
order_id: uuid.UUID
|
|
14
|
+
symbol: str
|
|
15
|
+
order_type: models.OrderType
|
|
16
|
+
side: models.OrderSide
|
|
17
|
+
quantity: float
|
|
18
|
+
limit_price: float | None = None
|
|
19
|
+
stop_price: float | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SimulatedBroker(BrokerBase):
|
|
23
|
+
commission_per_unit: float = 0.0
|
|
24
|
+
minimum_commission_per_order: float = 0.0
|
|
25
|
+
|
|
26
|
+
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
27
|
+
self._pending_market_orders: dict[uuid.UUID, _PendingOrder] = {}
|
|
28
|
+
self._pending_limit_orders: dict[uuid.UUID, _PendingOrder] = {}
|
|
29
|
+
self._pending_stop_orders: dict[uuid.UUID, _PendingOrder] = {}
|
|
30
|
+
self._pending_stop_limit_orders: dict[uuid.UUID, _PendingOrder] = {}
|
|
31
|
+
|
|
32
|
+
super().__init__(event_bus)
|
|
33
|
+
self._subscribe(events.BarReceived)
|
|
34
|
+
|
|
35
|
+
def _on_event(self, event: events.EventBase) -> None:
|
|
36
|
+
match event:
|
|
37
|
+
case events.BarReceived() as bar:
|
|
38
|
+
self._on_bar(bar)
|
|
39
|
+
case _:
|
|
40
|
+
super()._on_event(event)
|
|
41
|
+
|
|
42
|
+
def _on_bar(self, event: events.BarReceived) -> None:
|
|
43
|
+
self._process_market_orders(event)
|
|
44
|
+
self._process_stop_orders(event)
|
|
45
|
+
self._process_stop_limit_orders(event)
|
|
46
|
+
self._process_limit_orders(event)
|
|
47
|
+
|
|
48
|
+
def _process_market_orders(self, event: events.BarReceived) -> None:
|
|
49
|
+
for order_id, order in list(self._pending_market_orders.items()):
|
|
50
|
+
if order.symbol != event.symbol:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
self._publish(
|
|
54
|
+
events.OrderFilled(
|
|
55
|
+
ts_event=event.ts_event,
|
|
56
|
+
ts_broker=event.ts_event,
|
|
57
|
+
associated_order_id=order.order_id,
|
|
58
|
+
symbol=order.symbol,
|
|
59
|
+
side=order.side,
|
|
60
|
+
quantity_filled=order.quantity,
|
|
61
|
+
fill_price=event.open,
|
|
62
|
+
commission=max(
|
|
63
|
+
order.quantity * self.commission_per_unit,
|
|
64
|
+
self.minimum_commission_per_order,
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
del self._pending_market_orders[order_id]
|
|
69
|
+
|
|
70
|
+
def _process_stop_orders(self, event: events.BarReceived) -> None:
|
|
71
|
+
for order_id, order in list(self._pending_stop_orders.items()):
|
|
72
|
+
if order.symbol != event.symbol:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# This is for mypy, it has already been validated on submission
|
|
76
|
+
assert order.stop_price is not None
|
|
77
|
+
|
|
78
|
+
triggered = False
|
|
79
|
+
match order.side:
|
|
80
|
+
case models.OrderSide.BUY:
|
|
81
|
+
triggered = event.high >= order.stop_price
|
|
82
|
+
case models.OrderSide.SELL:
|
|
83
|
+
triggered = event.low <= order.stop_price
|
|
84
|
+
|
|
85
|
+
if not triggered:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
fill_price = 0.0
|
|
89
|
+
match order.side:
|
|
90
|
+
case models.OrderSide.BUY:
|
|
91
|
+
fill_price = max(order.stop_price, event.open)
|
|
92
|
+
case models.OrderSide.SELL:
|
|
93
|
+
fill_price = min(order.stop_price, event.open)
|
|
94
|
+
|
|
95
|
+
self._publish(
|
|
96
|
+
events.OrderFilled(
|
|
97
|
+
ts_event=event.ts_event,
|
|
98
|
+
ts_broker=event.ts_event,
|
|
99
|
+
associated_order_id=order.order_id,
|
|
100
|
+
symbol=order.symbol,
|
|
101
|
+
side=order.side,
|
|
102
|
+
quantity_filled=order.quantity,
|
|
103
|
+
fill_price=fill_price,
|
|
104
|
+
commission=max(
|
|
105
|
+
order.quantity * self.commission_per_unit,
|
|
106
|
+
self.minimum_commission_per_order,
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
del self._pending_stop_orders[order_id]
|
|
111
|
+
|
|
112
|
+
def _process_stop_limit_orders(self, event: events.BarReceived) -> None:
|
|
113
|
+
for order_id, order in list(self._pending_stop_limit_orders.items()):
|
|
114
|
+
if order.symbol != event.symbol:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# This is for mypy, it has already been validated on submission
|
|
118
|
+
assert order.stop_price is not None
|
|
119
|
+
|
|
120
|
+
triggered = False
|
|
121
|
+
match order.side:
|
|
122
|
+
case models.OrderSide.BUY:
|
|
123
|
+
triggered = event.high >= order.stop_price
|
|
124
|
+
case models.OrderSide.SELL:
|
|
125
|
+
triggered = event.low <= order.stop_price
|
|
126
|
+
|
|
127
|
+
if not triggered:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
limit_order = dataclasses.replace(order, order_type=models.OrderType.LIMIT)
|
|
131
|
+
self._pending_limit_orders[order_id] = limit_order
|
|
132
|
+
del self._pending_stop_limit_orders[order_id]
|
|
133
|
+
|
|
134
|
+
def _process_limit_orders(self, event: events.BarReceived) -> None:
|
|
135
|
+
for order_id, order in list(self._pending_limit_orders.items()):
|
|
136
|
+
if order.symbol != event.symbol:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
# This is for mypy, it has already been validated on submission
|
|
140
|
+
assert order.limit_price is not None
|
|
141
|
+
|
|
142
|
+
triggered = False
|
|
143
|
+
match order.side:
|
|
144
|
+
case models.OrderSide.BUY:
|
|
145
|
+
triggered = event.low <= order.limit_price
|
|
146
|
+
case models.OrderSide.SELL:
|
|
147
|
+
triggered = event.high >= order.limit_price
|
|
148
|
+
|
|
149
|
+
if not triggered:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
fill_price = 0.0
|
|
153
|
+
match order.side:
|
|
154
|
+
case models.OrderSide.BUY:
|
|
155
|
+
fill_price = min(order.limit_price, event.open)
|
|
156
|
+
case models.OrderSide.SELL:
|
|
157
|
+
fill_price = max(order.limit_price, event.open)
|
|
158
|
+
|
|
159
|
+
self._publish(
|
|
160
|
+
events.OrderFilled(
|
|
161
|
+
ts_event=event.ts_event,
|
|
162
|
+
ts_broker=event.ts_event,
|
|
163
|
+
associated_order_id=order.order_id,
|
|
164
|
+
symbol=order.symbol,
|
|
165
|
+
side=order.side,
|
|
166
|
+
quantity_filled=order.quantity,
|
|
167
|
+
fill_price=fill_price,
|
|
168
|
+
commission=max(
|
|
169
|
+
order.quantity * self.commission_per_unit,
|
|
170
|
+
self.minimum_commission_per_order,
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
del self._pending_limit_orders[order_id]
|
|
175
|
+
|
|
176
|
+
def _reject_if_invalid_submission(self, event: events.OrderSubmission) -> bool:
|
|
177
|
+
is_invalid = event.quantity <= 0
|
|
178
|
+
|
|
179
|
+
match event.order_type:
|
|
180
|
+
case models.OrderType.LIMIT:
|
|
181
|
+
is_invalid = (
|
|
182
|
+
is_invalid or event.limit_price is None or event.limit_price <= 0
|
|
183
|
+
)
|
|
184
|
+
case models.OrderType.STOP:
|
|
185
|
+
is_invalid = (
|
|
186
|
+
is_invalid or event.stop_price is None or event.stop_price <= 0
|
|
187
|
+
)
|
|
188
|
+
case models.OrderType.STOP_LIMIT:
|
|
189
|
+
is_invalid = is_invalid or (
|
|
190
|
+
event.limit_price is None
|
|
191
|
+
or event.limit_price <= 0
|
|
192
|
+
or event.stop_price is None
|
|
193
|
+
or event.stop_price <= 0
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if is_invalid:
|
|
197
|
+
# Use event timestamp to maintain simulated time consistency in backtesting
|
|
198
|
+
self._publish(
|
|
199
|
+
events.OrderSubmissionRejected(
|
|
200
|
+
ts_event=event.ts_event,
|
|
201
|
+
ts_broker=event.ts_event,
|
|
202
|
+
associated_order_id=event.system_order_id,
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return is_invalid
|
|
207
|
+
|
|
208
|
+
def _on_submit_order(self, event: events.OrderSubmission) -> None:
|
|
209
|
+
if self._reject_if_invalid_submission(event):
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
order = _PendingOrder(
|
|
213
|
+
order_id=event.system_order_id,
|
|
214
|
+
symbol=event.symbol,
|
|
215
|
+
order_type=event.order_type,
|
|
216
|
+
side=event.side,
|
|
217
|
+
quantity=event.quantity,
|
|
218
|
+
limit_price=event.limit_price,
|
|
219
|
+
stop_price=event.stop_price,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
match order.order_type:
|
|
223
|
+
case models.OrderType.MARKET:
|
|
224
|
+
self._pending_market_orders[order.order_id] = order
|
|
225
|
+
case models.OrderType.LIMIT:
|
|
226
|
+
self._pending_limit_orders[order.order_id] = order
|
|
227
|
+
case models.OrderType.STOP:
|
|
228
|
+
self._pending_stop_orders[order.order_id] = order
|
|
229
|
+
case models.OrderType.STOP_LIMIT:
|
|
230
|
+
self._pending_stop_limit_orders[order.order_id] = order
|
|
231
|
+
|
|
232
|
+
# Use event timestamp to maintain simulated time consistency in backtesting
|
|
233
|
+
self._publish(
|
|
234
|
+
events.OrderSubmissionAccepted(
|
|
235
|
+
ts_event=event.ts_event,
|
|
236
|
+
ts_broker=event.ts_event,
|
|
237
|
+
associated_order_id=order.order_id,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _on_cancel_order(self, event: events.OrderCancellation) -> None:
|
|
242
|
+
order_id = event.system_order_id
|
|
243
|
+
|
|
244
|
+
removed = False
|
|
245
|
+
for pending_orders in (
|
|
246
|
+
self._pending_market_orders,
|
|
247
|
+
self._pending_limit_orders,
|
|
248
|
+
self._pending_stop_orders,
|
|
249
|
+
self._pending_stop_limit_orders,
|
|
250
|
+
):
|
|
251
|
+
if order_id in pending_orders:
|
|
252
|
+
del pending_orders[order_id]
|
|
253
|
+
removed = True
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
# Use event timestamp to maintain simulated time consistency in backtesting
|
|
257
|
+
if removed:
|
|
258
|
+
self._publish(
|
|
259
|
+
events.OrderCancellationAccepted(
|
|
260
|
+
ts_event=event.ts_event,
|
|
261
|
+
ts_broker=event.ts_event,
|
|
262
|
+
associated_order_id=order_id,
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
self._publish(
|
|
267
|
+
events.OrderCancellationRejected(
|
|
268
|
+
ts_event=event.ts_event,
|
|
269
|
+
ts_broker=event.ts_event,
|
|
270
|
+
associated_order_id=order_id,
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def _reject_if_invalid_modification(self, event: events.OrderModification) -> bool:
|
|
275
|
+
is_invalid = (
|
|
276
|
+
(event.quantity is not None and event.quantity <= 0)
|
|
277
|
+
or (event.limit_price is not None and event.limit_price <= 0)
|
|
278
|
+
or (event.stop_price is not None and event.stop_price <= 0)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if is_invalid:
|
|
282
|
+
# Use event timestamp to maintain simulated time consistency in backtesting
|
|
283
|
+
self._publish(
|
|
284
|
+
events.OrderModificationRejected(
|
|
285
|
+
ts_event=event.ts_event,
|
|
286
|
+
ts_broker=event.ts_event,
|
|
287
|
+
associated_order_id=event.system_order_id,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return is_invalid
|
|
292
|
+
|
|
293
|
+
def _on_modify_order(self, event: events.OrderModification) -> None:
|
|
294
|
+
if self._reject_if_invalid_modification(event):
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
order_id = event.system_order_id
|
|
298
|
+
|
|
299
|
+
for pending_orders in (
|
|
300
|
+
self._pending_market_orders,
|
|
301
|
+
self._pending_limit_orders,
|
|
302
|
+
self._pending_stop_orders,
|
|
303
|
+
self._pending_stop_limit_orders,
|
|
304
|
+
):
|
|
305
|
+
if order_id in pending_orders:
|
|
306
|
+
order = pending_orders[order_id]
|
|
307
|
+
|
|
308
|
+
new_quantity = (
|
|
309
|
+
event.quantity if event.quantity is not None else order.quantity
|
|
310
|
+
)
|
|
311
|
+
new_limit_price = (
|
|
312
|
+
event.limit_price
|
|
313
|
+
if event.limit_price is not None
|
|
314
|
+
else order.limit_price
|
|
315
|
+
)
|
|
316
|
+
new_stop_price = (
|
|
317
|
+
event.stop_price
|
|
318
|
+
if event.stop_price is not None
|
|
319
|
+
else order.stop_price
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
pending_orders[order_id] = dataclasses.replace(
|
|
323
|
+
order,
|
|
324
|
+
quantity=new_quantity,
|
|
325
|
+
limit_price=new_limit_price,
|
|
326
|
+
stop_price=new_stop_price,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Use event timestamp to maintain simulated time consistency in backtesting
|
|
330
|
+
self._publish(
|
|
331
|
+
events.OrderModificationAccepted(
|
|
332
|
+
ts_event=event.ts_event,
|
|
333
|
+
ts_broker=event.ts_event,
|
|
334
|
+
associated_order_id=order_id,
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# Use event timestamp to maintain simulated time consistency in backtesting
|
|
340
|
+
self._publish(
|
|
341
|
+
events.OrderModificationRejected(
|
|
342
|
+
ts_event=event.ts_event,
|
|
343
|
+
ts_broker=event.ts_event,
|
|
344
|
+
associated_order_id=order_id,
|
|
345
|
+
)
|
|
346
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"EventBase",
|
|
3
|
+
"MarketEvent",
|
|
4
|
+
"BrokerRequestEvent",
|
|
5
|
+
"BrokerResponseEvent",
|
|
6
|
+
"BarReceived",
|
|
7
|
+
"BarProcessed",
|
|
8
|
+
"OrderSubmission",
|
|
9
|
+
"OrderModification",
|
|
10
|
+
"OrderCancellation",
|
|
11
|
+
"OrderSubmissionAccepted",
|
|
12
|
+
"OrderSubmissionRejected",
|
|
13
|
+
"OrderModificationAccepted",
|
|
14
|
+
"OrderModificationRejected",
|
|
15
|
+
"OrderCancellationAccepted",
|
|
16
|
+
"OrderCancellationRejected",
|
|
17
|
+
"OrderFilled",
|
|
18
|
+
"OrderExpired",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
from .bases import EventBase, MarketEvent, BrokerRequestEvent, BrokerResponseEvent
|
|
22
|
+
from .market import BarReceived, BarProcessed
|
|
23
|
+
from .requests import OrderSubmission, OrderModification, OrderCancellation
|
|
24
|
+
from .responses import (
|
|
25
|
+
OrderSubmissionAccepted,
|
|
26
|
+
OrderSubmissionRejected,
|
|
27
|
+
OrderModificationAccepted,
|
|
28
|
+
OrderModificationRejected,
|
|
29
|
+
OrderCancellationAccepted,
|
|
30
|
+
OrderCancellationRejected,
|
|
31
|
+
OrderFilled,
|
|
32
|
+
OrderExpired,
|
|
33
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
9
|
+
class EventBase:
|
|
10
|
+
ts_event: pd.Timestamp
|
|
11
|
+
ts_created: pd.Timestamp = dataclasses.field(
|
|
12
|
+
default_factory=lambda: pd.Timestamp.now(tz="UTC")
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
17
|
+
class MarketEvent(EventBase):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
22
|
+
class BrokerRequestEvent(EventBase):
|
|
23
|
+
system_order_id: uuid.UUID
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
27
|
+
class BrokerResponseEvent(EventBase):
|
|
28
|
+
ts_broker: pd.Timestamp
|
|
29
|
+
associated_order_id: uuid.UUID
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
|
|
5
|
+
from onesecondtrader import models
|
|
6
|
+
from . import bases
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
10
|
+
class BarReceived(bases.MarketEvent):
|
|
11
|
+
symbol: str
|
|
12
|
+
bar_period: models.data.BarPeriod
|
|
13
|
+
open: float
|
|
14
|
+
high: float
|
|
15
|
+
low: float
|
|
16
|
+
close: float
|
|
17
|
+
volume: int | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
21
|
+
class BarProcessed(BarReceived):
|
|
22
|
+
indicators: dict[str, float] = dataclasses.field(default_factory=dict)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from onesecondtrader import models
|
|
7
|
+
from . import bases
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
11
|
+
class OrderSubmission(bases.BrokerRequestEvent):
|
|
12
|
+
system_order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
13
|
+
symbol: str
|
|
14
|
+
order_type: models.orders.OrderType
|
|
15
|
+
side: models.orders.OrderSide
|
|
16
|
+
quantity: float
|
|
17
|
+
limit_price: float | None = None
|
|
18
|
+
stop_price: float | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
22
|
+
class OrderCancellation(bases.BrokerRequestEvent):
|
|
23
|
+
symbol: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
27
|
+
class OrderModification(bases.BrokerRequestEvent):
|
|
28
|
+
symbol: str
|
|
29
|
+
quantity: float | None = None
|
|
30
|
+
limit_price: float | None = None
|
|
31
|
+
stop_price: float | None = None
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from onesecondtrader import models
|
|
7
|
+
from . import bases
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
11
|
+
class OrderSubmissionAccepted(bases.BrokerResponseEvent):
|
|
12
|
+
broker_order_id: str | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
16
|
+
class OrderSubmissionRejected(bases.BrokerResponseEvent):
|
|
17
|
+
reason: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
21
|
+
class OrderModificationAccepted(bases.BrokerResponseEvent):
|
|
22
|
+
broker_order_id: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
26
|
+
class OrderModificationRejected(bases.BrokerResponseEvent):
|
|
27
|
+
reason: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
31
|
+
class OrderCancellationAccepted(bases.BrokerResponseEvent):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
36
|
+
class OrderCancellationRejected(bases.BrokerResponseEvent):
|
|
37
|
+
reason: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
41
|
+
class OrderFilled(bases.BrokerResponseEvent):
|
|
42
|
+
fill_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
43
|
+
broker_fill_id: str | None = None
|
|
44
|
+
symbol: str
|
|
45
|
+
side: models.orders.OrderSide
|
|
46
|
+
quantity_filled: float
|
|
47
|
+
fill_price: float
|
|
48
|
+
commission: float
|
|
49
|
+
exchange: str = "SIMULATED"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
53
|
+
class OrderExpired(bases.BrokerResponseEvent):
|
|
54
|
+
pass
|