Skip to content

Commit 4d1d7ee

Browse files
Pigbibicodex
andcommitted
fix: keep theme artifact readers version compatible
Co-Authored-By: Codex <noreply@openai.com>
1 parent dd5722f commit 4d1d7ee

3 files changed

Lines changed: 61 additions & 2 deletions

File tree

src/research_signal_context_pipelines/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from .context_bundle import DEFAULT_UNIVERSE, build_context_bundle, build_context_from_source
55
from .price_history import PriceExtractionSummary, write_filtered_price_history
66
from .schema import SignalValidationError, validate_signal
7-
from .theme_momentum import build_theme_momentum_snapshot, write_theme_momentum_snapshot
7+
from .theme_momentum import (
8+
build_theme_momentum_snapshot,
9+
validate_theme_momentum_snapshot,
10+
write_theme_momentum_snapshot,
11+
)
812
from .theme_universe import build_theme_context, load_symbol_theme_exposure, load_theme_taxonomy
913

1014
__all__ = [
@@ -20,6 +24,7 @@
2024
"load_symbol_theme_exposure",
2125
"load_theme_taxonomy",
2226
"validate_signal",
27+
"validate_theme_momentum_snapshot",
2328
"write_filtered_price_history",
2429
"write_theme_momentum_snapshot",
2530
]

src/research_signal_context_pipelines/theme_momentum.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,29 @@ def build_theme_momentum_snapshot(
271271
}
272272

273273

274+
def validate_theme_momentum_snapshot(snapshot: Mapping[str, Any]) -> None:
275+
"""Validate the stable metadata and core shape of v1/v2 theme artifacts."""
276+
required = ("schema_version", "as_of", "generated_at", "mode", "artifact_type", "theme_ranks", "data_quality", "policy")
277+
missing = [key for key in required if key not in snapshot]
278+
if missing:
279+
raise ValueError(f"theme momentum snapshot missing required keys: {', '.join(missing)}")
280+
schema_version = str(snapshot["schema_version"])
281+
if schema_version not in {"1", "2"}:
282+
raise ValueError("theme momentum snapshot schema_version must be '1' or '2'")
283+
parse_price_date(snapshot["as_of"])
284+
generated_at = snapshot["generated_at"]
285+
if not isinstance(generated_at, str) or not generated_at.strip():
286+
raise ValueError("theme momentum snapshot generated_at must be a non-empty string")
287+
if schema_version == "2":
288+
for key in ("expires_at", "model_version", "scoring_version"):
289+
if not isinstance(snapshot.get(key), str) or not snapshot[key].strip():
290+
raise ValueError(f"theme momentum snapshot {key} must be a non-empty string")
291+
if parse_price_date(snapshot["expires_at"]) < parse_price_date(snapshot["as_of"]):
292+
raise ValueError("theme momentum snapshot expires_at must not be before as_of")
293+
if not isinstance(snapshot["theme_ranks"], list) or not isinstance(snapshot["data_quality"], Mapping):
294+
raise ValueError("theme momentum snapshot core shape is invalid")
295+
296+
274297
def write_theme_momentum_snapshot(snapshot: Mapping[str, Any], path: str | Path) -> Path:
275298
output_path = Path(path)
276299
output_path.parent.mkdir(parents=True, exist_ok=True)

tests/test_theme_momentum.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import datetime as dt
44

55
from research_signal_context_pipelines.price_history import PriceRow
6-
from research_signal_context_pipelines.theme_momentum import build_theme_momentum_snapshot
6+
from research_signal_context_pipelines.theme_momentum import (
7+
build_theme_momentum_snapshot,
8+
validate_theme_momentum_snapshot,
9+
)
710
from research_signal_context_pipelines.theme_universe import SymbolThemeExposure, ThemeDefinition
811

912

@@ -100,3 +103,31 @@ def test_theme_momentum_records_missing_price_coverage() -> None:
100103
assert snapshot["data_quality"]["coverage"]["price_coverage_ratio"] == 0.5
101104
assert snapshot["theme_ranks"][0]["component_count"] == 2
102105
assert snapshot["theme_ranks"][0]["priced_symbol_count"] == 1
106+
107+
108+
def test_theme_momentum_validator_reads_v1_and_v2_metadata() -> None:
109+
themes = {
110+
"hbm_memory": ThemeDefinition(
111+
taxonomy_version="test-v1",
112+
theme_id="hbm_memory",
113+
theme_name="HBM and memory",
114+
sector="technology",
115+
horizon="6-24 months",
116+
description="memory theme",
117+
source_policy="primary evidence required",
118+
)
119+
}
120+
exposures = {"MU": SymbolThemeExposure("MU", ("hbm_memory",), "high", "memory exposure")}
121+
snapshot = build_theme_momentum_snapshot(
122+
_trend_rows("MU", start_close=50, daily_step=0.22),
123+
themes=themes,
124+
exposures=exposures,
125+
generated_at=dt.datetime(2026, 1, 1, tzinfo=dt.timezone.utc),
126+
)
127+
128+
validate_theme_momentum_snapshot(snapshot)
129+
legacy = dict(snapshot)
130+
legacy["schema_version"] = "1"
131+
for key in ("expires_at", "model_version", "scoring_version"):
132+
legacy.pop(key)
133+
validate_theme_momentum_snapshot(legacy)

0 commit comments

Comments
 (0)