onesecondtrader 0.34.0__tar.gz → 0.36.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.
Files changed (62) hide show
  1. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/PKG-INFO +1 -1
  2. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/pyproject.toml +1 -1
  3. onesecondtrader-0.36.0/src/onesecondtrader/connectors/__init__.py +2 -0
  4. onesecondtrader-0.36.0/src/onesecondtrader/connectors/brokers/__init__.py +3 -0
  5. onesecondtrader-0.36.0/src/onesecondtrader/connectors/brokers/simulated.py +346 -0
  6. onesecondtrader-0.36.0/src/onesecondtrader/connectors/datafeeds/__init__.py +4 -0
  7. onesecondtrader-0.36.0/src/onesecondtrader/connectors/datafeeds/base.py +19 -0
  8. onesecondtrader-0.36.0/src/onesecondtrader/connectors/datafeeds/simulated.py +99 -0
  9. onesecondtrader-0.36.0/src/onesecondtrader/core/__init__.py +6 -0
  10. onesecondtrader-0.36.0/src/onesecondtrader/core/brokers/__init__.py +3 -0
  11. onesecondtrader-0.36.0/src/onesecondtrader/core/brokers/base.py +39 -0
  12. onesecondtrader-0.36.0/src/onesecondtrader/core/events/market.py +22 -0
  13. onesecondtrader-0.36.0/src/onesecondtrader/core/events/requests.py +31 -0
  14. onesecondtrader-0.36.0/src/onesecondtrader/core/events/responses.py +54 -0
  15. onesecondtrader-0.36.0/src/onesecondtrader/core/indicators/averages.py +56 -0
  16. onesecondtrader-0.36.0/src/onesecondtrader/core/indicators/bar.py +47 -0
  17. onesecondtrader-0.36.0/src/onesecondtrader/core/indicators/base.py +60 -0
  18. onesecondtrader-0.36.0/src/onesecondtrader/core/messaging/eventbus.py +47 -0
  19. onesecondtrader-0.36.0/src/onesecondtrader/core/messaging/subscriber.py +69 -0
  20. onesecondtrader-0.36.0/src/onesecondtrader/core/strategies/__init__.py +7 -0
  21. onesecondtrader-0.36.0/src/onesecondtrader/core/strategies/base.py +318 -0
  22. onesecondtrader-0.36.0/src/onesecondtrader/core/strategies/examples.py +35 -0
  23. onesecondtrader-0.36.0/src/onesecondtrader/dashboard/__init__.py +0 -0
  24. onesecondtrader-0.36.0/src/onesecondtrader/events/__init__.py +33 -0
  25. onesecondtrader-0.36.0/src/onesecondtrader/events/bases.py +29 -0
  26. onesecondtrader-0.36.0/src/onesecondtrader/indicators/__init__.py +13 -0
  27. onesecondtrader-0.36.0/src/onesecondtrader/messaging/__init__.py +7 -0
  28. onesecondtrader-0.36.0/src/onesecondtrader/models/__init__.py +12 -0
  29. onesecondtrader-0.36.0/src/onesecondtrader/models/data.py +18 -0
  30. onesecondtrader-0.36.0/src/onesecondtrader/models/orders.py +15 -0
  31. onesecondtrader-0.36.0/src/onesecondtrader/models/records.py +32 -0
  32. onesecondtrader-0.36.0/src/onesecondtrader/secmaster/__init__.py +5 -0
  33. onesecondtrader-0.36.0/src/onesecondtrader/secmaster/schema.sql +46 -0
  34. onesecondtrader-0.36.0/src/onesecondtrader/secmaster/utils.py +26 -0
  35. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/LICENSE +0 -0
  36. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/README.md +0 -0
  37. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/__init__.py +0 -0
  38. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/brokers/__init__.py +0 -0
  39. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/brokers/base.py +0 -0
  40. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/brokers/simulated.py +0 -0
  41. {onesecondtrader-0.34.0/src/onesecondtrader → onesecondtrader-0.36.0/src/onesecondtrader/core}/events/__init__.py +0 -0
  42. {onesecondtrader-0.34.0/src/onesecondtrader → onesecondtrader-0.36.0/src/onesecondtrader/core}/events/bases.py +0 -0
  43. {onesecondtrader-0.34.0/src/onesecondtrader → onesecondtrader-0.36.0/src/onesecondtrader/core}/indicators/__init__.py +0 -0
  44. {onesecondtrader-0.34.0/src/onesecondtrader → onesecondtrader-0.36.0/src/onesecondtrader/core}/messaging/__init__.py +0 -0
  45. {onesecondtrader-0.34.0/src/onesecondtrader → onesecondtrader-0.36.0/src/onesecondtrader/core}/models/__init__.py +0 -0
  46. {onesecondtrader-0.34.0/src/onesecondtrader → onesecondtrader-0.36.0/src/onesecondtrader/core}/models/data.py +0 -0
  47. {onesecondtrader-0.34.0/src/onesecondtrader → onesecondtrader-0.36.0/src/onesecondtrader/core}/models/orders.py +0 -0
  48. {onesecondtrader-0.34.0/src/onesecondtrader → onesecondtrader-0.36.0/src/onesecondtrader/core}/models/records.py +0 -0
  49. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/datafeeds/__init__.py +0 -0
  50. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/datafeeds/base.py +0 -0
  51. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/datafeeds/simulated.py +0 -0
  52. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/events/market.py +0 -0
  53. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/events/requests.py +0 -0
  54. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/events/responses.py +0 -0
  55. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/indicators/averages.py +0 -0
  56. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/indicators/bar.py +0 -0
  57. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/indicators/base.py +0 -0
  58. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/messaging/eventbus.py +0 -0
  59. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/messaging/subscriber.py +0 -0
  60. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/strategies/__init__.py +0 -0
  61. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/strategies/base.py +0 -0
  62. {onesecondtrader-0.34.0 → onesecondtrader-0.36.0}/src/onesecondtrader/strategies/sma_crossover.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.34.0
