Skip to content

Commit 4ea6291

Browse files
committed
Retry IBKR quote snapshots before fallback
1 parent 27bc11a commit 4ea6291

4 files changed

Lines changed: 69 additions & 29 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "quant-platform-kit"
7-
version = "0.7.13"
7+
version = "0.7.14"
88
description = "Shared broker adapters, domain models, execution ports, and notification utilities for QuantStrategyLab strategies."
99
readme = "README.md"
1010
requires-python = ">=3.9"

src/quant_platform_kit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""QuantPlatformKit public package surface."""
22

3-
__version__ = "0.7.13"
3+
__version__ = "0.7.14"
44

55
from .common.models import (
66
ExecutionReport,

src/quant_platform_kit/ibkr/market_data.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ def fetch_quote_snapshots(
184184
symbols: list[str] | tuple[str, ...] | set[str],
185185
*,
186186
wait_seconds: float = 3.0,
187+
retry_wait_seconds: float = 1.5,
188+
attempts_per_data_type: int = 2,
187189
exchange: str = "SMART",
188190
currency: str = "USD",
189191
stock_factory: Callable[..., Any] | None = None,
@@ -199,38 +201,39 @@ def fetch_quote_snapshots(
199201
ib.qualifyContracts(contract)
200202
contracts.append((symbol, contract))
201203

202-
snapshots = _collect_quote_snapshots(
203-
ib,
204-
contracts,
205-
wait_seconds=wait_seconds,
206-
currency=currency,
207-
)
208-
missing_contracts = [(symbol, contract) for symbol, contract in contracts if symbol not in snapshots]
209-
if not missing_contracts:
210-
return snapshots
204+
snapshots: dict[str, QuoteSnapshot] = {}
205+
missing_contracts = list(contracts)
206+
attempts_per_data_type = max(int(attempts_per_data_type or 1), 1)
211207

212208
setter = getattr(ib, "reqMarketDataType", None)
213-
if not callable(setter):
214-
return snapshots
209+
market_data_types = (1, 2, 4) if callable(setter) else (1,)
215210

216211
try:
217-
for market_data_type in (2, 4):
218-
_set_market_data_type(ib, market_data_type)
219-
recovered = _collect_quote_snapshots(
220-
ib,
221-
missing_contracts,
222-
wait_seconds=wait_seconds,
223-
currency=currency,
224-
)
225-
snapshots.update(recovered)
226-
missing_contracts = [
227-
(symbol, contract)
228-
for symbol, contract in missing_contracts
229-
if symbol not in recovered
230-
]
212+
for market_data_type in market_data_types:
213+
if callable(setter):
214+
_set_market_data_type(ib, market_data_type)
215+
216+
for attempt_index in range(attempts_per_data_type):
217+
wait_for_attempt = wait_seconds if attempt_index == 0 else retry_wait_seconds
218+
recovered = _collect_quote_snapshots(
219+
ib,
220+
missing_contracts,
221+
wait_seconds=wait_for_attempt,
222+
currency=currency,
223+
)
224+
snapshots.update(recovered)
225+
missing_contracts = [
226+
(symbol, contract)
227+
for symbol, contract in missing_contracts
228+
if symbol not in snapshots
229+
]
230+
if not missing_contracts:
231+
break
232+
231233
if not missing_contracts:
232234
break
233235
finally:
234-
_set_market_data_type(ib, 1)
236+
if callable(setter):
237+
_set_market_data_type(ib, 1)
235238

236239
return snapshots

tests/test_ibkr_market_data.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,43 @@ def reqMktData(self, contract, *_args):
147147
self.assertEqual(snapshots["SPY"].last_price, 102.5)
148148

149149

150+
151+
def test_fetch_quote_snapshots_retries_same_market_data_type_before_fallback(self) -> None:
152+
class RetrySameTypeIB(FakeIB):
153+
def __init__(self):
154+
super().__init__()
155+
self.market_data_type = 1
156+
self.market_data_type_calls = []
157+
self.market_data_attempts = {}
158+
159+
def reqMarketDataType(self, market_data_type):
160+
self.market_data_type = market_data_type
161+
self.market_data_type_calls.append(market_data_type)
162+
163+
def reqMktData(self, contract, *_args):
164+
self.last_market_data_contract = contract
165+
key = (self.market_data_type, contract.symbol)
166+
attempt = self.market_data_attempts.get(key, 0)
167+
self.market_data_attempts[key] = attempt + 1
168+
if self.market_data_type == 1 and attempt == 0:
169+
return FakeTicker(-1.0, close=float("nan"), bid=None, ask=None)
170+
return FakeTicker(101.8, close=101.8, bid=101.7, ask=101.9)
171+
172+
ib = RetrySameTypeIB()
173+
snapshots = fetch_quote_snapshots(
174+
ib,
175+
{"SPY"},
176+
wait_seconds=0,
177+
retry_wait_seconds=0,
178+
attempts_per_data_type=2,
179+
stock_factory=FakeContract,
180+
)
181+
182+
self.assertEqual(snapshots["SPY"].last_price, 101.8)
183+
self.assertEqual(ib.market_data_attempts[(1, "SPY")], 2)
184+
self.assertNotIn(2, ib.market_data_type_calls)
185+
self.assertNotIn(4, ib.market_data_type_calls)
186+
150187
def test_fetch_quote_snapshots_retries_with_market_data_fallbacks(self) -> None:
151188
class FallbackMarketDataIB(FakeIB):
152189
def __init__(self):
@@ -175,7 +212,7 @@ def reqMktData(self, contract, *_args):
175212
)
176213

177214
self.assertEqual(snapshots["SPY"].last_price, 101.8)
178-
self.assertEqual(ib.market_data_type_calls, [2, 4, 1])
215+
self.assertEqual(ib.market_data_type_calls, [1, 2, 4, 1])
179216

180217

181218
if __name__ == "__main__":

0 commit comments

Comments
 (0)