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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ session/trading-day rollup, or event-Bar threshold and exact source-range eviden
verify the complete source snapshot, physical and logical partition hashes, PIT timestamps,
context membership, ordering, L2 snapshot/delta replay, selection hash and a second post-read
snapshot check. Legacy Curated manifests remain readable through `load_curated_snapshot` but fail
with `legacy-curated-not-m8-certified` at the certified factory.
with `legacy-curated-not-m8-certified` at the certified factory. `VerifiedFactorInput`是不可公开
构造、不可`dataclasses.replace`且不可变的opaque结果;模块不暴露认证令牌或可直接调用的
类工厂,只有上述两个完成全链路校验的loader会实例化它。

## M2 data-lake guarantees

Expand Down
2 changes: 1 addition & 1 deletion src/quant_data_kit/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Single authoritative package version used by builds and runtime imports."""

__version__ = "0.8.0"
__version__ = "0.8.1"
215 changes: 116 additions & 99 deletions src/quant_data_kit/research_contracts_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import hashlib
import re
from collections.abc import Mapping
from dataclasses import InitVar, dataclass, field
from typing import Any, Literal
from dataclasses import dataclass
from typing import Any, Literal, NoReturn

import pyarrow as pa
from pyarrow import ipc
Expand All @@ -30,7 +30,6 @@
_CANONICAL_NONNEGATIVE = re.compile(r"^(?:0|[1-9][0-9]*)$")
_CANONICAL_POSITIVE = re.compile(r"^[1-9][0-9]*$")
_INT64_MAX = 2**63 - 1
_VERIFIED_INPUT_FACTORY_TOKEN = object()


def _required_text(value: str, field_name: str) -> str:
Expand Down Expand Up @@ -381,103 +380,30 @@ def from_contract(cls, payload: Mapping[str, Any]) -> CuratedAggregation:
)


@dataclass(frozen=True, eq=False)
class VerifiedFactorInput:
layer: Literal["curated", "normalized"]
source_snapshot_id: str
source_logical_sha256: str
selection_logical_sha256: str
event_schemas: tuple[EventSchemaRef, ...]
table: pa.Table = field(repr=False)
calendar_id: str = ""
session_policy_version: str = ""
market_context_snapshot_id: str = ""
market_context_logical_sha256: str = ""
lineage: tuple[LineageRef, ...] = ()
aggregation: CuratedAggregation | None = None
schema_id: str = VERIFIED_FACTOR_INPUT_SCHEMA_ID
_factory_token: InitVar[object] = None

def __post_init__(self, _factory_token: object) -> None:
if _factory_token is not _VERIFIED_INPUT_FACTORY_TOKEN:
raise ValidationError("VerifiedFactorInput can only be created by a certified factory")
if self.schema_id != VERIFIED_FACTOR_INPUT_SCHEMA_ID:
raise ValidationError("unsupported VerifiedFactorInput schema")
if self.layer not in {"curated", "normalized"}:
raise ValidationError("unsupported verified input layer")
_require_snapshot_id(self.source_snapshot_id, "source_snapshot_id")
_require_hash(self.source_logical_sha256, "source_logical_sha256")
_require_hash(self.selection_logical_sha256, "selection_logical_sha256")
_required_text(self.calendar_id, "calendar_id")
_required_text(self.session_policy_version, "session_policy_version")
_require_snapshot_id(self.market_context_snapshot_id, "market_context_snapshot_id")
_require_hash(self.market_context_logical_sha256, "market_context_logical_sha256")
schemas = tuple(self.event_schemas)
if not schemas or schemas != tuple(sorted(set(schemas))):
raise ValidationError("event_schemas must be non-empty, unique, and sorted")
if not isinstance(self.table, pa.Table) or self.table.num_rows <= 0:
raise ValidationError("verified input table must be a non-empty Arrow table")
if self.selection_logical_sha256 != _arrow_table_logical_sha256(self.table):
raise ValidationError("verified input selection hash does not match its Arrow table")
lineage = tuple(self.lineage)
if not lineage or lineage != tuple(sorted(lineage)):
raise ValidationError("lineage must be non-empty and canonically ordered")
lineage_keys = [(item.role, item.snapshot_id) for item in lineage]
if len(lineage_keys) != len(set(lineage_keys)):
raise ValidationError("lineage roles and snapshots must be unique")
source_lineage = [item for item in lineage if item.role == "market"]
if len(source_lineage) != 1 or (
source_lineage[0].snapshot_id,
source_lineage[0].logical_sha256,
) != (self.source_snapshot_id, self.source_logical_sha256):
raise ValidationError("source snapshot differs from its market lineage")
context_lineage = [item for item in lineage if item.role == "market_context"]
if len(context_lineage) != 1 or (
context_lineage[0].snapshot_id,
context_lineage[0].logical_sha256,
) != (self.market_context_snapshot_id, self.market_context_logical_sha256):
raise ValidationError("market context differs from its lineage")
if self.layer == "curated":
if self.aggregation is None:
raise ValidationError("Curated verified input requires aggregation metadata")
if schemas != (EventSchemaRef(BAR_EVENT_SCHEMA_ID, SCHEMA_VERSION_V2),):
raise ValidationError("Curated verified input requires the frozen Bar schema")
if self.table.schema != get_arrow_schema(BAR_EVENT_SCHEMA_ID):
raise ValidationError("Curated verified input table is not the frozen Bar schema")
context_values = (
self.calendar_id,
self.session_policy_version,
self.market_context_snapshot_id,
self.market_context_logical_sha256,
)
aggregation_values = (
self.aggregation.calendar_id,
self.aggregation.session_policy_version,
self.aggregation.market_context_snapshot_id,
self.aggregation.market_context_logical_sha256,
)
if context_values != aggregation_values:
raise ValidationError("verified input context differs from its aggregation")
elif self.aggregation is not None:
raise ValidationError("Normalized verified input cannot contain aggregation metadata")
elif any(
item.schema_id == BAR_EVENT_SCHEMA_ID or item.schema_version != SCHEMA_VERSION_V2
for item in schemas
):
raise ValidationError("Normalized verified input requires non-Bar v2 event schemas")
if self.layer == "normalized":
if "event_schema_id" not in self.table.column_names:
raise ValidationError("Normalized verified input lacks event_schema_id")
actual_schema_ids = set(self.table.column("event_schema_id").to_pylist())
expected_schema_ids = {item.schema_id for item in schemas}
if actual_schema_ids != expected_schema_ids:
raise ValidationError("Normalized table event schemas differ from its contract")
object.__setattr__(self, "event_schemas", schemas)
object.__setattr__(self, "lineage", lineage)

@classmethod
def _from_certified_factory(cls, **values: Any) -> VerifiedFactorInput:
return cls(_factory_token=_VERIFIED_INPUT_FACTORY_TOKEN, **values)
"""Opaque immutable value created only after a complete certified loader succeeds."""

__slots__ = (
"aggregation",
"calendar_id",
"event_schemas",
"layer",
"lineage",
"market_context_logical_sha256",
"market_context_snapshot_id",
"schema_id",
"selection_logical_sha256",
"session_policy_version",
"source_logical_sha256",
"source_snapshot_id",
"table",
)

def __new__(cls, *_args: Any, **_kwargs: Any) -> NoReturn:
raise ValidationError("VerifiedFactorInput can only be created by a certified loader")

def __setattr__(self, _name: str, _value: Any) -> None:
raise ValidationError("VerifiedFactorInput is immutable")

@property
def arrow_schema_sha256(self) -> str:
Expand All @@ -502,6 +428,97 @@ def to_contract(self) -> dict[str, Any]:
}


def _validate_verified_factor_input_values(values: Mapping[str, Any]) -> None:
expected = set(VerifiedFactorInput.__slots__)
if set(values) != expected:
raise ValidationError("VerifiedFactorInput fields must be closed and complete")
schema_id = values["schema_id"]
layer = values["layer"]
source_snapshot_id = values["source_snapshot_id"]
source_logical_sha256 = values["source_logical_sha256"]
selection_logical_sha256 = values["selection_logical_sha256"]
event_schemas = values["event_schemas"]
table = values["table"]
calendar_id = values["calendar_id"]
session_policy_version = values["session_policy_version"]
market_context_snapshot_id = values["market_context_snapshot_id"]
market_context_logical_sha256 = values["market_context_logical_sha256"]
lineage = values["lineage"]
aggregation = values["aggregation"]

if schema_id != VERIFIED_FACTOR_INPUT_SCHEMA_ID:
raise ValidationError("unsupported VerifiedFactorInput schema")
if layer not in {"curated", "normalized"}:
raise ValidationError("unsupported verified input layer")
_require_snapshot_id(source_snapshot_id, "source_snapshot_id")
_require_hash(source_logical_sha256, "source_logical_sha256")
_require_hash(selection_logical_sha256, "selection_logical_sha256")
_required_text(calendar_id, "calendar_id")
_required_text(session_policy_version, "session_policy_version")
_require_snapshot_id(market_context_snapshot_id, "market_context_snapshot_id")
_require_hash(market_context_logical_sha256, "market_context_logical_sha256")
schemas = tuple(event_schemas)
if not schemas or schemas != tuple(sorted(set(schemas))):
raise ValidationError("event_schemas must be non-empty, unique, and sorted")
if not isinstance(table, pa.Table) or table.num_rows <= 0:
raise ValidationError("verified input table must be a non-empty Arrow table")
if selection_logical_sha256 != _arrow_table_logical_sha256(table):
raise ValidationError("verified input selection hash does not match its Arrow table")
canonical_lineage = tuple(lineage)
if not canonical_lineage or canonical_lineage != tuple(sorted(canonical_lineage)):
raise ValidationError("lineage must be non-empty and canonically ordered")
lineage_keys = [(item.role, item.snapshot_id) for item in canonical_lineage]
if len(lineage_keys) != len(set(lineage_keys)):
raise ValidationError("lineage roles and snapshots must be unique")
source_lineage = [item for item in canonical_lineage if item.role == "market"]
if len(source_lineage) != 1 or (
source_lineage[0].snapshot_id,
source_lineage[0].logical_sha256,
) != (source_snapshot_id, source_logical_sha256):
raise ValidationError("source snapshot differs from its market lineage")
context_lineage = [item for item in canonical_lineage if item.role == "market_context"]
if len(context_lineage) != 1 or (
context_lineage[0].snapshot_id,
context_lineage[0].logical_sha256,
) != (market_context_snapshot_id, market_context_logical_sha256):
raise ValidationError("market context differs from its lineage")
if layer == "curated":
if aggregation is None:
raise ValidationError("Curated verified input requires aggregation metadata")
if schemas != (EventSchemaRef(BAR_EVENT_SCHEMA_ID, SCHEMA_VERSION_V2),):
raise ValidationError("Curated verified input requires the frozen Bar schema")
if table.schema != get_arrow_schema(BAR_EVENT_SCHEMA_ID):
raise ValidationError("Curated verified input table is not the frozen Bar schema")
context_values = (
calendar_id,
session_policy_version,
market_context_snapshot_id,
market_context_logical_sha256,
)
aggregation_values = (
aggregation.calendar_id,
aggregation.session_policy_version,
aggregation.market_context_snapshot_id,
aggregation.market_context_logical_sha256,
)
if context_values != aggregation_values:
raise ValidationError("verified input context differs from its aggregation")
elif aggregation is not None:
raise ValidationError("Normalized verified input cannot contain aggregation metadata")
elif any(
item.schema_id == BAR_EVENT_SCHEMA_ID or item.schema_version != SCHEMA_VERSION_V2
for item in schemas
):
raise ValidationError("Normalized verified input requires non-Bar v2 event schemas")
if layer == "normalized":
if "event_schema_id" not in table.column_names:
raise ValidationError("Normalized verified input lacks event_schema_id")
actual_schema_ids = set(table.column("event_schema_id").to_pylist())
expected_schema_ids = {item.schema_id for item in schemas}
if actual_schema_ids != expected_schema_ids:
raise ValidationError("Normalized table event schemas differ from its contract")


def _arrow_table_logical_sha256(table: pa.Table) -> str:
combined = table.combine_chunks()
sink = pa.BufferOutputStream()
Expand Down
69 changes: 42 additions & 27 deletions src/quant_data_kit/research_inputs_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@
from quant_data_kit.market_clock_v2 import MarketClock
from quant_data_kit.research_contracts_v2 import (
MARKET_CONTEXT_SCHEMA_ID,
VERIFIED_FACTOR_INPUT_SCHEMA_ID,
CuratedAggregation,
EventBarPartitionEvidence,
EventSchemaRef,
LineageRef,
VerifiedFactorInput,
_validate_verified_factor_input_values,
)
from quant_data_kit.schemas_v2 import (
BAR_EVENT_SCHEMA_ID,
Expand Down Expand Up @@ -830,18 +832,18 @@ def load_verified_normalized_events(
if context_before.manifest() != context_after.manifest():
raise ValidationError("market context changed while building verified input")
_assert_file_stamps(partition_stamps)
return VerifiedFactorInput._from_certified_factory(
layer="normalized",
source_snapshot_id=before.snapshot_id,
source_logical_sha256=before.logical_sha256,
selection_logical_sha256=_table_logical_sha256(table),
event_schemas=refs,
table=table,
calendar_id=context_before.calendar_id,
session_policy_version=context_before.session_policy_version,
market_context_snapshot_id=context_before.snapshot_id,
market_context_logical_sha256=context_before.logical_sha256,
lineage=tuple(
values = {
"layer": "normalized",
"source_snapshot_id": before.snapshot_id,
"source_logical_sha256": before.logical_sha256,
"selection_logical_sha256": _table_logical_sha256(table),
"event_schemas": refs,
"table": table,
"calendar_id": context_before.calendar_id,
"session_policy_version": context_before.session_policy_version,
"market_context_snapshot_id": context_before.snapshot_id,
"market_context_logical_sha256": context_before.logical_sha256,
"lineage": tuple(
sorted(
(
LineageRef("market", before.snapshot_id, before.logical_sha256),
Expand All @@ -851,7 +853,14 @@ def load_verified_normalized_events(
)
)
),
)
"aggregation": None,
"schema_id": VERIFIED_FACTOR_INPUT_SCHEMA_ID,
}
_validate_verified_factor_input_values(values)
verified = object.__new__(VerifiedFactorInput)
for name in VerifiedFactorInput.__slots__:
object.__setattr__(verified, name, values[name])
return verified


def _validate_bar_rows(
Expand Down Expand Up @@ -1087,18 +1096,18 @@ def load_verified_curated_bars(
if normalized_before != normalized:
raise ValidationError("Normalized lineage changed while verifying event Bars")
_assert_file_stamps(normalized_stamps)
return VerifiedFactorInput._from_certified_factory(
layer="curated",
source_snapshot_id=before.snapshot_id,
source_logical_sha256=before.logical_sha256,
selection_logical_sha256=_table_logical_sha256(table),
event_schemas=(EventSchemaRef(BAR_EVENT_SCHEMA_ID, SCHEMA_VERSION_V2),),
table=table,
calendar_id=aggregation.calendar_id,
session_policy_version=aggregation.session_policy_version,
market_context_snapshot_id=context_before.snapshot_id,
market_context_logical_sha256=context_before.logical_sha256,
lineage=tuple(
values = {
"layer": "curated",
"source_snapshot_id": before.snapshot_id,
"source_logical_sha256": before.logical_sha256,
"selection_logical_sha256": _table_logical_sha256(table),
"event_schemas": (EventSchemaRef(BAR_EVENT_SCHEMA_ID, SCHEMA_VERSION_V2),),
"table": table,
"calendar_id": aggregation.calendar_id,
"session_policy_version": aggregation.session_policy_version,
"market_context_snapshot_id": context_before.snapshot_id,
"market_context_logical_sha256": context_before.logical_sha256,
"lineage": tuple(
sorted(
(
LineageRef("market", before.snapshot_id, before.logical_sha256),
Expand All @@ -1109,5 +1118,11 @@ def load_verified_curated_bars(
)
)
),
aggregation=aggregation,
)
"aggregation": aggregation,
"schema_id": VERIFIED_FACTOR_INPUT_SCHEMA_ID,
}
_validate_verified_factor_input_values(values)
verified = object.__new__(VerifiedFactorInput)
for name in VerifiedFactorInput.__slots__:
object.__setattr__(verified, name, values[name])
return verified
2 changes: 1 addition & 1 deletion tests/test_m2_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def crypto_context(provider: str) -> qdk.AdapterContext:


def test_public_m2_api_and_version_are_exposed() -> None:
assert qdk.__version__ == version("quant-data-kit") == "0.8.0"
assert qdk.__version__ == version("quant-data-kit") == "0.8.1"
for name in (
"write_raw_bytes",
"write_normalized_events",
Expand Down
Loading
Loading