onesecondtrader 0.27.0__tar.gz → 0.29.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.27.0 → onesecondtrader-0.29.0}/PKG-INFO +1 -1
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/pyproject.toml +1 -1
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/brokers/__init__.py +2 -0
- onesecondtrader-0.29.0/src/onesecondtrader/brokers/simulated.py +346 -0
- onesecondtrader-0.29.0/src/onesecondtrader/indicators/__init__.py +5 -0
- onesecondtrader-0.29.0/src/onesecondtrader/indicators/base.py +50 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/messaging/eventbus.py +1 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/LICENSE +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/README.md +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/__init__.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/brokers/base.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/events/__init__.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/events/bases.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/events/market.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/events/requests.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/events/responses.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/messaging/__init__.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/messaging/subscriber.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/models/__init__.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/models/data.py +0 -0
- {onesecondtrader-0.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/models/orders.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onesecondtrader
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.29.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.29.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,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,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import collections
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from onesecondtrader import events
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Indicator(abc.ABC):
|
|
13
|
+
def __init__(self, max_history: int = 100, plot_at: int = 99) -> None:
|
|
14
|
+
self._lock = threading.Lock()
|
|
15
|
+
self._max_history = max(1, int(max_history))
|
|
16
|
+
# Keyed by symbol only - each strategy subscribes to one timeframe, so the indicator only sees bars from that timeframe.
|
|
17
|
+
self._history: dict[str, collections.deque[float]] = {}
|
|
18
|
+
# 0 = main price chart, 1-98 = subcharts, 99 = no plot
|
|
19
|
+
self._plot_at = plot_at
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
def name(self) -> str:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
def _compute_indicator(self, incoming_bar: events.BarReceived) -> float:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def update(self, incoming_bar: events.BarReceived) -> None:
|
|
31
|
+
symbol = incoming_bar.symbol
|
|
32
|
+
value = self._compute_indicator(incoming_bar)
|
|
33
|
+
with self._lock:
|
|
34
|
+
if symbol not in self._history:
|
|
35
|
+
self._history[symbol] = collections.deque(maxlen=self._max_history)
|
|
36
|
+
self._history[symbol].append(value)
|
|
37
|
+
|
|
38
|
+
def latest(self, symbol: str) -> float:
|
|
39
|
+
with self._lock:
|
|
40
|
+
history = self._history.get(symbol, collections.deque())
|
|
41
|
+
return history[-1] if history else np.nan
|
|
42
|
+
|
|
43
|
+
def history(self, symbol: str) -> collections.deque[float]:
|
|
44
|
+
with self._lock:
|
|
45
|
+
h = self._history.get(symbol, collections.deque())
|
|
46
|
+
return collections.deque(h, maxlen=self._max_history)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def plot_at(self) -> int:
|
|
50
|
+
return self._plot_at
|
|
@@ -34,6 +34,7 @@ class EventBus:
|
|
|
34
34
|
self._subscribers.discard(subscriber)
|
|
35
35
|
|
|
36
36
|
def publish(self, event: events.bases.EventBase) -> None:
|
|
37
|
+
# Intentionally matches exact event types only, not parent classes
|
|
37
38
|
with self._lock:
|
|
38
39
|
subscribers = self._per_event_subscriptions[type(event)].copy()
|
|
39
40
|
for subscriber in subscribers:
|
|
File without changes
|
|
File without changes
|
|
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.27.0 → onesecondtrader-0.29.0}/src/onesecondtrader/messaging/subscriber.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|