onesecondtrader 0.38.0__py3-none-any.whl → 0.40.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.
- onesecondtrader/__init__.py +6 -3
- onesecondtrader/connectors/__init__.py +1 -0
- onesecondtrader/connectors/brokers/__init__.py +2 -1
- onesecondtrader/connectors/brokers/ib.py +418 -0
- onesecondtrader/connectors/brokers/simulated.py +3 -0
- onesecondtrader/connectors/datafeeds/__init__.py +2 -2
- onesecondtrader/connectors/datafeeds/ib.py +286 -0
- onesecondtrader/connectors/datafeeds/simulated.py +126 -73
- onesecondtrader/connectors/gateways/__init__.py +3 -0
- onesecondtrader/connectors/gateways/ib.py +314 -0
- onesecondtrader/core/__init__.py +1 -0
- onesecondtrader/core/brokers/base.py +7 -0
- onesecondtrader/core/datafeeds/__init__.py +3 -0
- onesecondtrader/core/datafeeds/base.py +29 -0
- onesecondtrader/secmaster/__init__.py +3 -2
- onesecondtrader/secmaster/schema.sql +661 -15
- onesecondtrader/secmaster/utils.py +629 -8
- {onesecondtrader-0.38.0.dist-info → onesecondtrader-0.40.0.dist-info}/METADATA +5 -1
- {onesecondtrader-0.38.0.dist-info → onesecondtrader-0.40.0.dist-info}/RECORD +21 -16
- onesecondtrader/connectors/datafeeds/base.py +0 -19
- {onesecondtrader-0.38.0.dist-info → onesecondtrader-0.40.0.dist-info}/WHEEL +0 -0
- {onesecondtrader-0.38.0.dist-info → onesecondtrader-0.40.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
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
|
+
)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sqlite3
|
|
2
5
|
import threading
|
|
3
6
|
|
|
4
7
|
import pandas as pd
|
|
5
8
|
|
|
6
9
|
from onesecondtrader.core import events, messaging, models
|
|
7
|
-
from .
|
|
10
|
+
from onesecondtrader.core.datafeeds import DatafeedBase
|
|
8
11
|
|
|
9
12
|
_RTYPE_MAP = {
|
|
10
13
|
models.BarPeriod.SECOND: 32,
|
|
@@ -13,87 +16,137 @@ _RTYPE_MAP = {
|
|
|
13
16
|
models.BarPeriod.DAY: 35,
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
_PRICE_SCALE = 1e9
|
|
20
|
+
|
|
16
21
|
|
|
17
|
-
class SimulatedDatafeed(
|
|
18
|
-
|
|
22
|
+
class SimulatedDatafeed(DatafeedBase):
|
|
23
|
+
db_path: str = ""
|
|
19
24
|
|
|
20
25
|
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
21
26
|
super().__init__(event_bus)
|
|
27
|
+
self._db_path = self.db_path or os.environ.get(
|
|
28
|
+
"SECMASTER_DB_PATH", "secmaster.db"
|
|
29
|
+
)
|
|
30
|
+
self._connected = False
|
|
31
|
+
self._subscriptions: set[tuple[str, models.BarPeriod]] = set()
|
|
32
|
+
self._connection: sqlite3.Connection | None = None
|
|
22
33
|
self._thread: threading.Thread | None = None
|
|
23
34
|
self._stop_event = threading.Event()
|
|
24
35
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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()
|
|
36
|
+
def connect(self) -> None:
|
|
37
|
+
if self._connected:
|
|
38
|
+
return
|
|
39
|
+
self._connection = sqlite3.connect(self._db_path, check_same_thread=False)
|
|
40
|
+
self._connected = True
|
|
46
41
|
|
|
47
|
-
def
|
|
42
|
+
def disconnect(self) -> None:
|
|
43
|
+
if not self._connected:
|
|
44
|
+
return
|
|
48
45
|
self._stop_event.set()
|
|
49
46
|
if self._thread and self._thread.is_alive():
|
|
50
47
|
self._thread.join()
|
|
48
|
+
if self._connection:
|
|
49
|
+
self._connection.close()
|
|
50
|
+
self._connection = None
|
|
51
|
+
self._connected = False
|
|
52
|
+
|
|
53
|
+
def subscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
|
|
54
|
+
if (symbol, bar_period) in self._subscriptions:
|
|
55
|
+
return
|
|
56
|
+
self._subscriptions.add((symbol, bar_period))
|
|
57
|
+
if self._thread is None or not self._thread.is_alive():
|
|
58
|
+
self._stop_event.clear()
|
|
59
|
+
self._thread = threading.Thread(
|
|
60
|
+
target=self._stream,
|
|
61
|
+
name=self.__class__.__name__,
|
|
62
|
+
daemon=False,
|
|
63
|
+
)
|
|
64
|
+
self._thread.start()
|
|
65
|
+
|
|
66
|
+
def unsubscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
|
|
67
|
+
self._subscriptions.discard((symbol, bar_period))
|
|
68
|
+
|
|
69
|
+
def _stream(self) -> None:
|
|
70
|
+
if not self._connection:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
subscriptions = list(self._subscriptions)
|
|
74
|
+
if not subscriptions:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
symbol_rtype_pairs = [
|
|
78
|
+
(symbol, _RTYPE_MAP[bar_period]) for symbol, bar_period in subscriptions
|
|
79
|
+
]
|
|
80
|
+
rtype_by_symbol = {symbol: rtype for symbol, rtype in symbol_rtype_pairs}
|
|
81
|
+
bar_period_by_symbol = {symbol: bp for symbol, bp in subscriptions}
|
|
82
|
+
symbols = list(rtype_by_symbol.keys())
|
|
51
83
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
cursor = self._connection.cursor()
|
|
85
|
+
|
|
86
|
+
instrument_ids = self._resolve_instrument_ids(cursor, symbols)
|
|
87
|
+
if not instrument_ids:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
instrument_to_symbol = {v: k for k, v in instrument_ids.items()}
|
|
91
|
+
id_list = list(instrument_ids.values())
|
|
92
|
+
rtype_list = list(set(rtype_by_symbol.values()))
|
|
93
|
+
|
|
94
|
+
placeholders_ids = ",".join("?" * len(id_list))
|
|
95
|
+
placeholders_rtypes = ",".join("?" * len(rtype_list))
|
|
96
|
+
|
|
97
|
+
query = f"""
|
|
98
|
+
SELECT instrument_id, rtype, ts_event, open, high, low, close, volume
|
|
99
|
+
FROM ohlcv
|
|
100
|
+
WHERE instrument_id IN ({placeholders_ids})
|
|
101
|
+
AND rtype IN ({placeholders_rtypes})
|
|
102
|
+
ORDER BY ts_event
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
cursor.execute(query, id_list + rtype_list)
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
if self._stop_event.is_set():
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
row = cursor.fetchone()
|
|
112
|
+
if row is None:
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
instrument_id, rtype, ts_event, open_, high, low, close, volume = row
|
|
116
|
+
symbol = instrument_to_symbol.get(instrument_id)
|
|
117
|
+
if symbol is None:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
expected_rtype = rtype_by_symbol.get(symbol)
|
|
121
|
+
if rtype != expected_rtype:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
bar_period = bar_period_by_symbol[symbol]
|
|
125
|
+
|
|
126
|
+
self._publish(
|
|
127
|
+
events.BarReceived(
|
|
128
|
+
ts_event=pd.Timestamp(ts_event, unit="ns", tz="UTC"),
|
|
129
|
+
symbol=symbol,
|
|
130
|
+
bar_period=bar_period,
|
|
131
|
+
open=open_ / _PRICE_SCALE,
|
|
132
|
+
high=high / _PRICE_SCALE,
|
|
133
|
+
low=low / _PRICE_SCALE,
|
|
134
|
+
close=close / _PRICE_SCALE,
|
|
135
|
+
volume=volume,
|
|
98
136
|
)
|
|
99
|
-
|
|
137
|
+
)
|
|
138
|
+
self._event_bus.wait_until_system_idle()
|
|
139
|
+
|
|
140
|
+
def _resolve_instrument_ids(
|
|
141
|
+
self, cursor: sqlite3.Cursor, symbols: list[str]
|
|
142
|
+
) -> dict[str, int]:
|
|
143
|
+
placeholders = ",".join("?" * len(symbols))
|
|
144
|
+
query = f"""
|
|
145
|
+
SELECT symbol, instrument_id
|
|
146
|
+
FROM symbology
|
|
147
|
+
WHERE symbol IN ({placeholders})
|
|
148
|
+
GROUP BY symbol
|
|
149
|
+
HAVING start_date = MAX(start_date)
|
|
150
|
+
"""
|
|
151
|
+
cursor.execute(query, symbols)
|
|
152
|
+
return {row[0]: row[1] for row in cursor.fetchall()}
|