This commit is contained in:
+227
-34
@@ -32,6 +32,7 @@ from quant_engine.risk import CovarianceSnapshot, labeled_component_risk
|
|||||||
|
|
||||||
RESEARCH_ARTIFACT_SCHEMA_VERSION = "1.1.0"
|
RESEARCH_ARTIFACT_SCHEMA_VERSION = "1.1.0"
|
||||||
BACKTEST_EVIDENCE_SCHEMA_VERSION = "1.0.0"
|
BACKTEST_EVIDENCE_SCHEMA_VERSION = "1.0.0"
|
||||||
|
_MAX_SAFE_INTEGER = (1 << 53) - 1
|
||||||
|
|
||||||
RISK_COLUMNS = [
|
RISK_COLUMNS = [
|
||||||
"run_id",
|
"run_id",
|
||||||
@@ -136,7 +137,7 @@ class ResearchRunArtifact:
|
|||||||
payload = {
|
payload = {
|
||||||
"schema_version": self.schema_version,
|
"schema_version": self.schema_version,
|
||||||
"tables": {
|
"tables": {
|
||||||
name: _frame_records(frame)
|
name: _frame_records(frame, name)
|
||||||
for name, frame in self._internal_table_frames().items()
|
for name, frame in self._internal_table_frames().items()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -275,8 +276,8 @@ class BacktestEvidenceManifest:
|
|||||||
cls,
|
cls,
|
||||||
value: Any,
|
value: Any,
|
||||||
*,
|
*,
|
||||||
backtest_run_ref: BacktestRunRef,
|
|
||||||
artifact: ResearchRunArtifact,
|
artifact: ResearchRunArtifact,
|
||||||
|
backtest_run_ref: BacktestRunRef | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
if type(value) is not dict:
|
if type(value) is not dict:
|
||||||
_manifest_fail(BacktestContractErrorCode.TYPE_ERROR, "$", "must be an object")
|
_manifest_fail(BacktestContractErrorCode.TYPE_ERROR, "$", "must be an object")
|
||||||
@@ -350,12 +351,59 @@ class BacktestEvidenceManifest:
|
|||||||
"$.qualification",
|
"$.qualification",
|
||||||
"unsupported evidence qualification",
|
"unsupported evidence qualification",
|
||||||
) from error
|
) from error
|
||||||
rebuilt = build_backtest_evidence_manifest(
|
raw_run_reference = value["run_reference"]
|
||||||
backtest_run_ref,
|
if type(raw_run_reference) is not dict:
|
||||||
artifact,
|
_manifest_fail(
|
||||||
artifact_available_at=value["artifact_available_at"],
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
qualification=qualification,
|
"$.run_reference",
|
||||||
)
|
"must be an object",
|
||||||
|
)
|
||||||
|
if any(type(key) is not str for key in raw_run_reference):
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
"$.run_reference",
|
||||||
|
"object keys must be strings",
|
||||||
|
)
|
||||||
|
reference_fields = {"kind", "value"}
|
||||||
|
if set(raw_run_reference) != reference_fields:
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.INVALID_VALUE,
|
||||||
|
"$.run_reference",
|
||||||
|
"must contain exactly kind and value",
|
||||||
|
)
|
||||||
|
reference_kind = raw_run_reference["kind"]
|
||||||
|
if reference_kind == "legacy_backtest_run":
|
||||||
|
if qualification is not EvidenceQualification.LEGACY_EXPLORATORY:
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.READINESS_ESCALATION,
|
||||||
|
"$.qualification",
|
||||||
|
"legacy evidence cannot be promoted",
|
||||||
|
)
|
||||||
|
legacy_run = _legacy_run_from_dict(raw_run_reference["value"])
|
||||||
|
rebuilt = build_legacy_backtest_evidence_manifest(
|
||||||
|
legacy_run,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at=value["artifact_available_at"],
|
||||||
|
)
|
||||||
|
elif reference_kind == "backtest_run_ref":
|
||||||
|
if backtest_run_ref is None:
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
"$.backtest_run_ref",
|
||||||
|
"complete BacktestRunRef is required",
|
||||||
|
)
|
||||||
|
rebuilt = build_backtest_evidence_manifest(
|
||||||
|
backtest_run_ref,
|
||||||
|
artifact,
|
||||||
|
artifact_available_at=value["artifact_available_at"],
|
||||||
|
qualification=qualification,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.INVALID_VALUE,
|
||||||
|
"$.run_reference.kind",
|
||||||
|
"unsupported run-reference kind",
|
||||||
|
)
|
||||||
if value != rebuilt.to_dict():
|
if value != rebuilt.to_dict():
|
||||||
_manifest_fail(
|
_manifest_fail(
|
||||||
BacktestContractErrorCode.IDENTITY_MISMATCH,
|
BacktestContractErrorCode.IDENTITY_MISMATCH,
|
||||||
@@ -384,13 +432,39 @@ def _aware_timestamp(value: str | pd.Timestamp, name: str) -> pd.Timestamp:
|
|||||||
return timestamp
|
return timestamp
|
||||||
|
|
||||||
|
|
||||||
def _json_value(value: object) -> object:
|
def _json_value(value: object, path: str = "$") -> object:
|
||||||
if value is None or isinstance(value, str | bool | int):
|
if value is None or type(value) is bool:
|
||||||
|
return value
|
||||||
|
if type(value) is int:
|
||||||
|
if abs(value) > _MAX_SAFE_INTEGER:
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.INVALID_VALUE,
|
||||||
|
path,
|
||||||
|
"integer exceeds the canonical safe range",
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
if type(value) is str:
|
||||||
|
try:
|
||||||
|
value.encode("utf-8")
|
||||||
|
except UnicodeEncodeError as error:
|
||||||
|
raise BacktestContractError(
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
path,
|
||||||
|
"text must be valid UTF-8",
|
||||||
|
) from error
|
||||||
|
return value
|
||||||
|
if type(value) is float:
|
||||||
|
if math.isnan(value):
|
||||||
|
return {"non_finite_float": "nan"}
|
||||||
|
if math.isinf(value):
|
||||||
|
return {
|
||||||
|
"non_finite_float": (
|
||||||
|
"positive_infinity" if value > 0 else "negative_infinity"
|
||||||
|
)
|
||||||
|
}
|
||||||
return value
|
return value
|
||||||
if isinstance(value, float):
|
|
||||||
return value if math.isfinite(value) else None
|
|
||||||
if isinstance(value, np.generic):
|
if isinstance(value, np.generic):
|
||||||
return _json_value(value.item())
|
return _json_value(value.item(), path)
|
||||||
if isinstance(value, pd.Timestamp):
|
if isinstance(value, pd.Timestamp):
|
||||||
return value.isoformat()
|
return value.isoformat()
|
||||||
if isinstance(value, datetime):
|
if isinstance(value, datetime):
|
||||||
@@ -398,13 +472,34 @@ def _json_value(value: object) -> object:
|
|||||||
if isinstance(value, date):
|
if isinstance(value, date):
|
||||||
return value.isoformat()
|
return value.isoformat()
|
||||||
if isinstance(value, Mapping):
|
if isinstance(value, Mapping):
|
||||||
return {
|
normalized: dict[str, object] = {}
|
||||||
str(key): _json_value(item)
|
for key, item in value.items():
|
||||||
for key, item in sorted(value.items(), key=lambda pair: str(pair[0]))
|
if type(key) is not str:
|
||||||
}
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
f"{path}.keys",
|
||||||
|
"object keys must be strings",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
key.encode("utf-8")
|
||||||
|
except UnicodeEncodeError as error:
|
||||||
|
raise BacktestContractError(
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
f"{path}.keys",
|
||||||
|
"object keys must be valid UTF-8",
|
||||||
|
) from error
|
||||||
|
normalized[key] = _json_value(item, f"{path}.{key}")
|
||||||
|
return dict(sorted(normalized.items()))
|
||||||
if isinstance(value, list | tuple):
|
if isinstance(value, list | tuple):
|
||||||
return [_json_value(item) for item in value]
|
return [
|
||||||
raise TypeError(f"value of type {type(value).__name__} is not JSON serializable")
|
_json_value(item, f"{path}[{index}]")
|
||||||
|
for index, item in enumerate(value)
|
||||||
|
]
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
path,
|
||||||
|
f"unsupported canonical value type: {type(value).__name__}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _canonical_mapping_json(values: Mapping[str, object]) -> str:
|
def _canonical_mapping_json(values: Mapping[str, object]) -> str:
|
||||||
@@ -418,24 +513,47 @@ def _canonical_mapping_json(values: Mapping[str, object]) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _frame_records(frame: pd.DataFrame) -> list[dict[str, object]]:
|
def _frame_records(frame: pd.DataFrame, table_name: str) -> list[dict[str, object]]:
|
||||||
|
try:
|
||||||
|
raw_records = frame.to_dict(orient="records")
|
||||||
|
except (TypeError, ValueError) as error:
|
||||||
|
raise BacktestContractError(
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
f"$.tables.{table_name}",
|
||||||
|
"table records cannot be serialized",
|
||||||
|
) from error
|
||||||
return [
|
return [
|
||||||
{str(key): _json_value(value) for key, value in row.items()}
|
{
|
||||||
for row in frame.to_dict(orient="records")
|
str(key): _evidence_json_value(
|
||||||
|
value,
|
||||||
|
f"$.tables.{table_name}.rows[{row_index}].{key}",
|
||||||
|
)
|
||||||
|
for key, value in row.items()
|
||||||
|
}
|
||||||
|
for row_index, row in enumerate(raw_records)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _evidence_json_value(value: object, path: str) -> object:
|
def _evidence_json_value(value: object, path: str) -> object:
|
||||||
if value is None or type(value) in {str, bool, int}:
|
if value is None or type(value) is bool:
|
||||||
if type(value) is str:
|
return value
|
||||||
try:
|
if type(value) is int:
|
||||||
value.encode("utf-8")
|
if abs(value) > _MAX_SAFE_INTEGER:
|
||||||
except UnicodeEncodeError as error:
|
_manifest_fail(
|
||||||
raise BacktestContractError(
|
BacktestContractErrorCode.INVALID_VALUE,
|
||||||
BacktestContractErrorCode.INVALID_FORMAT,
|
path,
|
||||||
path,
|
"integer exceeds the canonical safe range",
|
||||||
"text must be valid UTF-8",
|
)
|
||||||
) from error
|
return value
|
||||||
|
if type(value) is str:
|
||||||
|
try:
|
||||||
|
value.encode("utf-8")
|
||||||
|
except UnicodeEncodeError as error:
|
||||||
|
raise BacktestContractError(
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
path,
|
||||||
|
"text must be valid UTF-8",
|
||||||
|
) from error
|
||||||
return value
|
return value
|
||||||
if type(value) is float:
|
if type(value) is float:
|
||||||
if math.isnan(value):
|
if math.isnan(value):
|
||||||
@@ -453,11 +571,22 @@ def _evidence_json_value(value: object, path: str) -> object:
|
|||||||
if type(key) is not str:
|
if type(key) is not str:
|
||||||
_manifest_fail(
|
_manifest_fail(
|
||||||
BacktestContractErrorCode.TYPE_ERROR,
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
path,
|
f"{path}.keys",
|
||||||
"object cell keys must be strings",
|
"object cell keys must be strings",
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
key.encode("utf-8")
|
||||||
|
except UnicodeEncodeError as error:
|
||||||
|
raise BacktestContractError(
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
f"{path}.keys",
|
||||||
|
"object cell keys must be valid UTF-8",
|
||||||
|
) from error
|
||||||
normalized[key] = _evidence_json_value(item, f"{path}.{key}")
|
normalized[key] = _evidence_json_value(item, f"{path}.{key}")
|
||||||
return dict(sorted(normalized.items()))
|
return {
|
||||||
|
"canonical_value_type": "object",
|
||||||
|
"value": dict(sorted(normalized.items())),
|
||||||
|
}
|
||||||
if isinstance(value, list | tuple):
|
if isinstance(value, list | tuple):
|
||||||
return [
|
return [
|
||||||
_evidence_json_value(item, f"{path}[{index}]")
|
_evidence_json_value(item, f"{path}[{index}]")
|
||||||
@@ -476,6 +605,7 @@ def _evidence_cell_kind(value: object, path: str) -> str:
|
|||||||
if type(value) is bool:
|
if type(value) is bool:
|
||||||
return "boolean"
|
return "boolean"
|
||||||
if type(value) is int:
|
if type(value) is int:
|
||||||
|
_evidence_json_value(value, path)
|
||||||
return "integer"
|
return "integer"
|
||||||
if type(value) is float:
|
if type(value) is float:
|
||||||
return "number"
|
return "number"
|
||||||
@@ -639,6 +769,69 @@ def _legacy_run_dict(run: BacktestRun) -> dict[str, object]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_run_from_dict(value: object) -> BacktestRun:
|
||||||
|
path = "$.run_reference.value"
|
||||||
|
if type(value) is not dict:
|
||||||
|
_manifest_fail(BacktestContractErrorCode.TYPE_ERROR, path, "must be an object")
|
||||||
|
if any(type(key) is not str for key in value):
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
path,
|
||||||
|
"object keys must be strings",
|
||||||
|
)
|
||||||
|
fields = {
|
||||||
|
"run_id",
|
||||||
|
"dataset_snapshot_id",
|
||||||
|
"factor_version_id",
|
||||||
|
"strategy_version_id",
|
||||||
|
"code_revision",
|
||||||
|
"config_hash",
|
||||||
|
"created_at",
|
||||||
|
}
|
||||||
|
if set(value) != fields:
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.INVALID_VALUE,
|
||||||
|
path,
|
||||||
|
"legacy run reference has incomplete or unknown fields",
|
||||||
|
)
|
||||||
|
|
||||||
|
def required_text(field: str) -> str:
|
||||||
|
item = value[field]
|
||||||
|
item_path = f"{path}.{field}"
|
||||||
|
if type(item) is not str:
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
item_path,
|
||||||
|
"must be a string",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
item.encode("utf-8")
|
||||||
|
except UnicodeEncodeError as error:
|
||||||
|
raise BacktestContractError(
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
item_path,
|
||||||
|
"must be valid UTF-8 text",
|
||||||
|
) from error
|
||||||
|
if not item or item != item.strip():
|
||||||
|
_manifest_fail(
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
item_path,
|
||||||
|
"must be non-empty canonical text",
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
|
_, created_at = _manifest_instant(value["created_at"], f"{path}.created_at")
|
||||||
|
return BacktestRun(
|
||||||
|
run_id=required_text("run_id"),
|
||||||
|
dataset_snapshot_id=required_text("dataset_snapshot_id"),
|
||||||
|
factor_version_id=required_text("factor_version_id"),
|
||||||
|
strategy_version_id=required_text("strategy_version_id"),
|
||||||
|
code_revision=required_text("code_revision"),
|
||||||
|
config_hash=required_text("config_hash"),
|
||||||
|
created_at=created_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _artifact_frames(artifact: ResearchRunArtifact) -> dict[str, pd.DataFrame]:
|
def _artifact_frames(artifact: ResearchRunArtifact) -> dict[str, pd.DataFrame]:
|
||||||
if not isinstance(artifact, ResearchRunArtifact):
|
if not isinstance(artifact, ResearchRunArtifact):
|
||||||
_manifest_fail(
|
_manifest_fail(
|
||||||
|
|||||||
@@ -40,13 +40,17 @@ _GIT_SHA = re.compile(r"^[0-9a-f]{40}$")
|
|||||||
_LOGICAL_ID = re.compile(r"^[a-z0-9][a-z0-9._-]{0,127}$")
|
_LOGICAL_ID = re.compile(r"^[a-z0-9][a-z0-9._-]{0,127}$")
|
||||||
_SEMVER = re.compile(
|
_SEMVER = re.compile(
|
||||||
r"^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\."
|
r"^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\."
|
||||||
r"(?:0|[1-9][0-9]*)(?:-[0-9a-z.-]+)?$"
|
r"(?:0|[1-9][0-9]*)"
|
||||||
|
r"(?:-(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)"
|
||||||
|
r"(?:\.(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*)?"
|
||||||
|
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
|
||||||
)
|
)
|
||||||
_MAX_SAFE_INTEGER = (1 << 53) - 1
|
_MAX_SAFE_INTEGER = (1 << 53) - 1
|
||||||
_FORBIDDEN_ID_TOKENS = frozenset(
|
_FORBIDDEN_ID_TOKENS = frozenset(
|
||||||
{
|
{
|
||||||
"latest",
|
"latest",
|
||||||
"provider",
|
"provider",
|
||||||
|
"source",
|
||||||
"broker",
|
"broker",
|
||||||
"credential",
|
"credential",
|
||||||
"locator",
|
"locator",
|
||||||
@@ -57,6 +61,19 @@ _FORBIDDEN_ID_TOKENS = frozenset(
|
|||||||
"postgres",
|
"postgres",
|
||||||
"mysql",
|
"mysql",
|
||||||
"clickhouse",
|
"clickhouse",
|
||||||
|
"table",
|
||||||
|
"sql",
|
||||||
|
"tushare",
|
||||||
|
"wind",
|
||||||
|
"bloomberg",
|
||||||
|
"qtdb",
|
||||||
|
"edb",
|
||||||
|
"db",
|
||||||
|
"database",
|
||||||
|
"schema",
|
||||||
|
"bucket",
|
||||||
|
"path",
|
||||||
|
"endpoint",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -528,6 +545,16 @@ class BacktestRunRef:
|
|||||||
factor_set.artifact_available_at,
|
factor_set.artifact_available_at,
|
||||||
"$.factor_set.artifact_available_at",
|
"$.factor_set.artifact_available_at",
|
||||||
)
|
)
|
||||||
|
_, factor_evaluation = _backtest_instant(
|
||||||
|
factor_set.evaluation_at,
|
||||||
|
"$.factor_set.evaluation_at",
|
||||||
|
)
|
||||||
|
if evaluation_time < factor_evaluation or evaluation_time < factor_available:
|
||||||
|
_backtest_fail(
|
||||||
|
BacktestContractErrorCode.TIME_ORDER_VIOLATION,
|
||||||
|
"$.evaluation_at",
|
||||||
|
"run evaluation must not precede FactorSet evaluation or availability",
|
||||||
|
)
|
||||||
if computed_time < evaluation_time or computed_time < factor_available:
|
if computed_time < evaluation_time or computed_time < factor_available:
|
||||||
_backtest_fail(
|
_backtest_fail(
|
||||||
BacktestContractErrorCode.TIME_ORDER_VIOLATION,
|
BacktestContractErrorCode.TIME_ORDER_VIOLATION,
|
||||||
|
|||||||
@@ -65,7 +65,13 @@ def _sha256(value: bytes) -> str:
|
|||||||
return f"sha256:{hashlib.sha256(value).hexdigest()}"
|
return f"sha256:{hashlib.sha256(value).hexdigest()}"
|
||||||
|
|
||||||
|
|
||||||
def _accepted_authorities() -> tuple[
|
def _accepted_authorities(
|
||||||
|
*,
|
||||||
|
factor_evaluation_at: str = "2026-01-03T11:00:00Z",
|
||||||
|
factor_computed_at: str = "2026-01-03T10:15:00Z",
|
||||||
|
factor_artifact_available_at: str = "2026-01-03T10:20:00Z",
|
||||||
|
factor_availability_mode: AvailabilityMode = AvailabilityMode.AS_AVAILABLE,
|
||||||
|
) -> tuple[
|
||||||
DatasetSnapshotEnvelope,
|
DatasetSnapshotEnvelope,
|
||||||
DataFoundationEnvelope,
|
DataFoundationEnvelope,
|
||||||
FactorSetRef,
|
FactorSetRef,
|
||||||
@@ -115,10 +121,10 @@ def _accepted_authorities() -> tuple[
|
|||||||
output_schema_bytes=output_schema_bytes,
|
output_schema_bytes=output_schema_bytes,
|
||||||
output_content_bytes=output_content_bytes,
|
output_content_bytes=output_content_bytes,
|
||||||
output_artifact_ref=artifact_ref,
|
output_artifact_ref=artifact_ref,
|
||||||
availability_mode=AvailabilityMode.AS_AVAILABLE,
|
availability_mode=factor_availability_mode,
|
||||||
evaluation_at="2026-01-03T11:00:00Z",
|
evaluation_at=factor_evaluation_at,
|
||||||
computed_at="2026-01-03T10:15:00Z",
|
computed_at=factor_computed_at,
|
||||||
artifact_available_at="2026-01-03T10:20:00Z",
|
artifact_available_at=factor_artifact_available_at,
|
||||||
producer=ProducerIdentity("quant_engine", "1.0.0"),
|
producer=ProducerIdentity("quant_engine", "1.0.0"),
|
||||||
code_revision="c" * 40,
|
code_revision="c" * 40,
|
||||||
actor=ActorIdentity("service", "factor_worker_v1"),
|
actor=ActorIdentity("service", "factor_worker_v1"),
|
||||||
@@ -311,6 +317,48 @@ def test_backtest_run_ref_rejects_unclosed_upstream_and_unsafe_scalars() -> None
|
|||||||
_assert_error(factor_type, BacktestContractErrorCode.TYPE_ERROR, "$.factor_set")
|
_assert_error(factor_type, BacktestContractErrorCode.TYPE_ERROR, "$.factor_set")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("factor_times", "expected_path"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"factor_evaluation_at": "2026-01-08T01:00:01Z",
|
||||||
|
"factor_computed_at": "2026-01-08T00:59:59Z",
|
||||||
|
"factor_artifact_available_at": "2026-01-08T01:00:00Z",
|
||||||
|
},
|
||||||
|
"$.evaluation_at",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"factor_evaluation_at": "2026-01-03T11:00:00Z",
|
||||||
|
"factor_computed_at": "2026-01-08T01:00:00Z",
|
||||||
|
"factor_artifact_available_at": "2026-01-08T01:00:01Z",
|
||||||
|
"factor_availability_mode": AvailabilityMode.RETROSPECTIVE_REPLAY,
|
||||||
|
},
|
||||||
|
"$.evaluation_at",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_run_ref_evaluation_closes_factor_pit(
|
||||||
|
factor_times: dict[str, Any],
|
||||||
|
expected_path: str,
|
||||||
|
) -> None:
|
||||||
|
snapshot, foundation, factor_set = _accepted_authorities(**factor_times)
|
||||||
|
|
||||||
|
with pytest.raises(BacktestContractError) as lookahead:
|
||||||
|
_run_ref(
|
||||||
|
dataset_snapshot=snapshot,
|
||||||
|
foundation=foundation,
|
||||||
|
factor_set=factor_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_assert_error(
|
||||||
|
lookahead,
|
||||||
|
BacktestContractErrorCode.TIME_ORDER_VIOLATION,
|
||||||
|
expected_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_run_ref_rejects_aliases_locators_unsafe_integers_and_invalid_text() -> None:
|
def test_run_ref_rejects_aliases_locators_unsafe_integers_and_invalid_text() -> None:
|
||||||
with pytest.raises(BacktestContractError) as mutable_alias:
|
with pytest.raises(BacktestContractError) as mutable_alias:
|
||||||
_run_ref(strategy_id="latest")
|
_run_ref(strategy_id="latest")
|
||||||
@@ -355,6 +403,33 @@ def test_run_ref_rejects_aliases_locators_unsafe_integers_and_invalid_text() ->
|
|||||||
_assert_error(invalid_key, BacktestContractErrorCode.TYPE_ERROR, "$")
|
_assert_error(invalid_key, BacktestContractErrorCode.TYPE_ERROR, "$")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"physical_id",
|
||||||
|
["db.table", "source_alpha", "wind.model", "qtdb_view", "bloomberg-signal"],
|
||||||
|
)
|
||||||
|
def test_run_ref_rejects_physical_terms_in_logical_ids(physical_id: str) -> None:
|
||||||
|
with pytest.raises(BacktestContractError) as physical:
|
||||||
|
_run_ref(strategy_id=physical_id)
|
||||||
|
_assert_error(
|
||||||
|
physical,
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
"$.strategy_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("version", ["1.0.0-.", "1.0.0-foo..bar", "1.0.0-01"])
|
||||||
|
def test_run_ref_requires_strict_semver_prerelease_identifiers(version: str) -> None:
|
||||||
|
with pytest.raises(BacktestContractError) as invalid:
|
||||||
|
_run_ref(strategy_version=version)
|
||||||
|
_assert_error(
|
||||||
|
invalid,
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
"$.strategy_version",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert _run_ref(strategy_version="1.0.0-alpha.1").strategy_version == "1.0.0-alpha.1"
|
||||||
|
|
||||||
|
|
||||||
def test_replay_lineage_is_acyclic_and_cannot_claim_changed_inputs() -> None:
|
def test_replay_lineage_is_acyclic_and_cannot_claim_changed_inputs() -> None:
|
||||||
parent = _run_ref()
|
parent = _run_ref()
|
||||||
replay = _run_ref(
|
replay = _run_ref(
|
||||||
@@ -536,6 +611,21 @@ def test_manifest_binds_supported_schema_and_has_collision_free_cell_encoding()
|
|||||||
identities.add(manifest.manifest_id)
|
identities.add(manifest.manifest_id)
|
||||||
assert len(identities) == 3
|
assert len(identities) == 3
|
||||||
|
|
||||||
|
content_digests: set[str] = set()
|
||||||
|
for value in (float("nan"), {"non_finite_float": "nan"}):
|
||||||
|
performance = artifact.performance.astype(object)
|
||||||
|
performance.at[0, "alpha"] = value
|
||||||
|
manifest = build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
replace(artifact, _performance=performance),
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
performance_entry = next(
|
||||||
|
entry for entry in manifest.evidence if entry.category == "performance"
|
||||||
|
)
|
||||||
|
content_digests.add(performance_entry.tables[0].content_digest)
|
||||||
|
assert len(content_digests) == 2
|
||||||
|
|
||||||
unsupported = artifact.performance.astype(object)
|
unsupported = artifact.performance.astype(object)
|
||||||
unsupported.loc[0, "alpha"] = object()
|
unsupported.loc[0, "alpha"] = object()
|
||||||
with pytest.raises(BacktestContractError) as unsupported_cell:
|
with pytest.raises(BacktestContractError) as unsupported_cell:
|
||||||
@@ -550,6 +640,63 @@ def test_manifest_binds_supported_schema_and_has_collision_free_cell_encoding()
|
|||||||
"$.artifact.tables.performance.rows[0].alpha",
|
"$.artifact.tables.performance.rows[0].alpha",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
invalid_nested_key = artifact.performance.astype(object)
|
||||||
|
invalid_nested_key.at[0, "alpha"] = {"\ud800": "value"}
|
||||||
|
with pytest.raises(BacktestContractError) as invalid_utf8:
|
||||||
|
build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
replace(artifact, _performance=invalid_nested_key),
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
invalid_utf8,
|
||||||
|
BacktestContractErrorCode.INVALID_FORMAT,
|
||||||
|
"$.artifact.tables.performance.rows[0].alpha.keys",
|
||||||
|
)
|
||||||
|
|
||||||
|
unsafe_integer = artifact.performance.astype(object)
|
||||||
|
unsafe_integer.loc[0, "alpha"] = 10**5000
|
||||||
|
with pytest.raises(BacktestContractError) as unsafe_cell:
|
||||||
|
build_backtest_evidence_manifest(
|
||||||
|
run_ref,
|
||||||
|
replace(artifact, _performance=unsafe_integer),
|
||||||
|
artifact_available_at="2026-01-08T02:05:00Z",
|
||||||
|
)
|
||||||
|
_assert_error(
|
||||||
|
unsafe_cell,
|
||||||
|
BacktestContractErrorCode.INVALID_VALUE,
|
||||||
|
"$.artifact.tables.performance.rows[0].alpha",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_artifact_canonical_content_has_typed_collision_free_cell_encoding() -> None:
|
||||||
|
run_ref = _run_ref()
|
||||||
|
artifact = _artifact(run_ref)
|
||||||
|
|
||||||
|
content_hashes: set[str] = set()
|
||||||
|
for value in (
|
||||||
|
float("nan"),
|
||||||
|
float("inf"),
|
||||||
|
float("-inf"),
|
||||||
|
{"non_finite_float": "nan"},
|
||||||
|
):
|
||||||
|
performance = artifact.performance.astype(object)
|
||||||
|
performance.at[0, "alpha"] = value
|
||||||
|
mutated = replace(artifact, _performance=performance)
|
||||||
|
content_hashes.add(mutated.content_sha256)
|
||||||
|
assert "non_finite_float" in mutated.canonical_json()
|
||||||
|
assert len(content_hashes) == 4
|
||||||
|
|
||||||
|
unsupported = artifact.performance.astype(object)
|
||||||
|
unsupported.loc[0, "alpha"] = object()
|
||||||
|
with pytest.raises(BacktestContractError) as unsupported_cell:
|
||||||
|
replace(artifact, _performance=unsupported).canonical_json()
|
||||||
|
_assert_error(
|
||||||
|
unsupported_cell,
|
||||||
|
BacktestContractErrorCode.TYPE_ERROR,
|
||||||
|
"$.tables.performance.rows[0].alpha",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_bridge_is_explicit_and_cannot_be_contract_qualified() -> None:
|
def test_legacy_bridge_is_explicit_and_cannot_be_contract_qualified() -> None:
|
||||||
run_ref = _run_ref()
|
run_ref = _run_ref()
|
||||||
@@ -573,6 +720,10 @@ def test_legacy_bridge_is_explicit_and_cannot_be_contract_qualified() -> None:
|
|||||||
assert manifest.run_id == legacy_run.run_id
|
assert manifest.run_id == legacy_run.run_id
|
||||||
assert manifest.backtest_run_ref is None
|
assert manifest.backtest_run_ref is None
|
||||||
assert manifest.to_dict()["run_reference"]["kind"] == "legacy_backtest_run"
|
assert manifest.to_dict()["run_reference"]["kind"] == "legacy_backtest_run"
|
||||||
|
assert BacktestEvidenceManifest.from_dict(
|
||||||
|
manifest.to_dict(),
|
||||||
|
artifact=artifact,
|
||||||
|
) == manifest
|
||||||
with pytest.raises(BacktestContractError) as implicit_promotion:
|
with pytest.raises(BacktestContractError) as implicit_promotion:
|
||||||
build_backtest_evidence_manifest( # type: ignore[arg-type]
|
build_backtest_evidence_manifest( # type: ignore[arg-type]
|
||||||
legacy_run,
|
legacy_run,
|
||||||
|
|||||||
Reference in New Issue
Block a user