Artifex/gpu_feature_parity_contract_v1_1.py

245 lines
17 KiB
Python
Raw Normal View History

"""GPU_FEATURE_PARITY_CONTRACT_V1_1 calibration and frozen-contract validation.
Calibration is evidence production only. Validation never measures or derives
limits: it accepts a supplied, immutable contract and enforces its limits.
"""
from __future__ import annotations
import csv
import hashlib
import json
from pathlib import Path
from typing import Any, Mapping
import numpy as np
from control_plane.trading_studio.indicators.historical_band_channel import evaluate_band_channel
ARTIFACT = "GPU_FEATURE_PARITY_CONTRACT_V1_1"
CALIBRATION_ARTIFACT = "GPU_FEATURE_PARITY_CALIBRATION_V1_1"
VALIDATION_ARTIFACT = "GPU_FEATURE_PARITY_VALIDATION_V1_1"
CORPUS_ARTIFACT = "GPU_FEATURE_PARITY_CORPUS_MANIFEST_V1_1"
NAN_GAP_SEMANTICS_ARTIFACT = "GPU_FEATURE_PARITY_NAN_GAP_SEMANTICS_V1_1"
CPU_ORACLE_ARTIFACT = "GPU_FEATURE_PARITY_CPU_ORACLE_V1_1"
FAMILIES = {17: "bollinger", 18: "bollinger", 19: "supertrend", 20: "donchian", 21: "donchian", 23: "keltner", 24: "keltner", 28: "psar"}
SCENARIOS = (
"warmup", "constant", "nearly_constant_tiny_variance", "high_variance", "alternating", "uptrend", "downtrend",
"flat_breakout", "breakout_flat", "threshold_equality", "threshold_one_ulp_above", "threshold_one_ulp_below",
"repeated_equality", "zero_range", "tiny_range", "abrupt_atr",
)
TRACE_KEYS = {
"supertrend": frozenset(("output", "atr", "upper", "lower", "direction", "initialized")),
"psar": frozenset(("output", "bullish", "extreme", "acceleration", "reversal")),
}
class FrozenContractError(ValueError):
pass
def canonical_bytes(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def array_sha256(values: np.ndarray) -> str:
values = np.ascontiguousarray(values)
return sha256_bytes(canonical_bytes({"dtype": values.dtype.str, "shape": values.shape}) + b"\0" + values.tobytes())
def write_artifact(path: Path, payload: Mapping[str, Any]) -> None:
path.write_bytes(canonical_bytes(dict(payload)) + b"\n")
def deterministic_adversarial_ohlcv(*, length: int = 256, seed: int = 0) -> dict[str, np.ndarray]:
"""Generate reproducible valid OHLCV covering all declared edge categories."""
if length < len(SCENARIOS) * 4:
raise ValueError("length must allow every adversarial scenario")
rng = np.random.default_rng(seed)
block = max(4, length // len(SCENARIOS))
close = np.empty(length, dtype=np.float64)
spread = 0.15 + rng.random(length) * 0.2
level = 100.0
for number, scenario in enumerate(SCENARIOS):
start, stop = number * block, length if number == len(SCENARIOS) - 1 else min(length, (number + 1) * block)
count = stop - start
if scenario == "warmup": values = level + np.arange(count) * 0.03
elif scenario in {"constant", "repeated_equality"}: values = np.full(count, level)
elif scenario == "nearly_constant_tiny_variance": values = level + np.resize(np.array([0.0, np.spacing(level)]), count)
elif scenario == "high_variance": values = level + np.cumsum(rng.normal(0.0, 3.0, count))
elif scenario == "alternating": values = level + np.resize(np.array([0.75, -0.75]), count)
elif scenario == "uptrend": values = level + np.arange(1, count + 1) * 0.18
elif scenario == "downtrend": values = level - np.arange(1, count + 1) * 0.18
elif scenario == "flat_breakout": values = np.full(count, level); values[count // 2 :] += 4.0
elif scenario == "breakout_flat": values = np.full(count, level + 4.0)
elif scenario == "threshold_equality": values = np.full(count, level)
elif scenario == "threshold_one_ulp_above": values = np.full(count, np.nextafter(level, np.inf))
elif scenario == "threshold_one_ulp_below": values = np.full(count, np.nextafter(level, -np.inf))
elif scenario == "zero_range": values = np.full(count, level); spread[start:stop] = 0.0
elif scenario == "tiny_range": values = np.full(count, level); spread[start:stop] = np.spacing(level)
elif scenario == "abrupt_atr": values = level + np.cumsum(np.resize(np.array([0.01, -0.01]), count)); spread[start:stop] = np.resize(np.array([0.01, 5.0]), count)
close[start:stop] = values
level = values[-1]
high, low = close + spread, close - spread
volume = 1_000.0 + rng.integers(0, 10_000, length).astype(np.float64)
return {"close": close.astype(np.float64), "high": high.astype(np.float64), "low": low.astype(np.float64), "volume": volume}
def corpus_manifest(corpora: Mapping[str, Mapping[str, np.ndarray]], provenance: Mapping[str, Any]) -> dict[str, Any]:
entries = []
for name, ohlcv in sorted(corpora.items()):
if set(ohlcv) != {"close", "high", "low", "volume"}:
raise ValueError("corpus must contain exactly close, high, low, volume")
entries.append({"name": name, "columns": {key: array_sha256(value) for key, value in sorted(ohlcv.items())}, "row_count": int(len(ohlcv["close"]))})
return {"artifact": CORPUS_ARTIFACT, "schema_version": "1.1", "scenario_categories": list(SCENARIOS), "corpora": entries, "provenance": dict(provenance)}
def nan_gap_semantics_manifest() -> dict[str, Any]:
"""Record the deliberate non-finite-input policy separately from parity data."""
return {"artifact": NAN_GAP_SEMANTICS_ARTIFACT, "schema_version": "1.1", "historical_input_policy": "reject_non_finite_ohlcv", "internal_nan_gap_semantics": "undefined", "validation": "input_rejection_before_historical_evaluation"}
def _read_csv(path: Path) -> dict[str, np.ndarray]:
with path.open(newline="", encoding="utf-8") as handle:
rows = list(csv.DictReader(handle))
if not rows or not {"close", "high", "low", "volume"}.issubset(rows[0]):
raise ValueError("CSV must contain non-empty close, high, low, volume columns")
ohlcv = {key: np.asarray([float(row[key]) for row in rows], dtype=np.float64) for key in ("close", "high", "low", "volume")}
if not all(np.isfinite(values).all() for values in ohlcv.values()):
raise ValueError("historical NaN/gap semantics are undefined; non-finite OHLCV inputs are rejected")
return ohlcv
def historical_cpu_oracle(csv_path: Path, requests: list[Mapping[str, Any]]) -> dict[str, Any]:
"""Evaluate arbitrary CSV inputs through the historical Batch01 source port."""
ohlcv = _read_csv(csv_path)
outputs: dict[str, np.ndarray] = {}
for request in requests:
identifier = str(request["request_id"])
indicator = int(request["indicator_id"])
if indicator not in FAMILIES or identifier in outputs:
raise ValueError("requests must have unique supported request IDs")
outputs[identifier] = evaluate_band_channel(indicator, **ohlcv, period=int(request["period"]), p1=float(request["p1"]))
return {"artifact": CPU_ORACLE_ARTIFACT, "schema_version": "1.1", "input_csv_sha256": sha256_bytes(csv_path.read_bytes()), "source": {"module": "control_plane.trading_studio.indicators.historical_band_channel", "evaluator": "evaluate_band_channel"}, "outputs": outputs, "output_sha256": {name: array_sha256(value) for name, value in outputs.items()}}
def _continuity_decisions(close: np.ndarray, values: np.ndarray) -> dict[str, np.ndarray]:
valid = np.isfinite(close) & np.isfinite(values)
state = np.zeros(values.shape, dtype=np.int8)
state[valid] = np.sign(close[valid] - values[valid]).astype(np.int8)
adjacent = valid[1:] & valid[:-1]
transition = np.zeros(values.shape, dtype=np.bool_)
transition[1:] = adjacent & (state[1:] != state[:-1])
above = np.zeros(values.shape, dtype=np.bool_)
below = np.zeros(values.shape, dtype=np.bool_)
above[1:] = adjacent & (state[:-1] <= 0) & (state[1:] > 0)
below[1:] = adjacent & (state[:-1] >= 0) & (state[1:] < 0)
return {"valid": valid, "state": state, "transitions": transition, "crossings_above": above, "crossings_below": below}
def cpu_supertrend_trace(close: np.ndarray, high: np.ndarray, low: np.ndarray, period: int, multiplier: float) -> dict[str, np.ndarray]:
from control_plane.trading_studio.indicators.historical_band_channel import _rma, _true_range
n = len(close); atr = _rma(_true_range(high, low, close), period)
output = np.full(n, np.nan); upper = output.copy(); lower = output.copy(); direction = np.zeros(n, np.int8); initialized = np.zeros(n, bool)
for i in range(period, n):
if np.isnan(atr[i]): continue
basic_upper = (high[i] + low[i]) / 2 + multiplier * atr[i]; basic_lower = (high[i] + low[i]) / 2 - multiplier * atr[i]
if i == period:
upper[i], lower[i] = basic_upper, basic_lower; output[i] = lower[i] if close[i] > lower[i] else upper[i]
else:
upper[i] = min(basic_upper, upper[i - 1]) if close[i - 1] <= upper[i - 1] else basic_upper
lower[i] = max(basic_lower, lower[i - 1]) if close[i - 1] >= lower[i - 1] else basic_lower
output[i] = lower[i] if direction[i - 1] == 1 and close[i] >= lower[i] else upper[i] if direction[i - 1] == 1 else upper[i] if close[i] <= upper[i] else lower[i]
direction[i] = 1 if close[i] > output[i] else -1; initialized[i] = True
return {"output": output, "atr": atr, "upper": upper, "lower": lower, "direction": direction, "initialized": initialized}
def cpu_psar_trace(close: np.ndarray, high: np.ndarray, low: np.ndarray, step: float) -> dict[str, np.ndarray]:
n = len(close); output = np.full(n, np.nan); bullish = np.zeros(n, bool); extreme = np.full(n, np.nan); acceleration = np.full(n, np.nan); reversal = np.zeros(n, bool)
if n < 2: return {"output": output, "bullish": bullish, "extreme": extreme, "acceleration": acceleration, "reversal": reversal}
bull, af, ep, sar = True, .02, high[0], low[0]
for i in range(1, n):
sar += af * (ep - sar); sar = min(sar, low[i - 1], low[i - 2] if i >= 2 else low[i - 1]) if bull else max(sar, high[i - 1], high[i - 2] if i >= 2 else high[i - 1])
reverse = (bull and low[i] < sar) or (not bull and high[i] > sar)
if reverse: bull, sar, ep, af = (False, ep, low[i], .02) if bull else (True, ep, high[i], .02)
elif (bull and high[i] > ep) or (not bull and low[i] < ep): ep = high[i] if bull else low[i]; af = min(af + step, .2)
output[i], bullish[i], extreme[i], acceleration[i], reversal[i] = sar, bull, ep, af, reverse
return {"output": output, "bullish": bullish, "extreme": extreme, "acceleration": acceleration, "reversal": reversal}
def compare_stateful_trace(expected: Mapping[str, np.ndarray], actual: Mapping[str, np.ndarray], *, trace_type: str | None = None) -> dict[str, Any]:
if trace_type is not None and trace_type not in TRACE_KEYS:
raise ValueError("unknown stateful trace type")
keys = sorted(set(expected) | set(actual)); failures = []
required = TRACE_KEYS.get(trace_type, frozenset(keys))
if set(expected) != required or set(actual) != required:
failures.extend(sorted(required - set(expected) | required - set(actual)))
for key in keys:
if key not in expected or key not in actual or not np.array_equal(expected[key], actual[key], equal_nan=True): failures.append(key)
return {"exact": not failures, "required_keys": sorted(required), "mismatched_keys": sorted(set(failures))}
def _ulp_distances(actual: np.ndarray, expected: np.ndarray) -> np.ndarray:
"""Return finite IEEE-754 float64 distances, treating signed zero as equal."""
actual64, expected64 = np.asarray(actual, dtype=np.float64), np.asarray(expected, dtype=np.float64)
actual_bits, expected_bits = actual64.view(np.uint64), expected64.view(np.uint64)
actual_ordered = np.where(actual_bits >> 63 != 0, ~actual_bits, actual_bits | np.uint64(1 << 63))
expected_ordered = np.where(expected_bits >> 63 != 0, ~expected_bits, expected_bits | np.uint64(1 << 63))
distances = np.asarray(np.abs(actual_ordered.astype(object) - expected_ordered.astype(object)), dtype=np.float64)
distances[actual64 == expected64] = 0.0
return distances
def _numeric_metrics(actual: np.ndarray, expected: np.ndarray) -> dict[str, float]:
finite = np.isfinite(actual) & np.isfinite(expected)
delta = np.abs(actual[finite] - expected[finite])
finite_expected = expected[finite]
relative = delta[np.abs(finite_expected) != 0] / np.abs(finite_expected[np.abs(finite_expected) != 0])
ulps = _ulp_distances(actual[finite], expected[finite])
return {"max_absolute_error": float(delta.max()) if len(delta) else 0.0, "max_relative_error": float(relative.max()) if len(relative) else 0.0, "mae": float(delta.mean()) if len(delta) else 0.0, "max_ulp": float(ulps.max()) if len(ulps) else 0.0, "p50_ulp": float(np.percentile(ulps, 50)) if len(ulps) else 0.0, "p95_ulp": float(np.percentile(ulps, 95)) if len(ulps) else 0.0, "p99_ulp": float(np.percentile(ulps, 99)) if len(ulps) else 0.0, "p99_9_ulp": float(np.percentile(ulps, 99.9)) if len(ulps) else 0.0}
def calibrate(outputs: Mapping[str, np.ndarray], oracle: Mapping[str, np.ndarray], requests: list[Mapping[str, Any]], close: np.ndarray, corpus: Mapping[str, Any]) -> dict[str, Any]:
"""Serialize observed evidence; it is intentionally not an acceptance contract."""
records = []
for request in requests:
key = str(request["request_id"]); actual, expected = outputs[key], oracle[key]
metrics = _numeric_metrics(actual, expected) if actual.shape == expected.shape else _numeric_metrics(np.array([]), np.array([]))
records.append({"request_id": key, "family": FAMILIES[int(request["indicator_id"])], "shape": list(expected.shape), "dtype": expected.dtype.str, "nan_mask_exact": bool(actual.shape == expected.shape and np.array_equal(np.isnan(actual), np.isnan(expected))), **metrics, "decision": _continuity_decisions(close, expected), "actual_decision": _continuity_decisions(close, actual)})
# Decision vectors are retained as hashable evidence rather than JSON arrays.
for record in records:
record["decision_sha256"] = sha256_bytes(b"".join(array_sha256(value).encode() for value in record.pop("decision").values()))
record["actual_decision_sha256"] = sha256_bytes(b"".join(array_sha256(value).encode() for value in record.pop("actual_decision").values()))
return {"artifact": CALIBRATION_ARTIFACT, "schema_version": "1.1", "corpus": dict(corpus), "records": records}
def freeze_contract(calibration: Mapping[str, Any]) -> dict[str, Any]:
if calibration.get("artifact") != CALIBRATION_ARTIFACT: raise FrozenContractError("only V1_1 calibration evidence can be frozen")
fields = ("max_absolute_error", "max_relative_error", "mae", "max_ulp", "p50_ulp", "p95_ulp", "p99_ulp", "p99_9_ulp")
limits = {record["request_id"]: {key: record[key] for key in ("family", "shape", "dtype", "nan_mask_exact", *fields, "decision_sha256")} for record in calibration["records"]}
family_limits = {family: {field: max(record[field] for record in calibration["records"] if record["family"] == family) for field in fields} for family in sorted({record["family"] for record in calibration["records"]})}
return {"artifact": ARTIFACT, "schema_version": "1.1", "status": "frozen", "calibration_sha256": sha256_bytes(canonical_bytes(calibration)), "corpus": calibration["corpus"], "family_limits": family_limits, "feature_limits": limits}
def validate_frozen_contract(contract: Mapping[str, Any], outputs: Mapping[str, np.ndarray], oracle: Mapping[str, np.ndarray], close: np.ndarray) -> dict[str, Any]:
"""Fail closed against a supplied frozen contract; no limits are calculated here."""
if contract.get("artifact") != ARTIFACT or contract.get("schema_version") != "1.1" or contract.get("status") != "frozen": raise FrozenContractError("provided contract is not frozen GPU_FEATURE_PARITY_CONTRACT_V1_1")
limits, family_limits = contract.get("feature_limits"), contract.get("family_limits")
if not isinstance(limits, dict) or not isinstance(family_limits, dict) or set(limits) != set(oracle) or set(outputs) != set(oracle): raise FrozenContractError("contract, oracle, and GPU outputs must name exactly the same features")
records = []
for key, expected in oracle.items():
actual, limit = outputs[key], limits[key]
measured = _numeric_metrics(actual, expected) if actual.shape == expected.shape else _numeric_metrics(np.array([]), np.array([]))
decisions = _continuity_decisions(close, expected)
gpu_decisions = _continuity_decisions(close, actual) if actual.shape == close.shape else None
decision_hash = sha256_bytes(b"".join(array_sha256(value).encode() for value in decisions.values()))
family = family_limits.get(limit.get("family"))
numeric_violations = [name for name, value in measured.items() if not family or name not in limit or name not in family or value > float(limit[name]) or value > float(family[name])]
numeric_ok = not numeric_violations
passed = bool(actual.shape == tuple(limit["shape"]) == expected.shape and actual.dtype.str == limit["dtype"] == expected.dtype.str and np.array_equal(np.isnan(actual), np.isnan(expected)) and numeric_ok and decision_hash == limit["decision_sha256"] and gpu_decisions is not None and all(np.array_equal(decisions[name], gpu_decisions[name]) for name in decisions))
records.append({"request_id": key, "passed": passed, "numeric_violations": numeric_violations, **measured})
return {"artifact": VALIDATION_ARTIFACT, "schema_version": "1.1", "contract_sha256": sha256_bytes(canonical_bytes(contract)), "passed": all(item["passed"] for item in records), "records": records}