onesecondtrader 0.27.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.
@@ -1,5 +1,7 @@
1
1
  __all__ = [
2
2
  "BrokerBase",
3
+ "SimulatedBroker",
3
4
  ]
4
5
 
5
6
  from .base import BrokerBase
7
+ from .simulated import SimulatedBroker
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.27.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,18 +1,19 @@
1
1
  onesecondtrader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- onesecondtrader/brokers/__init__.py,sha256=n-idxbvvlPrAnbxdA7l6S3ecRVqgroTZ0XBkse8GamQ,62
2
+ onesecondtrader/brokers/__init__.py,sha256=YofzD0qlrfo_BzL6gwiHb9by7Webp36TQ_1O5EV0uzY,124
3
3
  onesecondtrader/brokers/base.py,sha256=b6Xq2gBUdy3RTpnUfpUiNSXwbuq_bRIjXJ-s89Xu7nE,1345
4
+ onesecondtrader/brokers/simulated.py,sha256=pJvZ7b76xAs-NBbOX_v78IJgVrdnuTLCadqj8kYQ5sg,12694
4
5
  onesecondtrader/events/__init__.py,sha256=IOlFRdiOXz93SpvkpKL8wr1954d_8olKSB1n9mlTL3Y,898
5
6
  onesecondtrader/events/bases.py,sha256=g-ykq2jgcitIAueRurUlqAq0jINQwuhSWi_khAniPHw,662
6
7
  onesecondtrader/events/market.py,sha256=IfHuIGfp_IUiw-dFay4c4RYmkoNzshxbhuWTglBqfN0,509
7
8
  onesecondtrader/events/requests.py,sha256=2KXwSckiar9-fy8wkN3vcSIeOkeBfeo_XhUhrNKEd2Q,831
8
9
  onesecondtrader/events/responses.py,sha256=w_BH1nkkPyxQjh30EXEVFcUGDoMprFc2PuAaqpVrQ8A,1436
9
10
  onesecondtrader/messaging/__init__.py,sha256=vMRDabHBgse_vZRTRFtnU8M8v2sY_o4pHjGzgu3hp3E,115
10
- onesecondtrader/messaging/eventbus.py,sha256=rTBDmtXKnUAVdOBeLyIn2pJmBLz9AszHGIcuOcVhEQI,1501
11
+ onesecondtrader/messaging/eventbus.py,sha256=Y8VbDZlEz8Q6KcCkfXRKsVIixsctBMRW1a5ANw297Ls,1576
11
12
  onesecondtrader/messaging/subscriber.py,sha256=ImpFmu5IstLXLoKVMaebmLp5MXN6225vHLdTL1ZOPvw,2106
12
13
  onesecondtrader/models/__init__.py,sha256=cH5xyniz78MQjM9_-fFdP1ZW6FFLTmayMwQauFO23bU,135
13
14
  onesecondtrader/models/data.py,sha256=TqUvTtjpmzTJT8ZdTYnlVoyI7Qck2IsseCazWZxJgD0,173
14
15
  onesecondtrader/models/orders.py,sha256=y6Ar-6fMqaOd_hRnRGvfWUF0Z13H_2hfTOW3ROOk0A8,254
15
- onesecondtrader-0.27.0.dist-info/METADATA,sha256=aHM_QsfmD8cIv5lqkvVFIveYEx5SxHsS1NjqaumOuXI,9682
16
- onesecondtrader-0.27.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
17
- onesecondtrader-0.27.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
18
- onesecondtrader-0.27.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,,