Skip to content

Commit 66cbce0

Browse files
authored
Merge pull request #512 from QuantStrategyLab/fix/managed-symbol-extractor-compat
fix: adapt managed-symbol extractor safe-haven keywords
2 parents 169f103 + 2e157e6 commit 66cbce0

2 files changed

Lines changed: 102 additions & 2 deletions

File tree

src/quant_platform_kit/common/feature_snapshot_runtime.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import inspect
34
from dataclasses import dataclass, field
45
from datetime import datetime, timezone
56
from typing import Any, Callable, Mapping
@@ -266,15 +267,57 @@ def extract_feature_snapshot_managed_symbols(
266267
return tuple(
267268
extractor(
268269
feature_snapshot,
269-
benchmark_symbol=benchmark_symbol,
270-
safe_haven=safe_haven_symbol,
270+
**_managed_symbols_extractor_kwargs(
271+
extractor,
272+
benchmark_symbol=benchmark_symbol,
273+
safe_haven_symbol=safe_haven_symbol,
274+
),
271275
)
272276
)
273277
if safe_haven_symbol:
274278
return (safe_haven_symbol,)
275279
return fallback_symbols
276280

277281

282+
def _managed_symbols_extractor_kwargs(
283+
extractor: Callable[..., Any],
284+
*,
285+
benchmark_symbol: str,
286+
safe_haven_symbol: str | None,
287+
) -> dict[str, Any]:
288+
"""Adapt the two reviewed safe-haven keyword spellings at the plug-in edge.
289+
290+
Strategy plug-ins are independently versioned. The historical contract
291+
accepted ``safe_haven`` while the global-ETF extractor uses the more
292+
explicit ``safe_haven_symbol``. Inspecting the callable before calling it
293+
preserves both contracts without catching a TypeError raised *inside* the
294+
plug-in itself.
295+
"""
296+
297+
try:
298+
parameters = inspect.signature(extractor).parameters.values()
299+
except (TypeError, ValueError):
300+
# Opaque callables retain the original, documented keyword spelling.
301+
return {
302+
"benchmark_symbol": benchmark_symbol,
303+
"safe_haven": safe_haven_symbol,
304+
}
305+
306+
accepted = {parameter.name for parameter in parameters}
307+
accepts_kwargs = any(
308+
parameter.kind is inspect.Parameter.VAR_KEYWORD
309+
for parameter in parameters
310+
)
311+
kwargs: dict[str, Any] = {}
312+
if "benchmark_symbol" in accepted or accepts_kwargs:
313+
kwargs["benchmark_symbol"] = benchmark_symbol
314+
if "safe_haven" in accepted or accepts_kwargs:
315+
kwargs["safe_haven"] = safe_haven_symbol
316+
elif "safe_haven_symbol" in accepted:
317+
kwargs["safe_haven_symbol"] = safe_haven_symbol
318+
return kwargs
319+
320+
278321
def _apply_runtime_policy(
279322
runtime_config: dict[str, Any],
280323
runtime_adapter: StrategyRuntimeAdapter,

tests/test_feature_snapshot_runtime.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
FeatureSnapshotContextRequest,
99
FeatureSnapshotRuntimeSettings,
1010
evaluate_feature_snapshot_strategy,
11+
extract_feature_snapshot_managed_symbols,
1112
)
1213
from quant_platform_kit.strategy_contracts import (
1314
CallableStrategyEntrypoint,
@@ -42,6 +43,62 @@ def _entrypoint() -> CallableStrategyEntrypoint:
4243

4344

4445
class FeatureSnapshotRuntimeTests(unittest.TestCase):
46+
def test_managed_symbol_extractor_accepts_safe_haven_symbol_contract(self) -> None:
47+
observed: dict[str, object] = {}
48+
49+
def extractor(
50+
_snapshot: object,
51+
*,
52+
benchmark_symbol: str | None = None,
53+
safe_haven_symbol: str | None = None,
54+
) -> tuple[str, ...]:
55+
observed["benchmark_symbol"] = benchmark_symbol
56+
observed["safe_haven_symbol"] = safe_haven_symbol
57+
return ("VT", str(safe_haven_symbol))
58+
59+
symbols = extract_feature_snapshot_managed_symbols(
60+
runtime_adapter=StrategyRuntimeAdapter(
61+
managed_symbols_extractor=extractor,
62+
),
63+
feature_snapshot=(),
64+
benchmark_symbol="VOO",
65+
safe_haven_symbol="BIL",
66+
)
67+
68+
self.assertEqual(symbols, ("VT", "BIL"))
69+
self.assertEqual(observed, {
70+
"benchmark_symbol": "VOO",
71+
"safe_haven_symbol": "BIL",
72+
})
73+
74+
def test_managed_symbol_extractor_retains_legacy_safe_haven_contract(self) -> None:
75+
observed: dict[str, object] = {}
76+
77+
def extractor(
78+
_snapshot: object,
79+
*,
80+
benchmark_symbol: str | None = None,
81+
safe_haven: str | None = None,
82+
) -> tuple[str, ...]:
83+
observed["benchmark_symbol"] = benchmark_symbol
84+
observed["safe_haven"] = safe_haven
85+
return ("QQQ", str(safe_haven))
86+
87+
symbols = extract_feature_snapshot_managed_symbols(
88+
runtime_adapter=StrategyRuntimeAdapter(
89+
managed_symbols_extractor=extractor,
90+
),
91+
feature_snapshot=(),
92+
benchmark_symbol="QQQ",
93+
safe_haven_symbol="BOXX",
94+
)
95+
96+
self.assertEqual(symbols, ("QQQ", "BOXX"))
97+
self.assertEqual(observed, {
98+
"benchmark_symbol": "QQQ",
99+
"safe_haven": "BOXX",
100+
})
101+
45102
def test_fail_closes_when_path_missing(self) -> None:
46103
result = evaluate_feature_snapshot_strategy(
47104
entrypoint=_entrypoint(),

0 commit comments

Comments
 (0)