Skip to content

Commit 0715fc8

Browse files
Pigbibicodex
andcommitted
feat: add point-in-time universe research contract
Co-Authored-By: Codex <noreply@openai.com>
1 parent 1b69250 commit 0715fc8

4 files changed

Lines changed: 307 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Point-in-time universe snapshot contract
2+
3+
`qsl.us-equity-point-in-time-universe.v1` is the smallest reusable input
4+
artifact needed before historical constituent strategies can claim a
5+
no-look-ahead replay. It binds:
6+
7+
- the named universe and effective date;
8+
- the timestamp when the source was available to a decision process;
9+
- the raw source-content SHA-256 and declared licence scope; and
10+
- the exact, sorted constituent symbols.
11+
12+
The decision-time validator rejects a snapshot whose `available_at` is later
13+
than the proposed decision. This is intentionally stronger than simply storing
14+
an “as of” date: a constituent list published after a rebalance cannot be used
15+
to make that earlier rebalance look better.
16+
17+
The artifact does not fetch iShares, a market-data vendor, or any website. It
18+
also does not prove adjusted prices, corporate actions, execution costs, or a
19+
strategy result. The first consumer is the future historical P1 reconstruction
20+
for the Russell/ETF combo research lane; it must pair every universe artifact
21+
with separately verified price and calendar inputs before P2/P3 may run.

