onesecondtrader 0.43.0__py3-none-any.whl → 0.44.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.
Files changed (55) hide show
  1. onesecondtrader/__init__.py +0 -60
  2. onesecondtrader/models/__init__.py +11 -0
  3. onesecondtrader/models/bar_fields.py +23 -0
  4. onesecondtrader/models/bar_period.py +21 -0
  5. onesecondtrader/models/order_types.py +21 -0
  6. onesecondtrader/models/trade_sides.py +20 -0
  7. {onesecondtrader-0.43.0.dist-info → onesecondtrader-0.44.0.dist-info}/METADATA +2 -2
  8. onesecondtrader-0.44.0.dist-info/RECORD +10 -0
  9. onesecondtrader/connectors/__init__.py +0 -3
  10. onesecondtrader/connectors/brokers/__init__.py +0 -4
  11. onesecondtrader/connectors/brokers/ib.py +0 -418
  12. onesecondtrader/connectors/brokers/simulated.py +0 -349
  13. onesecondtrader/connectors/datafeeds/__init__.py +0 -4
  14. onesecondtrader/connectors/datafeeds/ib.py +0 -286
  15. onesecondtrader/connectors/datafeeds/simulated.py +0 -198
  16. onesecondtrader/connectors/gateways/__init__.py +0 -3
  17. onesecondtrader/connectors/gateways/ib.py +0 -314
  18. onesecondtrader/core/__init__.py +0 -7
  19. onesecondtrader/core/brokers/__init__.py +0 -3
  20. onesecondtrader/core/brokers/base.py +0 -46
  21. onesecondtrader/core/datafeeds/__init__.py +0 -3
  22. onesecondtrader/core/datafeeds/base.py +0 -32
  23. onesecondtrader/core/events/__init__.py +0 -33
  24. onesecondtrader/core/events/bases.py +0 -29
  25. onesecondtrader/core/events/market.py +0 -22
  26. onesecondtrader/core/events/requests.py +0 -33
  27. onesecondtrader/core/events/responses.py +0 -54
  28. onesecondtrader/core/indicators/__init__.py +0 -13
  29. onesecondtrader/core/indicators/averages.py +0 -56
  30. onesecondtrader/core/indicators/bar.py +0 -47
  31. onesecondtrader/core/indicators/base.py +0 -60
  32. onesecondtrader/core/messaging/__init__.py +0 -7
  33. onesecondtrader/core/messaging/eventbus.py +0 -47
  34. onesecondtrader/core/messaging/subscriber.py +0 -69
  35. onesecondtrader/core/models/__init__.py +0 -15
  36. onesecondtrader/core/models/data.py +0 -18
  37. onesecondtrader/core/models/orders.py +0 -27
  38. onesecondtrader/core/models/params.py +0 -21
  39. onesecondtrader/core/models/records.py +0 -34
  40. onesecondtrader/core/strategies/__init__.py +0 -7
  41. onesecondtrader/core/strategies/base.py +0 -331
  42. onesecondtrader/core/strategies/examples.py +0 -47
  43. onesecondtrader/dashboard/__init__.py +0 -3
  44. onesecondtrader/dashboard/app.py +0 -2972
  45. onesecondtrader/dashboard/registry.py +0 -100
  46. onesecondtrader/orchestrator/__init__.py +0 -7
  47. onesecondtrader/orchestrator/orchestrator.py +0 -105
  48. onesecondtrader/orchestrator/recorder.py +0 -199
  49. onesecondtrader/orchestrator/schema.sql +0 -212
  50. onesecondtrader/secmaster/__init__.py +0 -6
  51. onesecondtrader/secmaster/schema.sql +0 -740
  52. onesecondtrader/secmaster/utils.py +0 -737
  53. onesecondtrader-0.43.0.dist-info/RECORD +0 -49
  54. {onesecondtrader-0.43.0.dist-info → onesecondtrader-0.44.0.dist-info}/WHEEL +0 -0
  55. {onesecondtrader-0.43.0.dist-info → onesecondtrader-0.44.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,349 +0,0 @@
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 connect(self) -> None:
36
- pass
37
-
38
- def _on_event(self, event: events.EventBase) -> None:
39
- match event:
40
- case events.BarReceived() as bar:
41
- self._on_bar(bar)
42
- case _:
43
- super()._on_event(event)
44
-
45
- def _on_bar(self, event: events.BarReceived) -> None:
46
- self._process_market_orders(event)
47
- self._process_stop_orders(event)
48
- self._process_stop_limit_orders(event)
49
- self._process_limit_orders(event)
50
-
51
- def _process_market_orders(self, event: events.BarReceived) -> None:
52
- for order_id, order in list(self._pending_market_orders.items()):
53
- if order.symbol != event.symbol:
54
- continue
55
-
56
- self._publish(
57
- events.OrderFilled(
58
- ts_event=event.ts_event,
59
- ts_broker=event.ts_event,
60
- associated_order_id=order.order_id,
61
- symbol=order.symbol,
62
- side=order.side,
63
- quantity_filled=order.quantity,
64
- fill_price=event.open,
65
- commission=max(
66
- order.quantity * self.commission_per_unit,
67
- self.minimum_commission_per_order,
68
- ),
69
- )
70
- )
71
- del self._pending_market_orders[order_id]
72
-
73
- def _process_stop_orders(self, event: events.BarReceived) -> None:
74
- for order_id, order in list(self._pending_stop_orders.items()):
75
- if order.symbol != event.symbol:
76
- continue
77
-
78
- # This is for mypy, it has already been validated on submission
79
- assert order.stop_price is not None
80
-
81
- triggered = False
82
- match order.side:
83
- case models.OrderSide.BUY:
84
- triggered = event.high >= order.stop_price
85
- case models.OrderSide.SELL:
86
- triggered = event.low <= order.stop_price
87
-
88
- if not triggered:
89
- continue
90
-
91
- fill_price = 0.0
92
- match order.side:
93
- case models.OrderSide.BUY:
94
- fill_price = max(order.stop_price, event.open)
95
- case models.OrderSide.SELL:
96
- fill_price = min(order.stop_price, event.open)
97
-
98
- self._publish(
99
- events.OrderFilled(
100
- ts_event=event.ts_event,
101
- ts_broker=event.ts_event,
102
- associated_order_id=order.order_id,
103
- symbol=order.symbol,
104
- side=order.side,
105
- quantity_filled=order.quantity,
106
- fill_price=fill_price,
107
- commission=max(
108
- order.quantity * self.commission_per_unit,
109
- self.minimum_commission_per_order,
110
- ),
111
- )
112
- )
113
- del self._pending_stop_orders[order_id]
114
-
115
- def _process_stop_limit_orders(self, event: events.BarReceived) -> None:
116
- for order_id, order in list(self._pending_stop_limit_orders.items()):
117
- if order.symbol != event.symbol:
118
- continue
119
-
120
- # This is for mypy, it has already been validated on submission
121
- assert order.stop_price is not None
122
-
123
- triggered = False
124
- match order.side:
125
- case models.OrderSide.BUY:
126
- triggered = event.high >= order.stop_price
127
- case models.OrderSide.SELL:
128
- triggered = event.low <= order.stop_price
129
-
130
- if not triggered:
131
- continue
132
-
133
- limit_order = dataclasses.replace(order, order_type=models.OrderType.LIMIT)
134
- self._pending_limit_orders[order_id] = limit_order
135
- del self._pending_stop_limit_orders[order_id]
136
-
137
- def _process_limit_orders(self, event: events.BarReceived) -> None:
138
- for order_id, order in list(self._pending_limit_orders.items()):
139
- if order.symbol != event.symbol:
140
- continue
141
-
142
- # This is for mypy, it has already been validated on submission
143
- assert order.limit_price is not None
144
-
145
- triggered = False
146
- match order.side:
147
- case models.OrderSide.BUY:
148
- triggered = event.low <= order.limit_price
149
- case models.OrderSide.SELL:
150
- triggered = event.high >= order.limit_price
151
-
152
- if not triggered:
153
- continue
154
-
155
- fill_price = 0.0
156
- match order.side:
157
- case models.OrderSide.BUY:
158
- fill_price = min(order.limit_price, event.open)
159
- case models.OrderSide.SELL:
160
- fill_price = max(order.limit_price, event.open)
161
-
162
- self._publish(
163
- events.OrderFilled(
164
- ts_event=event.ts_event,
165
- ts_broker=event.ts_event,
166
- associated_order_id=order.order_id,
167
- symbol=order.symbol,
168
- side=order.side,
169
- quantity_filled=order.quantity,
170
- fill_price=fill_price,
171
- commission=max(
172
- order.quantity * self.commission_per_unit,
173
- self.minimum_commission_per_order,
174
- ),
175
- )
176
- )
177
- del self._pending_limit_orders[order_id]
178
-
179
- def _reject_if_invalid_submission(self, event: events.OrderSubmission) -> bool:
180
- is_invalid = event.quantity <= 0
181
-
182
- match event.order_type:
183
- case models.OrderType.LIMIT:
184
- is_invalid = (
185
- is_invalid or event.limit_price is None or event.limit_price <= 0
186
- )
187
- case models.OrderType.STOP:
188
- is_invalid = (
189
- is_invalid or event.stop_price is None or event.stop_price <= 0
190
- )
191
- case models.OrderType.STOP_LIMIT:
192
- is_invalid = is_invalid or (
193
- event.limit_price is None
194
- or event.limit_price <= 0
195
- or event.stop_price is None
196
- or event.stop_price <= 0
197
- )
198
-
199
- if is_invalid:
200
- # Use event timestamp to maintain simulated time consistency in backtesting
201
- self._publish(
202
- events.OrderSubmissionRejected(
203
- ts_event=event.ts_event,
204
- ts_broker=event.ts_event,
205
- associated_order_id=event.system_order_id,
206
- )
207
- )
208
-
209
- return is_invalid
210
-
211
- def _on_submit_order(self, event: events.OrderSubmission) -> None:
212
- if self._reject_if_invalid_submission(event):
213
- return
214
-
215
- order = _PendingOrder(
216
- order_id=event.system_order_id,
217
- symbol=event.symbol,
218
- order_type=event.order_type,
219
- side=event.side,
220
- quantity=event.quantity,
221
- limit_price=event.limit_price,
222
- stop_price=event.stop_price,
223
- )
224
-
225
- match order.order_type:
226
- case models.OrderType.MARKET:
227
- self._pending_market_orders[order.order_id] = order
228
- case models.OrderType.LIMIT:
229
- self._pending_limit_orders[order.order_id] = order
230
- case models.OrderType.STOP:
231
- self._pending_stop_orders[order.order_id] = order
232
- case models.OrderType.STOP_LIMIT:
233
- self._pending_stop_limit_orders[order.order_id] = order
234
-
235
- # Use event timestamp to maintain simulated time consistency in backtesting
236
- self._publish(
237
- events.OrderSubmissionAccepted(
238
- ts_event=event.ts_event,
239
- ts_broker=event.ts_event,
240
- associated_order_id=order.order_id,
241
- )
242
- )
243
-
244
- def _on_cancel_order(self, event: events.OrderCancellation) -> None:
245
- order_id = event.system_order_id
246
-
247
- removed = False
248
- for pending_orders in (
249
- self._pending_market_orders,
250
- self._pending_limit_orders,
251
- self._pending_stop_orders,
252
- self._pending_stop_limit_orders,
253
- ):
254
- if order_id in pending_orders:
255
- del pending_orders[order_id]
256
- removed = True
257
- break
258
-
259
- # Use event timestamp to maintain simulated time consistency in backtesting
260
- if removed:
261
- self._publish(
262
- events.OrderCancellationAccepted(
263
- ts_event=event.ts_event,
264
- ts_broker=event.ts_event,
265
- associated_order_id=order_id,
266
- )
267
- )
268
- else:
269
- self._publish(
270
- events.OrderCancellationRejected(
271
- ts_event=event.ts_event,
272
- ts_broker=event.ts_event,
273
- associated_order_id=order_id,
274
- )
275
- )
276
-
277
- def _reject_if_invalid_modification(self, event: events.OrderModification) -> bool:
278
- is_invalid = (
279
- (event.quantity is not None and event.quantity <= 0)
280
- or (event.limit_price is not None and event.limit_price <= 0)
281
- or (event.stop_price is not None and event.stop_price <= 0)
282
- )
283
-
284
- if is_invalid:
285
- # Use event timestamp to maintain simulated time consistency in backtesting
286
- self._publish(
287
- events.OrderModificationRejected(
288
- ts_event=event.ts_event,
289
- ts_broker=event.ts_event,
290
- associated_order_id=event.system_order_id,
291
- )
292
- )
293
-
294
- return is_invalid
295
-
296
- def _on_modify_order(self, event: events.OrderModification) -> None:
297
- if self._reject_if_invalid_modification(event):
298
- return
299
-
300
- order_id = event.system_order_id
301
-
302
- for pending_orders in (
303
- self._pending_market_orders,
304
- self._pending_limit_orders,
305
- self._pending_stop_orders,
306
- self._pending_stop_limit_orders,
307
- ):
308
- if order_id in pending_orders:
309
- order = pending_orders[order_id]
310
-
311
- new_quantity = (
312
- event.quantity if event.quantity is not None else order.quantity
313
- )
314
- new_limit_price = (
315
- event.limit_price
316
- if event.limit_price is not None
317
- else order.limit_price
318
- )
319
- new_stop_price = (
320
- event.stop_price
321
- if event.stop_price is not None
322
- else order.stop_price
323
- )
324
-
325
- pending_orders[order_id] = dataclasses.replace(
326
- order,
327
- quantity=new_quantity,
328
- limit_price=new_limit_price,
329
- stop_price=new_stop_price,
330
- )
331
-
332
- # Use event timestamp to maintain simulated time consistency in backtesting
333
- self._publish(
334
- events.OrderModificationAccepted(
335
- ts_event=event.ts_event,
336
- ts_broker=event.ts_event,
337
- associated_order_id=order_id,
338
- )
339
- )
340
- return
341
-
342
- # Use event timestamp to maintain simulated time consistency in backtesting
343
- self._publish(
344
- events.OrderModificationRejected(
345
- ts_event=event.ts_event,
346
- ts_broker=event.ts_event,
347
- associated_order_id=order_id,
348
- )
349
- )
@@ -1,4 +0,0 @@
1
- __all__ = ["IBDatafeed", "SimulatedDatafeed"]
2
-
3
- from .ib import IBDatafeed
4
- from .simulated import SimulatedDatafeed
@@ -1,286 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import os
5
- import sqlite3
6
-
7
- import pandas as pd
8
- from ib_async import Contract
9
-
10
- from onesecondtrader.connectors.gateways.ib import _get_gateway, make_contract
11
- from onesecondtrader.core import events, messaging, models
12
- from onesecondtrader.core.datafeeds import DatafeedBase
13
-
14
- _logger = logging.getLogger(__name__)
15
-
16
- _BAR_SIZE_MAP = {
17
- models.BarPeriod.MINUTE: "1 min",
18
- models.BarPeriod.HOUR: "1 hour",
19
- models.BarPeriod.DAY: "1 day",
20
- }
21
-
22
-
23
- class IBDatafeed(DatafeedBase):
24
- """
25
- Live market data feed from Interactive Brokers.
26
-
27
- Subscribes to real-time bar data from IB and publishes BarReceived events.
28
-
29
- Symbol Resolution:
30
- Symbols are resolved to IB Contracts using a priority system:
31
-
32
- 1. Explicit format: Use colon-separated qualifiers for full control.
33
- Format: ``SYMBOL:SECTYPE:CURRENCY:EXCHANGE[:EXPIRY[:STRIKE[:RIGHT]]]``
34
-
35
- Examples:
36
- - ``"AAPL:STK:USD:SMART"`` - Apple stock
37
- - ``"EUR:CASH:USD:IDEALPRO"`` - EUR/USD forex
38
- - ``"ES:FUT:USD:CME:202503"`` - ES futures March 2025
39
- - ``"AAPL:OPT:USD:SMART:20250321:150:C"`` - AAPL call option
40
-
41
- 2. Secmaster lookup: If ``db_path`` is configured and the symbol exists
42
- in the instruments table, contract details are read from there.
43
-
44
- 3. Default: Simple symbols like ``"AAPL"`` are treated as US stocks
45
- traded on SMART routing with USD currency.
46
-
47
- Bar Periods:
48
- - ``BarPeriod.SECOND``: Uses tick-by-tick data aggregated into 1-second
49
- bars. Limited to 5 simultaneous subscriptions by IB.
50
- - ``BarPeriod.MINUTE``, ``HOUR``, ``DAY``: Uses historical data with
51
- live updates. Limited to 50 simultaneous subscriptions by IB.
52
-
53
- Attributes:
54
- db_path: Optional path to secmaster database for symbol resolution.
55
- If empty, uses SECMASTER_DB_PATH environment variable.
56
- If neither is set, secmaster lookup is disabled.
57
- """
58
-
59
- db_path: str = ""
60
-
61
- def __init__(self, event_bus: messaging.EventBus) -> None:
62
- super().__init__(event_bus)
63
- self._gateway = _get_gateway()
64
- self._connected = False
65
- self._subscriptions: set[tuple[str, models.BarPeriod]] = set()
66
- self._active_bars: dict[tuple[str, models.BarPeriod], object] = {}
67
- self._db_connection: sqlite3.Connection | None = None
68
- self._tick_aggregators: dict[str, _TickAggregator] = {}
69
- self._lock = __import__("threading").Lock()
70
-
71
- def connect(self) -> None:
72
- if self._connected:
73
- return
74
- _logger.info("Connecting to IB datafeed")
75
- self._gateway.acquire()
76
- self._gateway.register_reconnect_callback(self._on_reconnect)
77
- self._gateway.register_disconnect_callback(self._on_disconnect)
78
- db_path = self.db_path or os.environ.get("SECMASTER_DB_PATH", "")
79
- if db_path and os.path.exists(db_path):
80
- self._db_connection = sqlite3.connect(db_path, check_same_thread=False)
81
- self._connected = True
82
- _logger.info("Connected to IB datafeed")
83
-
84
- def disconnect(self) -> None:
85
- if not self._connected:
86
- return
87
- _logger.info("Disconnecting from IB datafeed")
88
- self._gateway.unregister_reconnect_callback(self._on_reconnect)
89
- self._gateway.unregister_disconnect_callback(self._on_disconnect)
90
- with self._lock:
91
- for symbol, bar_period in list(self._subscriptions):
92
- self.unsubscribe(symbol, bar_period)
93
- if self._db_connection:
94
- self._db_connection.close()
95
- self._db_connection = None
96
- self._gateway.release()
97
- self._connected = False
98
- _logger.info("Disconnected from IB datafeed")
99
-
100
- def _on_disconnect(self) -> None:
101
- _logger.warning("IB connection lost, clearing stale state")
102
- with self._lock:
103
- self._active_bars.clear()
104
- self._tick_aggregators.clear()
105
-
106
- def _on_reconnect(self) -> None:
107
- with self._lock:
108
- subscriptions_to_restore = list(self._subscriptions)
109
- _logger.info(
110
- "Restoring %d subscriptions after reconnect", len(subscriptions_to_restore)
111
- )
112
- with self._lock:
113
- self._active_bars.clear()
114
- self._tick_aggregators.clear()
115
- for symbol, bar_period in subscriptions_to_restore:
116
- with self._lock:
117
- self._subscriptions.discard((symbol, bar_period))
118
- self.subscribe(symbol, bar_period)
119
-
120
- def subscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
121
- with self._lock:
122
- if (symbol, bar_period) in self._subscriptions:
123
- return
124
- _logger.info("Subscribing to %s %s", symbol, bar_period.name)
125
- self._subscriptions.add((symbol, bar_period))
126
- contract = self._make_contract(symbol)
127
-
128
- if bar_period == models.BarPeriod.SECOND:
129
- self._subscribe_tick(symbol, contract, bar_period)
130
- else:
131
- self._subscribe_historical(symbol, contract, bar_period)
132
-
133
- def unsubscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
134
- with self._lock:
135
- if (symbol, bar_period) not in self._subscriptions:
136
- return
137
- _logger.info("Unsubscribing from %s %s", symbol, bar_period.name)
138
- self._subscriptions.discard((symbol, bar_period))
139
- bars = self._active_bars.pop((symbol, bar_period), None)
140
- if bars is None:
141
- return
142
-
143
- if bar_period == models.BarPeriod.SECOND:
144
- self._gateway.run_coro(self._cancel_tick_async(bars))
145
- with self._lock:
146
- self._tick_aggregators.pop(symbol, None)
147
- else:
148
- self._gateway.run_coro(self._cancel_historical_async(bars))
149
-
150
- def _subscribe_historical(
151
- self, symbol: str, contract: Contract, bar_period: models.BarPeriod
152
- ) -> None:
153
- bar_size = _BAR_SIZE_MAP[bar_period]
154
-
155
- async def _subscribe():
156
- bars = await self._gateway.ib.reqHistoricalDataAsync(
157
- contract,
158
- endDateTime="",
159
- durationStr="1 D",
160
- barSizeSetting=bar_size,
161
- whatToShow="TRADES",
162
- useRTH=False,
163
- keepUpToDate=True,
164
- )
165
- bars.updateEvent += lambda b, has_new: self._on_historical_update(
166
- symbol, bar_period, b, has_new
167
- )
168
- return bars
169
-
170
- bars = self._gateway.run_coro(_subscribe())
171
- self._active_bars[(symbol, bar_period)] = bars
172
-
173
- def _subscribe_tick(
174
- self, symbol: str, contract: Contract, bar_period: models.BarPeriod
175
- ) -> None:
176
- aggregator = _TickAggregator(symbol, bar_period, self._publish)
177
- self._tick_aggregators[symbol] = aggregator
178
-
179
- async def _subscribe():
180
- ticker = await self._gateway.ib.reqTickByTickDataAsync(
181
- contract, tickType="AllLast"
182
- )
183
- ticker.updateEvent += lambda t: self._on_tick_update(symbol, t)
184
- return ticker
185
-
186
- ticker = self._gateway.run_coro(_subscribe())
187
- self._active_bars[(symbol, bar_period)] = ticker
188
-
189
- async def _cancel_historical_async(self, bars) -> None:
190
- self._gateway.ib.cancelHistoricalData(bars)
191
-
192
- async def _cancel_tick_async(self, ticker) -> None:
193
- self._gateway.ib.cancelTickByTickData(ticker.contract, "AllLast")
194
-
195
- def _on_historical_update(
196
- self, symbol: str, bar_period: models.BarPeriod, bars, has_new_bar: bool
197
- ) -> None:
198
- if not has_new_bar or not bars:
199
- return
200
- bar = bars[-1]
201
- self._publish(
202
- events.BarReceived(
203
- ts_event=pd.Timestamp(bar.date, tz="UTC"),
204
- symbol=symbol,
205
- bar_period=bar_period,
206
- open=bar.open,
207
- high=bar.high,
208
- low=bar.low,
209
- close=bar.close,
210
- volume=int(bar.volume),
211
- )
212
- )
213
-
214
- def _on_tick_update(self, symbol: str, ticker) -> None:
215
- aggregator = self._tick_aggregators.get(symbol)
216
- if aggregator is None:
217
- return
218
- for tick in ticker.tickByTicks:
219
- if tick is not None:
220
- aggregator.on_tick(tick.time, tick.price, tick.size)
221
-
222
- def _make_contract(self, symbol: str):
223
- return make_contract(symbol, self._db_connection)
224
-
225
-
226
- class _TickAggregator:
227
- def __init__(
228
- self,
229
- symbol: str,
230
- bar_period: models.BarPeriod,
231
- publish_fn,
232
- ) -> None:
233
- self._symbol = symbol
234
- self._bar_period = bar_period
235
- self._publish = publish_fn
236
- self._current_second: pd.Timestamp | None = None
237
- self._open: float = 0.0
238
- self._high: float = 0.0
239
- self._low: float = 0.0
240
- self._close: float = 0.0
241
- self._volume: int = 0
242
-
243
- def on_tick(self, time: pd.Timestamp, price: float, size: int) -> None:
244
- tick_second = time.floor("s")
245
-
246
- if self._current_second is None:
247
- self._start_new_bar(tick_second, price, size)
248
- return
249
-
250
- if tick_second > self._current_second:
251
- self._emit_bar()
252
- self._start_new_bar(tick_second, price, size)
253
- else:
254
- self._update_bar(price, size)
255
-
256
- def _start_new_bar(
257
- self, tick_second: pd.Timestamp, price: float, size: int
258
- ) -> None:
259
- self._current_second = tick_second
260
- self._open = price
261
- self._high = price
262
- self._low = price
263
- self._close = price
264
- self._volume = size
265
-
266
- def _update_bar(self, price: float, size: int) -> None:
267
- self._high = max(self._high, price)
268
- self._low = min(self._low, price)
269
- self._close = price
270
- self._volume += size
271
-
272
- def _emit_bar(self) -> None:
273
- if self._current_second is None:
274
- return
275
- self._publish(
276
- events.BarReceived(
277
- ts_event=self._current_second,
278
- symbol=self._symbol,
279
- bar_period=self._bar_period,
280
- open=self._open,
281
- high=self._high,
282
- low=self._low,
283
- close=self._close,
284
- volume=self._volume,
285
- )
286
- )