244 lines
17 KiB
Python
244 lines
17 KiB
Python
"""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}
|