Skip to content

Commit fdab0a4

Browse files
authored
Materialize IBKR HK market history inputs
1 parent 956a011 commit fdab0a4

2 files changed

Lines changed: 127 additions & 2 deletions

File tree

strategy_runtime.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,34 @@
8080
}
8181

8282

83+
def _get_direct_market_history_profiles() -> frozenset[str]:
84+
try:
85+
from hk_equity_strategies import get_direct_market_history_profiles
86+
except (ImportError, AttributeError): # pragma: no cover - compatibility fallback
87+
return frozenset()
88+
return frozenset(
89+
str(profile).strip().lower()
90+
for profile in get_direct_market_history_profiles()
91+
)
92+
93+
94+
def _requires_materialized_market_history(strategy_profile: str) -> bool:
95+
return str(strategy_profile or "").strip().lower() in _get_direct_market_history_profiles()
96+
97+
98+
def _loaded_history_to_rows(history):
99+
if (
100+
hasattr(history, "items")
101+
and not hasattr(history, "columns")
102+
and not isinstance(history, Mapping)
103+
):
104+
return [
105+
{"date": date_value, "close": close_value}
106+
for date_value, close_value in history.items()
107+
]
108+
return history
109+
110+
83111
@dataclass(frozen=True)
84112
class StrategyEvaluationResult:
85113
decision: StrategyDecision
@@ -310,6 +338,37 @@ def _build_historical_close_map(
310338
close_map[symbol] = latest_close
311339
return close_map
312340

341+
def _market_history_symbols(self) -> tuple[str, ...]:
342+
raw_symbols = (
343+
self.merged_runtime_config.get("universe_symbols")
344+
or dict(getattr(self.entrypoint.manifest, "default_config", {}) or {}).get("universe_symbols")
345+
or self.merged_runtime_config.get("managed_symbols")
346+
or ()
347+
)
348+
if isinstance(raw_symbols, str):
349+
raw_symbols = raw_symbols.replace(";", ",").split(",")
350+
return tuple(
351+
dict.fromkeys(
352+
str(symbol).strip()
353+
for symbol in raw_symbols
354+
if str(symbol).strip()
355+
)
356+
)
357+
358+
def _build_market_history_inputs(
359+
self,
360+
ib,
361+
historical_close_loader: Callable[..., Any],
362+
) -> Mapping[str, Any]:
363+
if not _requires_materialized_market_history(self.profile):
364+
return build_market_history_inputs(historical_close_loader)
365+
return {
366+
_MARKET_HISTORY_INPUT: {
367+
symbol: _loaded_history_to_rows(historical_close_loader(ib, symbol))
368+
for symbol in self._market_history_symbols()
369+
}
370+
}
371+
313372
def _build_strategy_context(
314373
self,
315374
*,
@@ -429,7 +488,7 @@ def _evaluate_market_data_strategy(
429488
ctx = self._build_strategy_context(
430489
runtime_adapter=self.runtime_adapter,
431490
as_of=run_as_of,
432-
market_inputs=build_market_history_inputs(historical_close_loader),
491+
market_inputs=self._build_market_history_inputs(ib, historical_close_loader),
433492
portfolio_snapshot=portfolio_snapshot,
434493
runtime_config=runtime_config,
435494
current_holdings=current_holdings,
@@ -619,7 +678,7 @@ def build_available_inputs(feature_snapshot) -> Mapping[str, Any]:
619678
runtime_config["option_chains"] = option_chains
620679
market_inputs: dict[str, Any] = {_FEATURE_SNAPSHOT_INPUT: feature_snapshot}
621680
if _MARKET_HISTORY_INPUT in self.required_inputs:
622-
market_inputs.update(build_market_history_inputs(historical_close_loader))
681+
market_inputs.update(self._build_market_history_inputs(ib, historical_close_loader))
623682
if _BENCHMARK_HISTORY_INPUT in self.required_inputs:
624683
if historical_candle_loader is None:
625684
raise ValueError(

tests/test_strategy_runtime.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,72 @@ def loader(*_args, **_kwargs):
543543
assert result.metadata["execution_timing_contract"] == "next_trading_day"
544544

545545

546+
def test_hk_direct_market_history_runtime_materializes_loader(monkeypatch):
547+
captured = {}
548+
observed_calls = []
549+
550+
class FakeEntrypoint:
551+
manifest = StrategyManifest(
552+
profile="hk_listed_global_etf_rotation",
553+
domain="hk_equity",
554+
display_name="HK-listed Global ETF Rotation",
555+
description="test",
556+
required_inputs=frozenset({"market_history"}),
557+
default_config={"universe_symbols": ("02800", "02834")},
558+
)
559+
560+
def evaluate(self, ctx):
561+
captured["market_data"] = dict(ctx.market_data)
562+
return StrategyDecision()
563+
564+
def close_loader(ib, symbol, **_kwargs):
565+
observed_calls.append((ib, symbol))
566+
return strategy_runtime_module.pd.Series(
567+
[10.0, 11.0],
568+
index=strategy_runtime_module.pd.to_datetime(
569+
["2026-05-29", "2026-06-01"],
570+
utc=True,
571+
),
572+
dtype=float,
573+
)
574+
575+
runtime = strategy_runtime_module.LoadedStrategyRuntime(
576+
entrypoint=FakeEntrypoint(),
577+
runtime_adapter=StrategyRuntimeAdapter(
578+
status_icon="🇭🇰",
579+
runtime_policy=StrategyRuntimePolicy(signal_effective_after_trading_days=1),
580+
),
581+
runtime_settings=_build_runtime_settings(profile="hk_listed_global_etf_rotation"),
582+
runtime_config={},
583+
merged_runtime_config={"universe_symbols": ("02800", "02834")},
584+
status_icon="🇭🇰",
585+
logger=lambda _message: None,
586+
)
587+
monkeypatch.setattr(
588+
strategy_runtime_module,
589+
"_requires_materialized_market_history",
590+
lambda profile: profile == "hk_listed_global_etf_rotation",
591+
)
592+
593+
runtime.evaluate(
594+
ib="fake-ib",
595+
current_holdings=set(),
596+
historical_close_loader=close_loader,
597+
run_as_of=strategy_runtime_module.pd.Timestamp("2026-04-01"),
598+
translator=lambda key, **_kwargs: key,
599+
pacing_sec=0.5,
600+
)
601+
602+
market_history = captured["market_data"]["market_history"]
603+
assert observed_calls == [("fake-ib", "02800"), ("fake-ib", "02834")]
604+
assert sorted(market_history) == ["02800", "02834"]
605+
assert market_history["02800"][0]["date"] == strategy_runtime_module.pd.Timestamp(
606+
"2026-05-29",
607+
tz="UTC",
608+
)
609+
assert market_history["02800"][0]["close"] == 10.0
610+
611+
546612
def test_market_history_value_runtime_requires_portfolio_snapshot(monkeypatch):
547613
captured = {}
548614

0 commit comments

Comments
 (0)