onesecondtrader 0.26.0__py3-none-any.whl → 0.28.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ __all__ = [
2
+ "BrokerBase",
3
+ "SimulatedBroker",
4
+ ]
5
+
6
+ from .base import BrokerBase
7
+ from .simulated import SimulatedBroker
@@ -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
+ )
@@ -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:
@@ -36,10 +36,12 @@ class Subscriber(abc.ABC):
36
36
  self._thread.join()
37
37
 
38
38
  def _subscribe(self, *event_types: type[events.bases.EventBase]) -> None:
39
- # Call as last line of subclass __init__ to avoid receiving events before fully initialized
40
39
  for event_type in event_types:
41
40
  self._event_bus.subscribe(self, event_type)
42
41
 
42
+ def _publish(self, event: events.bases.EventBase) -> None:
43
+ self._event_bus.publish(event)
44
+
43
45
  def _event_loop(self) -> None:
44
46
  while True:
45
47
  event = self._queue.get()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.26.0
3
+ Version: 0.28.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,16 +1,19 @@
1
1
  onesecondtrader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ onesecondtrader/brokers/__init__.py,sha256=YofzD0qlrfo_BzL6gwiHb9by7Webp36TQ_1O5EV0uzY,124
3
+ onesecondtrader/brokers/base.py,sha256=b6Xq2gBUdy3RTpnUfpUiNSXwbuq_bRIjXJ-s89Xu7nE,1345
4
+ onesecondtrader/brokers/simulated.py,sha256=pJvZ7b76xAs-NBbOX_v78IJgVrdnuTLCadqj8kYQ5sg,12694
2
5
  onesecondtrader/events/__init__.py,sha256=IOlFRdiOXz93SpvkpKL8wr1954d_8olKSB1n9mlTL3Y,898
3
6
  onesecondtrader/events/bases.py,sha256=g-ykq2jgcitIAueRurUlqAq0jINQwuhSWi_khAniPHw,662
4
7
  onesecondtrader/events/market.py,sha256=IfHuIGfp_IUiw-dFay4c4RYmkoNzshxbhuWTglBqfN0,509
5
8
  onesecondtrader/events/requests.py,sha256=2KXwSckiar9-fy8wkN3vcSIeOkeBfeo_XhUhrNKEd2Q,831
6
9
  onesecondtrader/events/responses.py,sha256=w_BH1nkkPyxQjh30EXEVFcUGDoMprFc2PuAaqpVrQ8A,1436
7
10
  onesecondtrader/messaging/__init__.py,sha256=vMRDabHBgse_vZRTRFtnU8M8v2sY_o4pHjGzgu3hp3E,115
8
- onesecondtrader/messaging/eventbus.py,sha256=rTBDmtXKnUAVdOBeLyIn2pJmBLz9AszHGIcuOcVhEQI,1501
9
- onesecondtrader/messaging/subscriber.py,sha256=Vg8po_h47qMPm_qN6OauIBiCE179cszjy3NCd493__s,2103
11
+ onesecondtrader/messaging/eventbus.py,sha256=Y8VbDZlEz8Q6KcCkfXRKsVIixsctBMRW1a5ANw297Ls,1576
12
+ onesecondtrader/messaging/subscriber.py,sha256=ImpFmu5IstLXLoKVMaebmLp5MXN6225vHLdTL1ZOPvw,2106
10
13
  onesecondtrader/models/__init__.py,sha256=cH5xyniz78MQjM9_-fFdP1ZW6FFLTmayMwQauFO23bU,135
11
14
  onesecondtrader/models/data.py,sha256=TqUvTtjpmzTJT8ZdTYnlVoyI7Qck2IsseCazWZxJgD0,173
12
15
  onesecondtrader/models/orders.py,sha256=y6Ar-6fMqaOd_hRnRGvfWUF0Z13H_2hfTOW3ROOk0A8,254
13
- onesecondtrader-0.26.0.dist-info/METADATA,sha256=4JFvirUCpSuaYhLlTru35NFBcLc0M0kjEMTjRZPqe5w,9682
14
- onesecondtrader-0.26.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
15
- onesecondtrader-0.26.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
16
- onesecondtrader-0.26.0.dist-info/RECORD,,
16
+ onesecondtrader-0.28.0.dist-info/METADATA,sha256=heQCUCvcEuwy8W9mzjn6fm9_wR8sGP1T7weYq9b19eQ,9682
17
+ onesecondtrader-0.28.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
18
+ onesecondtrader-0.28.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
19
+ onesecondtrader-0.28.0.dist-info/RECORD,,