Skip to content

Commit 57e42b2

Browse files
authored
Materialize HK market history inputs
1 parent f0fd6e6 commit 57e42b2

2 files changed

Lines changed: 117 additions & 2 deletions

File tree

‎application/runtime_strategy_adapters.py‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@
1414
)
1515

1616

17+
def _get_direct_market_history_profiles() -> frozenset[str]:
18+
try:
19+
from hk_equity_strategies import get_direct_market_history_profiles
20+
except (ImportError, AttributeError): # pragma: no cover - compatibility fallback
21+
return frozenset()
22+
return frozenset(
23+
str(profile).strip().lower()
24+
for profile in get_direct_market_history_profiles()
25+
)
26+
27+
28+
def _requires_materialized_market_history(strategy_profile: str) -> bool:
29+
return str(strategy_profile or "").strip().lower() in _get_direct_market_history_profiles()
30+
31+
32+
def _loaded_history_to_rows(history):
33+
if (
34+
hasattr(history, "items")
35+
and not hasattr(history, "columns")
36+
and not isinstance(history, Mapping)
37+
):
38+
return [
39+
{"date": date_value, "close": close_value}
40+
for date_value, close_value in history.items()
41+
]
42+
return history
43+
44+
1745
@dataclass(frozen=True)
1846
class LongBridgeRuntimeStrategyAdapters:
1947
strategy_runtime: Any
@@ -80,8 +108,13 @@ def calculate_strategy_indicators(self, quote_context):
80108
if "market_history" in available_inputs or "benchmark_history" in available_inputs or "qqq_history" in available_inputs:
81109
market_data_port = self.broker_adapters.build_market_data_port(quote_context)
82110
if "market_history" in available_inputs:
111+
market_history = (
112+
self._build_materialized_market_history(market_data_port)
113+
if _requires_materialized_market_history(self.strategy_profile)
114+
else self.broker_adapters.build_market_history_loader(market_data_port)
115+
)
83116
market_inputs = {
84-
"market_history": self.broker_adapters.build_market_history_loader(market_data_port),
117+
"market_history": market_history,
85118
}
86119
if "benchmark_history" in available_inputs:
87120
market_inputs["benchmark_history"] = self.broker_adapters.build_price_history(
@@ -99,6 +132,29 @@ def calculate_strategy_indicators(self, quote_context):
99132
trend_ma_window = int(self.strategy_runtime_config.get("trend_ma_window", 150))
100133
return self.calculate_rotation_indicators_fn(quote_context, trend_window=trend_ma_window)
101134

135+
def _market_history_symbols(self) -> tuple[str, ...]:
136+
raw_symbols = (
137+
self.strategy_runtime_config.get("universe_symbols")
138+
or getattr(self.broker_adapters, "strategy_symbols", ())
139+
or getattr(self.strategy_runtime, "managed_symbols", ())
140+
)
141+
if isinstance(raw_symbols, str):
142+
raw_symbols = raw_symbols.replace(";", ",").split(",")
143+
return tuple(
144+
dict.fromkeys(
145+
str(symbol).strip()
146+
for symbol in raw_symbols
147+
if str(symbol).strip()
148+
)
149+
)
150+
151+
def _build_materialized_market_history(self, market_data_port):
152+
load_market_history = self.broker_adapters.build_market_history_loader(market_data_port)
153+
return {
154+
symbol: _loaded_history_to_rows(load_market_history(None, symbol))
155+
for symbol in self._market_history_symbols()
156+
}
157+
102158
def resolve_rebalance_plan(self, *, indicators, snapshot=None, account_state=None):
103159
available_inputs = set(self.available_inputs)
104160
resolved_snapshot = snapshot

‎tests/test_runtime_strategy_adapters.py‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22
from pathlib import Path
33
from types import SimpleNamespace
44

5+
import pandas as pd
6+
57

68
ROOT = Path(__file__).resolve().parents[1]
79
if str(ROOT) not in sys.path:
810
sys.path.insert(0, str(ROOT))
11+
HK_STRATEGIES_SRC = ROOT.parent / "HkEquityStrategies" / "src"
12+
if str(HK_STRATEGIES_SRC) not in sys.path:
13+
sys.path.insert(0, str(HK_STRATEGIES_SRC))
914

1015
from application.runtime_strategy_adapters import build_runtime_strategy_adapters
1116

@@ -35,7 +40,9 @@ def build_price_history(self, market_data_port, symbol):
3540
signal_text_fn=lambda icon: f"signal:{icon}",
3641
translator=lambda key, **_kwargs: key,
3742
broker_adapters=FakeBrokerAdapters(),
38-
calculate_rotation_indicators_fn=lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("unexpected fallback")),
43+
calculate_rotation_indicators_fn=lambda *_args, **_kwargs: (
44+
_ for _ in ()
45+
).throw(AssertionError("unexpected fallback")),
3946
build_strategy_evaluation_inputs_fn=lambda **_kwargs: {},
4047
map_strategy_decision_to_plan_fn=lambda *_args, **_kwargs: {},
4148
)
@@ -52,6 +59,58 @@ def build_price_history(self, market_data_port, symbol):
5259
}
5360

5461

62+
def test_runtime_strategy_adapters_materialize_hk_direct_market_history():
63+
observed = {}
64+
65+
class FakeBrokerAdapters:
66+
strategy_symbols = ("02800", "02834")
67+
68+
def build_market_data_port(self, quote_context):
69+
observed["market_data_port_context"] = quote_context
70+
return "market-data-port"
71+
72+
def build_market_history_loader(self, market_data_port):
73+
observed["market_history_loader_port"] = market_data_port
74+
75+
def load_market_history(_broker_client, symbol):
76+
observed.setdefault("history_calls", []).append(symbol)
77+
return pd.Series(
78+
[10.0, 11.0],
79+
index=pd.to_datetime(["2026-05-29", "2026-06-01"], utc=True),
80+
dtype=float,
81+
)
82+
83+
return load_market_history
84+
85+
adapters = build_runtime_strategy_adapters(
86+
strategy_runtime=SimpleNamespace(evaluate=lambda **_kwargs: None),
87+
strategy_profile="hk_listed_global_etf_rotation",
88+
strategy_runtime_config={"universe_symbols": ("02800", "02834")},
89+
available_inputs=("market_history",),
90+
benchmark_symbol="QQQ",
91+
signal_text_fn=lambda icon: f"signal:{icon}",
92+
translator=lambda key, **_kwargs: key,
93+
broker_adapters=FakeBrokerAdapters(),
94+
calculate_rotation_indicators_fn=lambda *_args, **_kwargs: (
95+
_ for _ in ()
96+
).throw(AssertionError("unexpected fallback")),
97+
build_strategy_evaluation_inputs_fn=lambda **_kwargs: {},
98+
map_strategy_decision_to_plan_fn=lambda *_args, **_kwargs: {},
99+
)
100+
101+
result = adapters.calculate_strategy_indicators("quote-context")
102+
103+
assert observed["market_data_port_context"] == "quote-context"
104+
assert observed["market_history_loader_port"] == "market-data-port"
105+
assert observed["history_calls"] == ["02800", "02834"]
106+
assert sorted(result["market_history"]) == ["02800", "02834"]
107+
assert result["market_history"]["02800"][0]["date"] == pd.Timestamp(
108+
"2026-05-29",
109+
tz="UTC",
110+
)
111+
assert result["market_history"]["02800"][0]["close"] == 10.0
112+
113+
55114
def test_runtime_strategy_adapters_fall_back_to_rotation_indicators():
56115
observed = {}
57116

0 commit comments

Comments
 (0)