src/market_signal_sources/artifacts/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@
9595
MARKET_SIGNAL_PLATFORM_PUBLICATION_SCHEMA_VERSION,
9696
publish_platform_signal_handoff,
9797
)
98+
from .point_in_time_universe import (
99+
POINT_IN_TIME_UNIVERSE_SCHEMA,
100+
PointInTimeUniverseError,
101+
build_point_in_time_universe_snapshot,
102+
calculate_point_in_time_universe_sha256,
103+
validate_point_in_time_universe_snapshot,
104+
validate_universe_snapshot_for_decision,
105+
)
98106
from .validation import (
99107
REQUIRED_INDICATOR_FIELDS_BY_CONSUMER,
100108
SignalBundleValidationError,
@@ -130,6 +138,7 @@
130138
"MARKET_SIGNAL_RUNTIME_INJECTION_PLAN_SCHEMA_VERSION",
131139
"MARKET_SIGNAL_RUNTIME_PLAN_AUDIT_MATCH_SCHEMA_VERSION",
132140
"MARKET_SIGNAL_PLATFORM_PUBLICATION_SCHEMA_VERSION",
141+
"POINT_IN_TIME_UNIVERSE_SCHEMA",
133142
"RESEARCH_EXPORT_SCHEMA_VERSION",
134143
"SIGNAL_OWNERSHIP_MATRIX_SCHEMA_VERSION",
135144
"SIGNAL_SOURCE_FAMILY_CATALOG_MANIFEST_SCHEMA_VERSION",
@@ -140,11 +149,13 @@
140149
"SignalConsumerContractError",
141150
"SignalBundleValidationError",
142151
"QualityReportValidationError",
152+
"PointInTimeUniverseError",
143153
"build_btc_cycle_signal_bundle",
144154
"build_daily_technical_signal_bundle",
145155
"build_derived_indicator_signal_bundle",
146156
"build_semiconductor_rotation_signal_bundle",
147157
"build_ohlcv_quality_report",
158+
"build_point_in_time_universe_snapshot",
148159
"consumer_contract_for",
149160
"audit_signal_consumption",
150161
"compatible_profiles_for_signal_source_family",
@@ -180,6 +191,7 @@
180191
"validate_ohlcv_quality_report_file",
181192
"validate_platform_signal_handoff_index",
182193
"validate_platform_signal_handoff_manifest",
194+
"validate_point_in_time_universe_snapshot",
183195
"validate_consumption_audit_file",
184196
"validate_runtime_adapter_config",
185197
"validate_runtime_adapter_config_set_files",
@@ -189,6 +201,7 @@
189201
"validate_runtime_signal_injection_plan_file",
190202
"validate_runtime_signal_injection_plan_matches_audit",
191203
"write_research_export_manifest",
204+
"calculate_point_in_time_universe_sha256",
192205
"write_research_signal_handoff_manifest",
193206
"write_consumption_audit_artifact",
194207
"write_runtime_signal_injection_plan_artifact",
@@ -205,4 +218,5 @@
205218
"upsert_platform_signal_handoff_index",
206219
"publish_platform_signal_handoff",
207220
"runtime_signal_injection_plan",
221+
"validate_universe_snapshot_for_decision",
208222
]
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Canonical point-in-time universe snapshots for historical research inputs.
2+
3+
The artifact binds a constituent set to its source content digest and the time
4+
at which that set became available. It is deliberately a local pure contract:
5+
it does not fetch a provider, select securities, write storage, or enable a
6+
strategy/runtime lane.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import hashlib
12+
import json
13+
import re
14+
from collections.abc import Iterable, Mapping
15+
from datetime import UTC, date, datetime
16+
from typing import Any
17+
18+
POINT_IN_TIME_UNIVERSE_SCHEMA = "qsl.us-equity-point-in-time-universe.v1"
19+
_DIGEST = re.compile(r"^[0-9a-f]{64}$")
20+
_IDENTIFIER = re.compile(r"^[a-z0-9][a-z0-9_.-]{0,127}$")
21+
_SYMBOL = re.compile(r"^[A-Z][A-Z0-9.-]{0,14}$")
22+
_TIMESTAMP = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")
23+
_ROOT_FIELDS = frozenset(
24+
{
25+
"schema_version",
26+
"universe_id",
27+
"effective_date",
28+
"available_at",
29+
"source",
30+
"constituents",
31+
"snapshot_sha256",
32+
}
33+
)
34+
_SOURCE_FIELDS = frozenset({"source_id", "raw_artifact_sha256", "license_scope"})
35+
36+
37+
class PointInTimeUniverseError(ValueError):
38+
"""Raised when a universe snapshot cannot prove no-look-ahead eligibility."""
39+
40+
41+
def _fail(message: str) -> None:
42+
raise PointInTimeUniverseError(message)
43+
44+
45+
def _identifier(value: object, label: str) -> str:
46+
if not isinstance(value, str) or not _IDENTIFIER.fullmatch(value):
47+
_fail(f"invalid {label}")
48+
return value
49+
50+
51+
def _digest(value: object, label: str) -> str:
52+
if not isinstance(value, str) or not _DIGEST.fullmatch(value):
53+
_fail(f"invalid {label}")
54+
return value
55+
56+
57+
def _date(value: object, label: str) -> str:
58+
if not isinstance(value, str):
59+
_fail(f"invalid {label}")
60+
try:
61+
return date.fromisoformat(value).isoformat()
62+
except ValueError as exc:
63+
raise PointInTimeUniverseError(f"invalid {label}") from exc
64+
65+
66+
def _timestamp(value: object, label: str) -> datetime:
67+
if not isinstance(value, str) or not _TIMESTAMP.fullmatch(value):
68+
_fail(f"invalid {label}")
69+
try:
70+
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
71+
except ValueError as exc:
72+
raise PointInTimeUniverseError(f"invalid {label}") from exc
73+
74+
75+
def _constituents(value: object) -> tuple[str, ...]:
76+
if not isinstance(value, Iterable) or isinstance(value, (str, bytes, Mapping)):
77+
_fail("invalid constituents")
78+
items = tuple(value)
79+
if not items:
80+
_fail("empty constituents")
81+
normalized: list[str] = []
82+
for symbol in items:
83+
if not isinstance(symbol, str) or not _SYMBOL.fullmatch(symbol):
84+
_fail("invalid constituent symbol")
85+
normalized.append(symbol)
86+
if len(set(normalized)) != len(normalized):
87+
_fail("duplicate constituent symbol")
88+
return tuple(sorted(normalized))
89+
90+
91+
def _source(value: object) -> dict[str, str]:
92+
if not isinstance(value, Mapping) or set(value) != _SOURCE_FIELDS:
93+
_fail("invalid universe source")
94+
license_scope = value["license_scope"]
95+
if not isinstance(license_scope, str) or not license_scope or license_scope != license_scope.strip():
96+
_fail("invalid universe license scope")
97+
return {
98+
"source_id": _identifier(value["source_id"], "universe source id"),
99+
"raw_artifact_sha256": _digest(value["raw_artifact_sha256"], "universe raw artifact digest"),
100+
"license_scope": license_scope,
101+
}
102+
103+
104+
def _canonical_json(value: Mapping[str, Any], *, without_digest: bool) -> bytes:
105+
material = dict(value)
106+
if without_digest:
107+
material.pop("snapshot_sha256", None)
108+
try:
109+
return json.dumps(
110+
material, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False
111+
).encode("utf-8")
112+
except (TypeError, ValueError) as exc:
113+
raise PointInTimeUniverseError("invalid universe snapshot") from exc
114+
115+
116+
def calculate_point_in_time_universe_sha256(value: Mapping[str, Any]) -> str:
117+
"""Return the self-digest for one exact point-in-time universe snapshot."""
118+
return hashlib.sha256(_canonical_json(value, without_digest=True)).hexdigest()
119+
120+
121+
def build_point_in_time_universe_snapshot(
122+
*,
123+
universe_id: object,
124+
effective_date: object,
125+
available_at: object,
126+
source_id: object,
127+
raw_artifact_sha256: object,
128+
license_scope: object,
129+
constituents: object,
130+
) -> dict[str, object]:
131+
"""Build a canonical local snapshot without making any provider request."""
132+
result: dict[str, object] = {
133+
"schema_version": POINT_IN_TIME_UNIVERSE_SCHEMA,
134+
"universe_id": _identifier(universe_id, "universe id"),
135+
"effective_date": _date(effective_date, "effective date"),
136+
"available_at": _timestamp(available_at, "available timestamp").strftime(
137+
"%Y-%m-%dT%H:%M:%SZ"
138+
),
139+
"source": _source(
140+
{
141+
"source_id": source_id,
142+
"raw_artifact_sha256": raw_artifact_sha256,
143+
"license_scope": license_scope,
144+
}
145+
),
146+
"constituents": list(_constituents(constituents)),
147+
"snapshot_sha256": "",
148+
}
149+
result["snapshot_sha256"] = calculate_point_in_time_universe_sha256(result)
150+
return validate_point_in_time_universe_snapshot(result)
151+
152+
153+
def validate_point_in_time_universe_snapshot(value: object) -> dict[str, object]:
154+
"""Validate one exact artifact and its constituent/source binding."""
155+
if not isinstance(value, Mapping) or set(value) != _ROOT_FIELDS:
156+
_fail("invalid universe snapshot")
157+
if value["schema_version"] != POINT_IN_TIME_UNIVERSE_SCHEMA:
158+
_fail("invalid universe snapshot schema")
159+
normalized: dict[str, object] = {
160+
"schema_version": POINT_IN_TIME_UNIVERSE_SCHEMA,
161+
"universe_id": _identifier(value["universe_id"], "universe id"),
162+
"effective_date": _date(value["effective_date"], "effective date"),
163+
"available_at": _timestamp(value["available_at"], "available timestamp").strftime(
164+
"%Y-%m-%dT%H:%M:%SZ"
165+
),
166+
"source": _source(value["source"]),
167+
"constituents": list(_constituents(value["constituents"])),
168+
"snapshot_sha256": _digest(value["snapshot_sha256"], "universe snapshot digest"),
169+
}
170+
if normalized["snapshot_sha256"] != calculate_point_in_time_universe_sha256(normalized):
171+
_fail("universe snapshot digest mismatch")
172+
return normalized
173+
174+
175+
def validate_universe_snapshot_for_decision(
176+
value: object, *, decision_at: object
177+
) -> dict[str, object]:
178+
"""Require that a snapshot was available no later than the decision time."""
179+
snapshot = validate_point_in_time_universe_snapshot(value)
180+
available_at = _timestamp(snapshot["available_at"], "available timestamp")
181+
decision = _timestamp(decision_at, "decision timestamp")
182+
if available_at > decision:
183+
_fail("universe snapshot was unavailable at decision time")
184+
return snapshot
185+
186+
187+
__all__ = [
188+
"POINT_IN_TIME_UNIVERSE_SCHEMA",
189+
"PointInTimeUniverseError",
190+
"build_point_in_time_universe_snapshot",
191+
"calculate_point_in_time_universe_sha256",
192+
"validate_point_in_time_universe_snapshot",
193+
"validate_universe_snapshot_for_decision",
194+
]
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from market_signal_sources.artifacts.point_in_time_universe import (
6+
POINT_IN_TIME_UNIVERSE_SCHEMA,
7+
PointInTimeUniverseError,
8+
build_point_in_time_universe_snapshot,
9+
validate_point_in_time_universe_snapshot,
10+
validate_universe_snapshot_for_decision,
11+
)
12+
13+
14+
def _snapshot() -> dict[str, object]:
15+
return build_point_in_time_universe_snapshot(
16+
universe_id="russell_1000",
17+
effective_date="2025-06-30",
18+
available_at="2025-06-30T20:15:00Z",
19+
source_id="ishares.iwb.holdings_csv",
20+
raw_artifact_sha256="a" * 64,
21+
license_scope="private_research",
22+
constituents=("MSFT", "AAPL", "BRK.B"),
23+
)
24+
25+
26+
def test_snapshot_is_canonical_and_binds_the_source_and_constituents() -> None:
27+
snapshot = _snapshot()
28+
29+
assert snapshot["schema_version"] == POINT_IN_TIME_UNIVERSE_SCHEMA
30+
assert snapshot["constituents"] == ["AAPL", "BRK.B", "MSFT"]
31+
assert snapshot["source"] == {
32+
"source_id": "ishares.iwb.holdings_csv",
33+
"raw_artifact_sha256": "a" * 64,
34+
"license_scope": "private_research",
35+
}
36+
assert validate_point_in_time_universe_snapshot(snapshot) == snapshot
37+
38+
39+
def test_decision_cannot_consume_a_universe_before_it_was_available() -> None:
40+
snapshot = _snapshot()
41+
42+
assert validate_universe_snapshot_for_decision(
43+
snapshot, decision_at="2025-06-30T20:15:00Z"
44+
) == snapshot
45+
with pytest.raises(PointInTimeUniverseError, match="unavailable at decision time"):
46+
validate_universe_snapshot_for_decision(snapshot, decision_at="2025-06-30T20:14:59Z")
47+
48+
49+
@pytest.mark.parametrize(
50+
("mutate", "message"),
51+
[
52+
(lambda value: value.update({"snapshot_sha256": "b" * 64}), "digest mismatch"),
53+
(lambda value: value.update({"constituents": ["AAPL", "AAPL"]}), "duplicate constituent"),
54+
(
55+
lambda value: value["source"].update({"raw_artifact_sha256": "bad"}),
56+
"invalid universe raw artifact digest",
57+
),
58+
],
59+
)
60+
def test_tampered_or_ambiguous_snapshots_fail_closed(mutate, message: str) -> None:
61+
snapshot = _snapshot()
62+
mutate(snapshot)
63+
64+
with pytest.raises(PointInTimeUniverseError, match=message):
65+
validate_point_in_time_universe_snapshot(snapshot)
66+
67+
68+
def test_builder_rejects_duplicate_or_invalid_constituents() -> None:
69+
with pytest.raises(PointInTimeUniverseError, match="duplicate constituent"):
70+
build_point_in_time_universe_snapshot(
71+
universe_id="russell_1000",
72+
effective_date="2025-06-30",
73+
available_at="2025-06-30T20:15:00Z",
74+
source_id="ishares.iwb.holdings_csv",
75+
raw_artifact_sha256="a" * 64,
76+
license_scope="private_research",
77+
constituents=("AAPL", "AAPL"),
78+
)

0 commit comments

Comments
 (0)