3
+ Version: 0.36.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.34.0"
3
+ version = "0.36.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,2 @@
1
+ from onesecondtrader.connectors import brokers as brokers
2
+ from onesecondtrader.connectors import datafeeds as datafeeds
@@ -0,0 +1,3 @@
1
+ __all__ = ["SimulatedBroker"]
2
+
3
+ from .simulated import SimulatedBroker
@@ -0,0 +1,346 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import uuid
5
+
6
+ from onesecondtrader.core import events, messaging, models
7
+ from onesecondtrader.core.brokers 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,4 @@
1
+ __all__ = ["Datafeed", "SimulatedDatafeed"]
2
+
3
+ from .base import Datafeed
4
+ from .simulated import SimulatedDatafeed
@@ -0,0 +1,19 @@
1
+ import abc
2
+
3
+ from onesecondtrader.core import events, messaging, models
4
+
5
+
6
+ class Datafeed(abc.ABC):
7
+ def __init__(self, event_bus: messaging.EventBus) -> None:
8
+ self._event_bus = event_bus
9
+
10
+ def _publish(self, event: events.EventBase) -> None:
11
+ self._event_bus.publish(event)
12
+
13
+ @abc.abstractmethod
14
+ def stream(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
15
+ pass
16
+
17
+ @abc.abstractmethod
18
+ def shutdown(self) -> None:
19
+ pass
@@ -0,0 +1,99 @@
1
+ import pathlib
2
+ import threading
3
+
4
+ import pandas as pd
5
+
6
+ from onesecondtrader.core import events, messaging, models
7
+ from .base import Datafeed
8
+
9
+ _RTYPE_MAP = {
10
+ models.BarPeriod.SECOND: 32,
11
+ models.BarPeriod.MINUTE: 33,
12
+ models.BarPeriod.HOUR: 34,
13
+ models.BarPeriod.DAY: 35,
14
+ }
15
+
16
+
17
+ class SimulatedDatafeed(Datafeed):
18
+ csv_path: str = ""
19
+
20
+ def __init__(self, event_bus: messaging.EventBus) -> None:
21
+ super().__init__(event_bus)
22
+ self._thread: threading.Thread | None = None
23
+ self._stop_event = threading.Event()
24
+
25
+ def stream(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
26
+ csv_path = pathlib.Path(self.csv_path)
27
+ if self._thread and self._thread.is_alive():
28
+ raise RuntimeError("Already streaming")
29
+ if not csv_path.exists():
30
+ raise FileNotFoundError(f"CSV file not found: {csv_path}")
31
+ if not symbols:
32
+ raise ValueError("symbols list cannot be empty")
33
+
34
+ self._stop_event.clear()
35
+ self._thread = threading.Thread(
36
+ target=self._stream,
37
+ args=(symbols, bar_period),
38
+ name=self.__class__.__name__,
39
+ daemon=False,
40
+ )
41
+ self._thread.start()
42
+
43
+ def wait(self) -> None:
44
+ if self._thread:
45
+ self._thread.join()
46
+
47
+ def shutdown(self) -> None:
48
+ self._stop_event.set()
49
+ if self._thread and self._thread.is_alive():
50
+ self._thread.join()
51
+
52
+ def _stream(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
53
+ symbols_set = set(symbols)
54
+ rtype = _RTYPE_MAP[bar_period]
55
+
56
+ for chunk in pd.read_csv(
57
+ self.csv_path,
58
+ usecols=[
59
+ "ts_event",
60
+ "rtype",
61
+ "open",
62
+ "high",
63
+ "low",
64
+ "close",
65
+ "volume",
66
+ "symbol",
67
+ ],
68
+ dtype={
69
+ "ts_event": int,
70
+ "rtype": int,
71
+ "open": int,
72
+ "high": int,
73
+ "low": int,
74
+ "close": int,
75
+ "volume": int,
76
+ "symbol": str,
77
+ },
78
+ chunksize=10_000,
79
+ ):
80
+ for row in chunk.itertuples():
81
+ if self._stop_event.is_set():
82
+ return
83
+
84
+ if row.symbol not in symbols_set or row.rtype != rtype:
85
+ continue
86
+
87
+ self._publish(
88
+ events.BarReceived(
89
+ ts_event=pd.Timestamp(row.ts_event, unit="ns", tz="UTC"),
90
+ symbol=row.symbol,
91
+ bar_period=bar_period,
92
+ open=row.open / 1e9,
93
+ high=row.high / 1e9,
94
+ low=row.low / 1e9,
95
+ close=row.close / 1e9,
96
+ volume=row.volume,
97
+ )
98
+ )
99
+ self._event_bus.wait_until_system_idle()
@@ -0,0 +1,6 @@
1
+ from onesecondtrader.core import brokers as brokers
2
+ from onesecondtrader.core import events as events
3
+ from onesecondtrader.core import indicators as indicators
4
+ from onesecondtrader.core import messaging as messaging
5
+ from onesecondtrader.core import models as models
6
+ from onesecondtrader.core import strategies as strategies
@@ -0,0 +1,3 @@
1
+ __all__ = ["BrokerBase"]
2
+
3
+ from .base import BrokerBase
@@ -0,0 +1,39 @@
1
+ import abc
2
+
3
+ from onesecondtrader.core 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,22 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+
5
+ from onesecondtrader.core 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.core 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.core 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