Skip to content

Commit 4ff8cf1

Browse files
committed
Add IBKR benchmark history runtime helpers
1 parent 95d288c commit 4ff8cf1

5 files changed

Lines changed: 107 additions & 4 deletions

File tree

src/quant_platform_kit/ibkr/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
from .connection import connect_ib, ensure_event_loop
22
from .execution import submit_order_intent
3-
from .market_data import fetch_historical_price_series, fetch_quote_snapshots
3+
from .market_data import (
4+
fetch_historical_price_candles,
5+
fetch_historical_price_series,
6+
fetch_quote_snapshots,
7+
)
48
from .portfolio import fetch_portfolio_snapshot
59
from .runtime_inputs import (
10+
build_benchmark_history_inputs,
611
build_ibkr_strategy_context,
712
build_market_history_inputs,
813
build_semiconductor_rotation_indicators,
914
build_semiconductor_rotation_inputs,
1015
)
1116

1217
__all__ = [
18+
"build_benchmark_history_inputs",
1319
"build_ibkr_strategy_context",
1420
"build_market_history_inputs",
1521
"build_semiconductor_rotation_indicators",
1622
"build_semiconductor_rotation_inputs",
1723
"connect_ib",
1824
"ensure_event_loop",
25+
"fetch_historical_price_candles",
1926
"submit_order_intent",
2027
"fetch_historical_price_series",
2128
"fetch_quote_snapshots",

src/quant_platform_kit/ibkr/market_data.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,45 @@ def fetch_historical_price_series(
6565
return PriceSeries(symbol=symbol, currency=currency, points=points)
6666

6767

68+
def fetch_historical_price_candles(
69+
ib: Any,
70+
symbol: str,
71+
*,
72+
duration: str = "2 Y",
73+
bar_size: str = "1 day",
74+
exchange: str = "SMART",
75+
currency: str = "USD",
76+
stock_factory: Callable[..., Any] | None = None,
77+
) -> list[dict[str, Any]]:
78+
contract = _build_stock_contract(
79+
symbol,
80+
exchange=exchange,
81+
currency=currency,
82+
stock_factory=stock_factory,
83+
)
84+
ib.qualifyContracts(contract)
85+
bars = ib.reqHistoricalData(
86+
contract,
87+
endDateTime="",
88+
durationStr=duration,
89+
barSizeSetting=bar_size,
90+
whatToShow="ADJUSTED_LAST",
91+
useRTH=True,
92+
formatDate=1,
93+
)
94+
return [
95+
{
96+
"as_of": _coerce_as_of(bar.date),
97+
"open": float(getattr(bar, "open", bar.close)),
98+
"high": float(getattr(bar, "high", bar.close)),
99+
"low": float(getattr(bar, "low", bar.close)),
100+
"close": float(bar.close),
101+
"volume": float(getattr(bar, "volume", 0.0) or 0.0),
102+
}
103+
for bar in bars or ()
104+
]
105+
106+
68107
def _coerce_positive_price(value: Any) -> float | None:
69108
if value is None:
70109
return None

src/quant_platform_kit/ibkr/runtime_inputs.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,24 @@ def build_market_history_inputs(
1919
return {"market_history": historical_close_loader}
2020

2121

22+
def build_benchmark_history_inputs(
23+
ib: Any,
24+
historical_candle_loader: Callable[..., Any],
25+
*,
26+
benchmark_symbol: str,
27+
duration: str = "2 Y",
28+
bar_size: str = "1 day",
29+
) -> dict[str, Any]:
30+
return {
31+
"benchmark_history": historical_candle_loader(
32+
ib,
33+
benchmark_symbol,
34+
duration=duration,
35+
bar_size=bar_size,
36+
)
37+
}
38+
39+
2240
def build_ibkr_strategy_context(
2341
*,
2442
entrypoint: StrategyEntrypoint,

tests/test_ibkr_market_data.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from datetime import date
55
import unittest
66

7-
from quant_platform_kit.ibkr.market_data import fetch_historical_price_series, fetch_quote_snapshots
7+
from quant_platform_kit.ibkr.market_data import (
8+
fetch_historical_price_candles,
9+
fetch_historical_price_series,
10+
fetch_quote_snapshots,
11+
)
812

913

1014
@dataclass
@@ -18,6 +22,10 @@ class FakeContract:
1822
class FakeBar:
1923
date: date
2024
close: float
25+
open: float = 0.0
26+
high: float = 0.0
27+
low: float = 0.0
28+
volume: float = 0.0
2129

2230

2331
class FakeTicker:
@@ -52,8 +60,8 @@ def reqHistoricalData(self, contract, **kwargs):
5260
self.last_history_contract = contract
5361
self.last_history_kwargs = kwargs
5462
return [
55-
FakeBar(date=date(2026, 3, 27), close=100.5),
56-
FakeBar(date=date(2026, 3, 28), close=101.0),
63+
FakeBar(date=date(2026, 3, 27), open=100.0, high=101.0, low=99.5, close=100.5, volume=1000.0),
64+
FakeBar(date=date(2026, 3, 28), open=100.5, high=101.5, low=100.0, close=101.0, volume=1200.0),
5765
]
5866

5967
def reqMktData(self, contract, *_args):
@@ -78,6 +86,20 @@ def test_fetch_historical_price_series_builds_price_points(self) -> None:
7886
self.assertEqual(ib.last_history_contract.symbol, "SPY")
7987
self.assertEqual(ib.last_history_kwargs["durationStr"], "2 Y")
8088

89+
def test_fetch_historical_price_candles_exposes_ohlc_fields(self) -> None:
90+
ib = FakeIB()
91+
candles = fetch_historical_price_candles(
92+
ib,
93+
"QQQ",
94+
stock_factory=FakeContract,
95+
)
96+
97+
self.assertEqual(candles[-1]["close"], 101.0)
98+
self.assertEqual(candles[-1]["open"], 100.5)
99+
self.assertEqual(candles[-1]["high"], 101.5)
100+
self.assertEqual(candles[-1]["low"], 100.0)
101+
self.assertEqual(candles[-1]["volume"], 1200.0)
102+
81103
def test_fetch_quote_snapshots_returns_last_price(self) -> None:
82104
ib = FakeIB()
83105
snapshots = fetch_quote_snapshots(

tests/test_ibkr_runtime_inputs.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from quant_platform_kit.strategy_contracts import StrategyManifest, StrategyRuntimeAdapter
66
from quant_platform_kit.ibkr.runtime_inputs import (
7+
build_benchmark_history_inputs,
78
build_ibkr_strategy_context,
89
build_market_history_inputs,
910
build_semiconductor_rotation_indicators,
@@ -21,6 +22,22 @@ def loader(*_args, **_kwargs):
2122
self.assertEqual(set(payload), {"market_history"})
2223
self.assertIs(payload["market_history"], loader)
2324

25+
def test_build_benchmark_history_inputs_loads_candles_for_symbol(self) -> None:
26+
observed = {}
27+
28+
def loader(_ib, symbol, duration="2 Y", bar_size="1 day"):
29+
observed["call"] = (symbol, duration, bar_size)
30+
return [{"close": 1.0, "high": 1.1, "low": 0.9}]
31+
32+
payload = build_benchmark_history_inputs(
33+
"fake-ib",
34+
loader,
35+
benchmark_symbol="QQQ",
36+
)
37+
38+
self.assertEqual(observed["call"], ("QQQ", "2 Y", "1 day"))
39+
self.assertEqual(payload["benchmark_history"][0]["close"], 1.0)
40+
2441
def test_build_ibkr_strategy_context_uses_required_inputs_and_portfolio(self) -> None:
2542
entrypoint = type(
2643
"Entrypoint",

0 commit comments

Comments
 (0)