diff --git a/scripts/fingerprint_self_test.py b/scripts/fingerprint_self_test.py index 5ea3fb2..e274117 100644 --- a/scripts/fingerprint_self_test.py +++ b/scripts/fingerprint_self_test.py @@ -13,6 +13,7 @@ """ from __future__ import annotations +import ast import base64 import importlib.util import json @@ -146,6 +147,49 @@ def check(name: str, cond: bool) -> None: fp2 = m.build_fingerprint(prov) check(f"{label}: deterministic token", fp["token"] == fp2["token"]) + # --- the effective execution gate must participate in the fingerprint + runtime_kwargs = { + "input_tf": "15", + "script_tf": "", + "bar_magnifier": False, + "magnifier_samples": 4, + "magnifier_distribution": "ENDPOINTS", + "chart_timezone": "UTC", + } + gate_ms = 1712345678901 + open_runtime = rs.build_runtime_provenance(runtime_kwargs, None) + gated_runtime = rs.build_runtime_provenance(runtime_kwargs, gate_ms) + same_gate_runtime = rs.build_runtime_provenance(runtime_kwargs, str(gate_ms)) + check("run_strategy: open execution records trade_start_ms=null", + "trade_start_ms" in open_runtime and open_runtime["trade_start_ms"] is None) + check("run_strategy: gated execution records trade_start_ms as int", + gated_runtime["trade_start_ms"] == gate_ms + and isinstance(gated_runtime["trade_start_ms"], int)) + check("run_strategy: equivalent gate values normalize identically", + same_gate_runtime == gated_runtime) + check("run_strategy: runtime helper is pure", + "trade_start_ms" not in runtime_kwargs) + + open_fp = rs.build_fingerprint({"runtime": open_runtime}) + gated_fp = rs.build_fingerprint({"runtime": gated_runtime}) + same_gate_fp = rs.build_fingerprint({"runtime": same_gate_runtime}) + check("run_strategy: gated/open digests differ", + gated_fp["digest"] != open_fp["digest"]) + check("run_strategy: same effective gate has deterministic digest", + gated_fp["digest"] == same_gate_fp["digest"]) + + source_tree = ast.parse(RUN_STRATEGY.read_text(encoding="utf-8")) + main_fn = next((node for node in source_tree.body + if isinstance(node, ast.FunctionDef) and node.name == "main"), None) + main_uses_helper = main_fn is not None and any( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "build_runtime_provenance" + for node in ast.walk(main_fn) + ) + check("run_strategy: main fingerprint path uses runtime helper", + main_uses_helper) + # --- the two copies must agree byte-for-byte on the fixture ---------- check("copies agree: parse_strategy_params", rj.parse_strategy_params(FIXTURE_CPP) == rs.parse_strategy_params(FIXTURE_CPP)) diff --git a/scripts/run_strategy.py b/scripts/run_strategy.py index 255a25d..9dcf27a 100644 --- a/scripts/run_strategy.py +++ b/scripts/run_strategy.py @@ -292,6 +292,19 @@ def build_fingerprint(provenance: dict) -> dict: # <<< fingerprint helpers +def build_runtime_provenance(run_kwargs: dict, trade_start_ms: int | None) -> dict: + """Return the effective runtime settings that participate in a fingerprint.""" + return { + "input_tf": run_kwargs.get("input_tf") or "", + "script_tf": run_kwargs.get("script_tf") or "", + "bar_magnifier": bool(run_kwargs.get("bar_magnifier")), + "magnifier_samples": int(run_kwargs.get("magnifier_samples") or 4), + "magnifier_distribution": run_kwargs.get("magnifier_distribution") or "ENDPOINTS", + "chart_timezone": run_kwargs.get("chart_timezone") or "", + "trade_start_ms": None if trade_start_ms is None else int(trade_start_ms), + } + + # --- ctypes mirror of ------------------------- # # Field order, types, and widths must match the C struct exactly. The @@ -1325,14 +1338,7 @@ def main() -> int: overrides_applied = { str(k): str(v) for k, v in (run_kwargs.get("strategy_overrides") or {}).items() } - runtime = { - "input_tf": run_kwargs.get("input_tf") or "", - "script_tf": run_kwargs.get("script_tf") or "", - "bar_magnifier": bool(run_kwargs.get("bar_magnifier")), - "magnifier_samples": int(run_kwargs.get("magnifier_samples") or 4), - "magnifier_distribution": run_kwargs.get("magnifier_distribution") or "ENDPOINTS", - "chart_timezone": run_kwargs.get("chart_timezone") or "", - } + runtime = build_runtime_provenance(run_kwargs, trade_start_ms) fp = build_fingerprint(build_provenance( engine_version(strat.lib), cpp_path if cpp_path.exists() else None, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8658cf7..53f770d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -117,6 +117,12 @@ add_test( ${PROJECT_SOURCE_DIR}/scripts/derive_corpus_feeds_self_test.py ) +add_test( + NAME test_fingerprint + COMMAND ${Python3_EXECUTABLE} + ${PROJECT_SOURCE_DIR}/scripts/fingerprint_self_test.py +) + # When PINEFORGE_ENABLE_COVERAGE is ON we also instrument the test # binaries — header-only code (Series, na, color, log, math::pine_random) # is otherwise reported as 0% because it's only inlined into test TUs.