Skip to content

Commit 6f71766

Browse files
authored
Merge pull request #66 from QuantStrategyLab/fix-ibkr-live-precheck
Fix IBKR historical data requests
2 parents 3193c14 + a0f3856 commit 6f71766

2 files changed

Lines changed: 87 additions & 14 deletions

File tree

src/quant_platform_kit/ibkr/market_data.py

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

33
from datetime import date, datetime, time
4+
from math import ceil
45
from math import isnan
6+
import re
57
from typing import Any, Callable
68

79
from quant_platform_kit.common.models import PricePoint, PriceSeries, QuoteSnapshot
@@ -32,6 +34,49 @@ def _build_stock_contract(
3234
return stock_factory(symbol, exchange, currency)
3335

3436

37+
def _normalize_duration_for_ibkr(duration: str) -> str:
38+
text = str(duration or "").strip()
39+
match = re.fullmatch(r"(\d+)\s*([A-Za-z]+)", text)
40+
if not match:
41+
return text
42+
43+
quantity = int(match.group(1))
44+
unit = match.group(2).upper()
45+
if unit == "D" and quantity > 365:
46+
return f"{ceil(quantity / 365)} Y"
47+
return f"{quantity} {unit}"
48+
49+
50+
def _request_historical_bars(
51+
ib: Any,
52+
contract: Any,
53+
*,
54+
duration: str,
55+
bar_size: str,
56+
) -> Any:
57+
normalized_duration = _normalize_duration_for_ibkr(duration)
58+
last_error: Exception | None = None
59+
for what_to_show in ("ADJUSTED_LAST", "TRADES"):
60+
try:
61+
bars = ib.reqHistoricalData(
62+
contract,
63+
endDateTime="",
64+
durationStr=normalized_duration,
65+
barSizeSetting=bar_size,
66+
whatToShow=what_to_show,
67+
useRTH=True,
68+
formatDate=1,
69+
)
70+
except Exception as exc: # pragma: no cover - exercised by live broker adapters.
71+
last_error = exc
72+
continue
73+
if bars:
74+
return bars
75+
if last_error is not None:
76+
raise last_error
77+
return ()
78+
79+
3580
def fetch_historical_price_series(
3681
ib: Any,
3782
symbol: str,
@@ -49,14 +94,11 @@ def fetch_historical_price_series(
4994
stock_factory=stock_factory,
5095
)
5196
ib.qualifyContracts(contract)
52-
bars = ib.reqHistoricalData(
97+
bars = _request_historical_bars(
98+
ib,
5399
contract,
54-
endDateTime="",
55-
durationStr=duration,
56-
barSizeSetting=bar_size,
57-
whatToShow="ADJUSTED_LAST",
58-
useRTH=True,
59-
formatDate=1,
100+
duration=duration,
101+
bar_size=bar_size,
60102
)
61103
points = tuple(
62104
PricePoint(as_of=_coerce_as_of(bar.date), close=float(bar.close))
@@ -82,14 +124,11 @@ def fetch_historical_price_candles(
82124
stock_factory=stock_factory,
83125
)
84126
ib.qualifyContracts(contract)
85-
bars = ib.reqHistoricalData(
127+
bars = _request_historical_bars(
128+
ib,
86129
contract,
87-
endDateTime="",
88-
durationStr=duration,
89-
barSizeSetting=bar_size,
90-
whatToShow="ADJUSTED_LAST",
91-
useRTH=True,
92-
formatDate=1,
130+
duration=duration,
131+
bar_size=bar_size,
93132
)
94133
return [
95134
{

tests/test_ibkr_market_data.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,40 @@ def test_fetch_historical_price_series_builds_price_points(self) -> None:
8585
self.assertEqual(series.points[-1].close, 101.0)
8686
self.assertEqual(ib.last_history_contract.symbol, "SPY")
8787
self.assertEqual(ib.last_history_kwargs["durationStr"], "2 Y")
88+
self.assertEqual(ib.last_history_kwargs["whatToShow"], "ADJUSTED_LAST")
89+
90+
def test_fetch_historical_price_series_converts_long_day_duration_to_years(self) -> None:
91+
ib = FakeIB()
92+
fetch_historical_price_series(
93+
ib,
94+
"SOXL",
95+
duration="420 D",
96+
stock_factory=FakeContract,
97+
)
98+
99+
self.assertEqual(ib.last_history_kwargs["durationStr"], "2 Y")
100+
101+
def test_fetch_historical_price_series_falls_back_to_trades_when_adjusted_last_is_empty(self) -> None:
102+
class AdjustedLastEmptyIB(FakeIB):
103+
def __init__(self):
104+
super().__init__()
105+
self.history_calls = []
106+
107+
def reqHistoricalData(self, contract, **kwargs):
108+
self.history_calls.append(kwargs)
109+
if kwargs["whatToShow"] == "ADJUSTED_LAST":
110+
return []
111+
return super().reqHistoricalData(contract, **kwargs)
112+
113+
ib = AdjustedLastEmptyIB()
114+
series = fetch_historical_price_series(
115+
ib,
116+
"QQQ",
117+
stock_factory=FakeContract,
118+
)
119+
120+
self.assertEqual(series.points[-1].close, 101.0)
121+
self.assertEqual([call["whatToShow"] for call in ib.history_calls], ["ADJUSTED_LAST", "TRADES"])
88122

89123
def test_fetch_historical_price_candles_exposes_ohlc_fields(self) -> None:
90124
ib = FakeIB()

0 commit comments

Comments
 (0)