Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,34 @@
}


def _get_direct_market_history_profiles() -> frozenset[str]:
try:
from hk_equity_strategies import get_direct_market_history_profiles

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use a helper available in the pinned HK package

With the current requirements.txt pin for hk-equity-strategies (71141ce9...), this top-level get_direct_market_history_profiles symbol is not exported, so the import falls into the compatibility fallback and _requires_materialized_market_history() is false for hk_listed_global_etf_rotation. In that deployed environment the materialization path never runs and the HK strategy still receives the loader callable as market_history, which its direct market-history implementation treats as row/mapping input, causing HK dry runs to fail before producing targets; please either update the dependency pin or keep a local fallback allow-list for the enabled HK direct profile.

Useful? React with 👍 / 👎.

except (ImportError, AttributeError): # pragma: no cover - compatibility fallback
return frozenset()
return frozenset(
str(profile).strip().lower()
for profile in get_direct_market_history_profiles()
)


def _requires_materialized_market_history(strategy_profile: str) -> bool:
return str(strategy_profile or "").strip().lower() in _get_direct_market_history_profiles()


def _loaded_history_to_rows(history):
if (
hasattr(history, "items")
and not hasattr(history, "columns")
and not isinstance(history, Mapping)
):
return [
{"date": date_value, "close": close_value}
for date_value, close_value in history.items()
]
return history


@dataclass(frozen=True)
class StrategyEvaluationResult:
decision: StrategyDecision
Expand Down Expand Up @@ -310,6 +338,37 @@ def _build_historical_close_map(
close_map[symbol] = latest_close
return close_map

def _market_history_symbols(self) -> tuple[str, ...]:
raw_symbols = (
self.merged_runtime_config.get("universe_symbols")
or dict(getattr(self.entrypoint.manifest, "default_config", {}) or {}).get("universe_symbols")
or self.merged_runtime_config.get("managed_symbols")
or ()
)
if isinstance(raw_symbols, str):
raw_symbols = raw_symbols.replace(";", ",").split(",")
return tuple(
dict.fromkeys(
str(symbol).strip()
for symbol in raw_symbols
if str(symbol).strip()
)
)

def _build_market_history_inputs(
self,
ib,
historical_close_loader: Callable[..., Any],
) -> Mapping[str, Any]:
if not _requires_materialized_market_history(self.profile):
return build_market_history_inputs(historical_close_loader)
return {
_MARKET_HISTORY_INPUT: {
symbol: _loaded_history_to_rows(historical_close_loader(ib, symbol))
for symbol in self._market_history_symbols()
}
}

def _build_strategy_context(
self,
*,
Expand Down Expand Up @@ -429,7 +488,7 @@ def _evaluate_market_data_strategy(
ctx = self._build_strategy_context(
runtime_adapter=self.runtime_adapter,
as_of=run_as_of,
market_inputs=build_market_history_inputs(historical_close_loader),
market_inputs=self._build_market_history_inputs(ib, historical_close_loader),
portfolio_snapshot=portfolio_snapshot,
runtime_config=runtime_config,
current_holdings=current_holdings,
Expand Down Expand Up @@ -619,7 +678,7 @@ def build_available_inputs(feature_snapshot) -> Mapping[str, Any]:
runtime_config["option_chains"] = option_chains
market_inputs: dict[str, Any] = {_FEATURE_SNAPSHOT_INPUT: feature_snapshot}
if _MARKET_HISTORY_INPUT in self.required_inputs:
market_inputs.update(build_market_history_inputs(historical_close_loader))
market_inputs.update(self._build_market_history_inputs(ib, historical_close_loader))
if _BENCHMARK_HISTORY_INPUT in self.required_inputs:
if historical_candle_loader is None:
raise ValueError(
Expand Down
66 changes: 66 additions & 0 deletions tests/test_strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,72 @@ def loader(*_args, **_kwargs):
assert result.metadata["execution_timing_contract"] == "next_trading_day"


def test_hk_direct_market_history_runtime_materializes_loader(monkeypatch):
captured = {}
observed_calls = []

class FakeEntrypoint:
manifest = StrategyManifest(
profile="hk_listed_global_etf_rotation",
domain="hk_equity",
display_name="HK-listed Global ETF Rotation",
description="test",
required_inputs=frozenset({"market_history"}),
default_config={"universe_symbols": ("02800", "02834")},
)

def evaluate(self, ctx):
captured["market_data"] = dict(ctx.market_data)
return StrategyDecision()

def close_loader(ib, symbol, **_kwargs):
observed_calls.append((ib, symbol))
return strategy_runtime_module.pd.Series(
[10.0, 11.0],
index=strategy_runtime_module.pd.to_datetime(
["2026-05-29", "2026-06-01"],
utc=True,
),
dtype=float,
)

runtime = strategy_runtime_module.LoadedStrategyRuntime(
entrypoint=FakeEntrypoint(),
runtime_adapter=StrategyRuntimeAdapter(
status_icon="🇭🇰",
runtime_policy=StrategyRuntimePolicy(signal_effective_after_trading_days=1),
),
runtime_settings=_build_runtime_settings(profile="hk_listed_global_etf_rotation"),
runtime_config={},
merged_runtime_config={"universe_symbols": ("02800", "02834")},
status_icon="🇭🇰",
logger=lambda _message: None,
)
monkeypatch.setattr(
strategy_runtime_module,
"_requires_materialized_market_history",
lambda profile: profile == "hk_listed_global_etf_rotation",
)

runtime.evaluate(
ib="fake-ib",
current_holdings=set(),
historical_close_loader=close_loader,
run_as_of=strategy_runtime_module.pd.Timestamp("2026-04-01"),
translator=lambda key, **_kwargs: key,
pacing_sec=0.5,
)

market_history = captured["market_data"]["market_history"]
assert observed_calls == [("fake-ib", "02800"), ("fake-ib", "02834")]
assert sorted(market_history) == ["02800", "02834"]
assert market_history["02800"][0]["date"] == strategy_runtime_module.pd.Timestamp(
"2026-05-29",
tz="UTC",
)
assert market_history["02800"][0]["close"] == 10.0


def test_market_history_value_runtime_requires_portfolio_snapshot(monkeypatch):
captured = {}

Expand Down