Add GPU parity and feature gap analysis
This commit is contained in:
parent
c38f3d7efb
commit
203bc7917a
48 changed files with 95351 additions and 0 deletions
32
Dockerfile.gpu-feature-v1
Normal file
32
Dockerfile.gpu-feature-v1
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
FROM --platform=linux/arm64 nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends python3 python3-pip \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& python3 -m pip install --no-cache-dir --upgrade pip \
|
||||||
|
&& python3 -m pip install --no-cache-dir \
|
||||||
|
--index-url https://download.pytorch.org/whl/cu128 \
|
||||||
|
--extra-index-url https://pypi.org/simple \
|
||||||
|
torch==2.7.1+cu128 numpy
|
||||||
|
|
||||||
|
# Fail the ARM64 image build if it selected an incompatible interpreter or PyTorch wheel.
|
||||||
|
RUN test "$TARGETARCH" = arm64 \
|
||||||
|
&& python3 -c "import platform, torch; assert platform.machine() == 'aarch64'; assert torch.version.cuda == '12.8'; print(f'{platform.machine()} torch={torch.__version__} cuda={torch.version.cuda}')"
|
||||||
|
|
||||||
|
WORKDIR /opt/gpu-feature
|
||||||
|
COPY gpu_feature_engine_v1.py /opt/gpu-feature/gpu_feature_engine_v1.py
|
||||||
|
COPY gpu_batch01_v1_1_runner.py gpu_feature_parity_contract_v1_1.py /opt/gpu-feature/
|
||||||
|
COPY control_plane/trading_studio/indicators/historical_band_channel.py /opt/gpu-feature/control_plane/trading_studio/indicators/historical_band_channel.py
|
||||||
|
|
||||||
|
# Oracle and market data are intentionally supplied as read-only runtime mounts.
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
CUDA_DEVICE_ORDER=PCI_BUS_ID \
|
||||||
|
GPU_FEATURE_DATA_CSV=/data/binance_btcusdt_spot_2m_180d.csv \
|
||||||
|
GPU_FEATURE_ORACLE_NPZ=/oracle/batch01_oracle_outputs.npz \
|
||||||
|
GPU_FEATURE_REQUEST_JSON=/oracle/batch01_oracle_request.json \
|
||||||
|
GPU_FEATURE_CACHE_DIR=/cache
|
||||||
|
|
||||||
|
ENTRYPOINT ["python3", "/opt/gpu-feature/gpu_feature_engine_v1.py"]
|
||||||
|
CMD ["--mode", "smoke"]
|
||||||
337
cohort001_feature_failure_mining_v1.py
Normal file
337
cohort001_feature_failure_mining_v1.py
Normal file
|
|
@ -0,0 +1,337 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Read-only Cohort001 entry-to-frozen-oracle failure mining.
|
||||||
|
|
||||||
|
Associations in this report are observational and must not be interpreted as
|
||||||
|
causal feature effects. The runner never replays a strategy or alters inputs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
ARTIFACT = "COHORT001_FEATURE_FAILURE_MINING_V1"
|
||||||
|
GOOD_LABEL = "GOOD_ENTRY"
|
||||||
|
FAILURE_LABELS = ("WRONG_DIRECTION", "HIGH_MAE_ENTRY", "LATE_SIGNAL", "RECOVERY_DEPENDENT")
|
||||||
|
ENTRY_FIELDS = ("entry_bar", "entry_index", "entry_bar_index", "entry_idx", "bar_index")
|
||||||
|
|
||||||
|
|
||||||
|
def file_hash(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as source:
|
||||||
|
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||||
|
digest.update(block)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def directory_hash(path: Path) -> str:
|
||||||
|
"""Hash names and contents without loading the oracle into memory."""
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
for item in sorted(path.glob("*.npy")):
|
||||||
|
digest.update(item.name.encode("utf-8"))
|
||||||
|
digest.update(b"\0")
|
||||||
|
digest.update(bytes.fromhex(file_hash(item)))
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def json_compatible(value: Any) -> Any:
|
||||||
|
"""Convert NumPy values to strict JSON-compatible Python values."""
|
||||||
|
if isinstance(value, np.ndarray):
|
||||||
|
return json_compatible(value.tolist())
|
||||||
|
if isinstance(value, np.bool_):
|
||||||
|
return bool(value)
|
||||||
|
if isinstance(value, np.integer):
|
||||||
|
return int(value)
|
||||||
|
if isinstance(value, np.floating):
|
||||||
|
value = float(value)
|
||||||
|
if isinstance(value, float):
|
||||||
|
return value if np.isfinite(value) else None
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {json_compatible(key): json_compatible(item) for key, item in value.items()}
|
||||||
|
if isinstance(value, (list, tuple)):
|
||||||
|
return [json_compatible(item) for item in value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def load_engineering_map(path: Path) -> dict[str, dict[str, Any]]:
|
||||||
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
rows = payload.get("primitives", payload.get("requests", []))
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
raise ValueError("engineering map must contain primitives or requests")
|
||||||
|
result = {}
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
key = feature_key(row)
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if key in result:
|
||||||
|
raise ValueError(f"duplicate engineering primitive: {key}")
|
||||||
|
result[key] = dict(row, feature_key=key)
|
||||||
|
if not result:
|
||||||
|
raise ValueError("engineering map contains no usable primitives")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def feature_key(row: dict[str, Any]) -> str:
|
||||||
|
return f"{int(row['indicator_id'])}:{int(row['period'])}:{float(row['p1']):g}"
|
||||||
|
|
||||||
|
|
||||||
|
def load_semantics(path: Path, engineering: dict[str, dict[str, Any]]) -> dict[str, dict[str, str]]:
|
||||||
|
try:
|
||||||
|
import pyarrow.parquet as pq
|
||||||
|
except ImportError as error:
|
||||||
|
raise ValueError("pyarrow is required to read semantic-map parquet") from error
|
||||||
|
result = {key: {"domain": "unclassified", "output_type": "continuous"} for key in engineering}
|
||||||
|
for row in pq.read_table(path).to_pylist():
|
||||||
|
try:
|
||||||
|
key = str(row.get("feature_key") or feature_key(row))
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if key not in result:
|
||||||
|
continue
|
||||||
|
raw = str(row.get("output_type", row.get("semantic_type", row.get("value_type", "continuous")))).lower()
|
||||||
|
kind = "event" if raw in {"event", "detection", "binary_event"} else "state" if raw in {"state", "categorical", "boolean"} else "continuous"
|
||||||
|
result[key] = {"domain": str(row.get("domain", row.get("semantic_domain", row.get("engineering_family", row.get("family", "unclassified"))))), "output_type": kind}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def feature_keys_from_row(row: dict[str, Any]) -> list[str]:
|
||||||
|
"""Read explicit triples first, then the persisted flat five-triple genome."""
|
||||||
|
for field in ("feature_triples_json", "feature_triples"):
|
||||||
|
value = row.get(field)
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if isinstance(value, list):
|
||||||
|
keys = []
|
||||||
|
for triple in value:
|
||||||
|
if isinstance(triple, dict):
|
||||||
|
try:
|
||||||
|
keys.append(feature_key(triple))
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if keys:
|
||||||
|
return keys
|
||||||
|
value = row.get("genome_json", row.get("genome"))
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return []
|
||||||
|
combo = value.get("combo") if isinstance(value, dict) else None
|
||||||
|
if isinstance(combo, str):
|
||||||
|
try:
|
||||||
|
combo = json.loads(combo)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return []
|
||||||
|
if not isinstance(combo, list) or len(combo) < 15:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
return [f"{int(combo[index])}:{int(combo[index + 1])}:{float(combo[index + 2]):g}" for index in range(0, 15, 3)]
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def entry_bar(row: dict[str, Any]) -> tuple[int | None, str | None]:
|
||||||
|
"""Resolve only an integer bar offset; timestamps are intentionally not guessed."""
|
||||||
|
containers = [("column", row)]
|
||||||
|
for field in ("raw_ledger_json", "context_json"):
|
||||||
|
value = row.get(field)
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if isinstance(value, dict):
|
||||||
|
containers.append((field, value))
|
||||||
|
for source, values in containers:
|
||||||
|
for field in ENTRY_FIELDS:
|
||||||
|
value = values.get(field)
|
||||||
|
if isinstance(value, bool) or value is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
numeric = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if np.isfinite(numeric) and numeric.is_integer() and numeric >= 0:
|
||||||
|
return int(numeric), f"{source}.{field}"
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def checkpoint_paths(directory: Path, engineering: dict[str, dict[str, Any]]) -> dict[str, Path]:
|
||||||
|
paths = {}
|
||||||
|
for path in directory.glob("*.npy"):
|
||||||
|
parts = path.stem.split("_")
|
||||||
|
if len(parts) != 4 or parts[0] != "hs22":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
key = f"{int(parts[1])}:{int(parts[2])}:{float(parts[3]):g}"
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if key in engineering and key not in paths:
|
||||||
|
paths[key] = path
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def ks_statistic(good: np.ndarray, failure: np.ndarray) -> tuple[float, str]:
|
||||||
|
try:
|
||||||
|
from scipy.stats import ks_2samp
|
||||||
|
|
||||||
|
return float(ks_2samp(good, failure, method="auto").statistic), "scipy_ks_2samp"
|
||||||
|
except ImportError:
|
||||||
|
# Exact empirical CDF distance evaluated at every observed rank boundary.
|
||||||
|
values = np.sort(np.concatenate((good, failure)))
|
||||||
|
left = np.searchsorted(np.sort(good), values, side="right") / good.size
|
||||||
|
right = np.searchsorted(np.sort(failure), values, side="right") / failure.size
|
||||||
|
return float(np.max(np.abs(left - right))), "exact_rank_approx"
|
||||||
|
|
||||||
|
|
||||||
|
def odds_ratio(good: np.ndarray, failure: np.ndarray) -> float | None:
|
||||||
|
if not np.all(np.isin(np.concatenate((good, failure)), (0, 1))):
|
||||||
|
return None
|
||||||
|
# Haldane-Anscombe correction keeps complete separation reportable.
|
||||||
|
good_on, failure_on = good.sum(), failure.sum()
|
||||||
|
return float(((failure_on + 0.5) / (failure.size - failure_on + 0.5)) / ((good_on + 0.5) / (good.size - good_on + 0.5)))
|
||||||
|
|
||||||
|
|
||||||
|
def comparison(good: list[tuple[float, str]], failure: list[tuple[float, str]]) -> dict[str, Any]:
|
||||||
|
good_values, failure_values = np.array([item[0] for item in good]), np.array([item[0] for item in failure])
|
||||||
|
median_difference = float(np.median(failure_values) - np.median(good_values))
|
||||||
|
pooled = np.concatenate((good_values, failure_values))
|
||||||
|
good_variance = np.var(good_values, ddof=1) if good_values.size > 1 else 0.0
|
||||||
|
failure_variance = np.var(failure_values, ddof=1) if failure_values.size > 1 else 0.0
|
||||||
|
degrees_of_freedom = good_values.size + failure_values.size - 2
|
||||||
|
scale = float(np.sqrt(((good_values.size - 1) * good_variance + (failure_values.size - 1) * failure_variance) / degrees_of_freedom)) if degrees_of_freedom else 0.0
|
||||||
|
effect = median_difference / scale if scale else None
|
||||||
|
pooled_sorted = np.sort(pooled)
|
||||||
|
quantile_separation = float(
|
||||||
|
np.searchsorted(pooled_sorted, np.median(failure_values), side="right") / pooled.size
|
||||||
|
- np.searchsorted(pooled_sorted, np.median(good_values), side="right") / pooled.size
|
||||||
|
)
|
||||||
|
ks, ks_method = ks_statistic(good_values, failure_values)
|
||||||
|
direction = np.sign(median_difference)
|
||||||
|
folds = []
|
||||||
|
for fold in sorted(set(item[1] for item in good) & set(item[1] for item in failure)):
|
||||||
|
fold_difference = np.median([item[0] for item in failure if item[1] == fold]) - np.median([item[0] for item in good if item[1] == fold])
|
||||||
|
folds.append(float(fold_difference))
|
||||||
|
consistent = sum(np.sign(item) == direction for item in folds) if direction else sum(item == 0 for item in folds)
|
||||||
|
return {
|
||||||
|
"good_samples": int(good_values.size), "failure_samples": int(failure_values.size),
|
||||||
|
"median_difference_failure_minus_good": median_difference,
|
||||||
|
"standardized_effect_size": effect, "standardized_effect_size_method": "median_difference_over_pooled_within_group_standard_deviation", "quantile_separation": quantile_separation,
|
||||||
|
"ks_statistic": ks, "ks_method": ks_method,
|
||||||
|
"folds_compared": len(folds), "fold_direction_consistent": consistent,
|
||||||
|
"fold_consistency": consistent / len(folds) if folds else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mine(rows: list[dict[str, Any]], engineering: dict[str, dict[str, Any]], semantics: dict[str, dict[str, str]], paths: dict[str, Path]) -> tuple[list[dict[str, Any]], dict[str, int], list[str]]:
|
||||||
|
samples: dict[str, dict[str, list[tuple[float, str]]]] = defaultdict(lambda: defaultdict(list))
|
||||||
|
skipped = Counter()
|
||||||
|
schemas = set()
|
||||||
|
arrays: dict[str, np.ndarray] = {}
|
||||||
|
for row in rows:
|
||||||
|
label = row.get("failure_label")
|
||||||
|
if label not in (GOOD_LABEL, *FAILURE_LABELS):
|
||||||
|
continue
|
||||||
|
bar, schema = entry_bar(row)
|
||||||
|
keys = feature_keys_from_row(row)
|
||||||
|
if bar is None:
|
||||||
|
skipped["unmappable_entry_bar"] += 1
|
||||||
|
continue
|
||||||
|
if not keys:
|
||||||
|
skipped["unmappable_feature_triples"] += 1
|
||||||
|
continue
|
||||||
|
schemas.add(schema)
|
||||||
|
fold = str(row.get("fold", "unknown"))
|
||||||
|
for key in set(keys):
|
||||||
|
if key not in engineering or key not in paths:
|
||||||
|
skipped["feature_missing_from_oracle"] += 1
|
||||||
|
continue
|
||||||
|
values = arrays.setdefault(key, np.load(paths[key], allow_pickle=False, mmap_mode="r").reshape(-1))
|
||||||
|
if bar >= values.size:
|
||||||
|
skipped["entry_bar_outside_oracle"] += 1
|
||||||
|
continue
|
||||||
|
value = float(values[bar])
|
||||||
|
if not np.isfinite(value):
|
||||||
|
skipped["nonfinite_oracle_value"] += 1
|
||||||
|
continue
|
||||||
|
samples[key][label].append((value, fold))
|
||||||
|
output = []
|
||||||
|
for key in sorted(samples):
|
||||||
|
good = samples[key][GOOD_LABEL]
|
||||||
|
if not good:
|
||||||
|
continue
|
||||||
|
for label in FAILURE_LABELS:
|
||||||
|
failure = samples[key][label]
|
||||||
|
if not failure:
|
||||||
|
continue
|
||||||
|
result = {"feature_key": key, "failure_label": label, "attribution": "ASSOCIATIVE_NOT_CAUSAL", **engineering[key], **semantics[key], **comparison(good, failure)}
|
||||||
|
if semantics[key]["output_type"] in {"state", "event"}:
|
||||||
|
result["state_event_odds_ratio_failure_vs_good"] = odds_ratio(np.array([x[0] for x in good]), np.array([x[0] for x in failure]))
|
||||||
|
output.append(result)
|
||||||
|
return output, dict(sorted(skipped.items())), sorted(schema for schema in schemas if schema)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--failure-context", type=Path, required=True)
|
||||||
|
parser.add_argument("--semantic-map", type=Path, required=True)
|
||||||
|
parser.add_argument("--oracle-checkpoint-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--engineering-map", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-dir", type=Path, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not args.oracle_checkpoint_dir.is_dir():
|
||||||
|
parser.error("--oracle-checkpoint-dir must be a directory")
|
||||||
|
for path in (args.failure_context, args.semantic_map, args.engineering_map):
|
||||||
|
if not path.is_file():
|
||||||
|
parser.error(f"input is not a file: {path}")
|
||||||
|
try:
|
||||||
|
import pyarrow as pa
|
||||||
|
import pyarrow.parquet as pq
|
||||||
|
except ImportError as error:
|
||||||
|
raise SystemExit("pyarrow is required for Cohort001 failure mining") from error
|
||||||
|
engineering = load_engineering_map(args.engineering_map)
|
||||||
|
semantics = load_semantics(args.semantic_map, engineering)
|
||||||
|
paths = checkpoint_paths(args.oracle_checkpoint_dir, engineering)
|
||||||
|
rows = pq.read_table(args.failure_context).to_pylist()
|
||||||
|
features, skipped, entry_schemas = mine(rows, engineering, semantics, paths)
|
||||||
|
domains: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
|
||||||
|
for item in features:
|
||||||
|
domains[(item["domain"], item["failure_label"])].append(item)
|
||||||
|
domain_rows = [{
|
||||||
|
"domain": domain, "failure_label": label, "feature_comparisons": len(items),
|
||||||
|
"median_standardized_effect_size": float(np.median([x["standardized_effect_size"] for x in items if x["standardized_effect_size"] is not None])) if any(x["standardized_effect_size"] is not None for x in items) else None,
|
||||||
|
"median_ks_statistic": float(np.median([x["ks_statistic"] for x in items])),
|
||||||
|
"median_fold_consistency": float(np.median([x["fold_consistency"] for x in items if x["fold_consistency"] is not None])) if any(x["fold_consistency"] is not None for x in items) else None,
|
||||||
|
"attribution": "ASSOCIATIVE_NOT_CAUSAL",
|
||||||
|
} for (domain, label), items in sorted(domains.items())]
|
||||||
|
result = {
|
||||||
|
"schema_version": 1, "artifact": ARTIFACT, "read_only": True,
|
||||||
|
"attribution": "ASSOCIATIVE_NOT_CAUSAL",
|
||||||
|
"attribution_note": "Entry-time associations do not establish causal feature effects.",
|
||||||
|
"inputs": {
|
||||||
|
str(path): file_hash(path) for path in (args.failure_context, args.semantic_map, args.engineering_map)
|
||||||
|
} | {str(args.oracle_checkpoint_dir): directory_hash(args.oracle_checkpoint_dir)},
|
||||||
|
"entry_bar_schema_detected": entry_schemas,
|
||||||
|
"failure_context_rows": len(rows), "oracle_features_available": len(paths),
|
||||||
|
"skipped": skipped, "feature_failure_comparisons": features, "domain_aggregates": domain_rows,
|
||||||
|
}
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
pq.write_table(pa.Table.from_pylist(features), args.output_dir / "cohort001_feature_failure_mining_v1.parquet", compression="zstd")
|
||||||
|
output = args.output_dir / "cohort001_feature_failure_mining_v1.json"
|
||||||
|
output.write_text(json.dumps(json_compatible(result), indent=2, sort_keys=True, allow_nan=False) + "\n", encoding="utf-8")
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import fields
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.db import connection, transaction
|
||||||
|
|
||||||
|
from control_plane.trading_studio.indicators.feature_catalog import feature_definition
|
||||||
|
from control_plane.trading_studio.indicators.schema import HS22SchemaError, parse_hs22
|
||||||
|
from control_plane.trading_studio.management.commands.cohort_qualification_hyperscalper_001 import (
|
||||||
|
RUNNER_NAME as QUALIFICATION_RUNNER,
|
||||||
|
)
|
||||||
|
from control_plane.trading_studio.management.commands.materialize_hyperscalper_cohort_001 import (
|
||||||
|
DATASET_SHA256,
|
||||||
|
DATASET_VERSION_ID,
|
||||||
|
)
|
||||||
|
from control_plane.trading_studio.management.commands.specimen_hyperscalper_cohort_001 import (
|
||||||
|
RUNNER_NAME as SPECIMEN_RUNNER,
|
||||||
|
SCENARIO_NAMES,
|
||||||
|
)
|
||||||
|
from control_plane.trading_studio.models import QualificationReplayRun, TradingCohort
|
||||||
|
from control_plane.trading_studio.qualification import DEQPrimitives, ReplayMode
|
||||||
|
|
||||||
|
|
||||||
|
ALLOWED_RUNNERS = frozenset((SPECIMEN_RUNNER, QUALIFICATION_RUNNER))
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Read-only JSON export of completed Cohort001 CANARY_ONLY DEQ ledger data."
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("--cohort-id", required=True, type=UUID)
|
||||||
|
parser.add_argument("--output", required=True, help="JSON output path.")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
output = Path(options["output"])
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
# Must be the first statement in this transaction; PostgreSQL rejects writes thereafter.
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("SET TRANSACTION READ ONLY")
|
||||||
|
payload = self._payload(options["cohort_id"])
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output.write_text(
|
||||||
|
json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=True) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
except (OSError, ValueError, TradingCohort.DoesNotExist) as error:
|
||||||
|
raise CommandError(str(error)) from error
|
||||||
|
self.stdout.write(json.dumps({"cohort_id": str(options["cohort_id"]), "output": str(output)}))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _payload(cls, cohort_id: UUID) -> dict:
|
||||||
|
cohort = TradingCohort.objects.select_related("dataset_version").get(pk=cohort_id)
|
||||||
|
cls._validate_cohort(cohort)
|
||||||
|
memberships = list(
|
||||||
|
cohort.memberships.select_related("strategy_version").order_by("ordinal")
|
||||||
|
)
|
||||||
|
if len(memberships) != 20 or [member.ordinal for member in memberships] != list(range(1, 21)):
|
||||||
|
raise ValueError("Cohort001 must contain exactly 20 ordered memberships.")
|
||||||
|
|
||||||
|
runs = list(
|
||||||
|
QualificationReplayRun.objects.filter(
|
||||||
|
strategy_version_id__in=[member.strategy_version_id for member in memberships],
|
||||||
|
dataset_version_id=cohort.dataset_version_id,
|
||||||
|
replay_mode=ReplayMode.CANARY_ONLY,
|
||||||
|
runner_name__in=ALLOWED_RUNNERS,
|
||||||
|
)
|
||||||
|
.prefetch_related("ledger_rows")
|
||||||
|
.order_by("strategy_version_id", "fold", "scenario_name", "runner_name")
|
||||||
|
)
|
||||||
|
run_index = cls._validated_run_index(memberships, runs)
|
||||||
|
folds = cohort.policy_snapshot["reconstruction_v1"]["folds"]
|
||||||
|
|
||||||
|
strategies = []
|
||||||
|
for member in memberships:
|
||||||
|
runner = SPECIMEN_RUNNER if member.ordinal == 1 else QUALIFICATION_RUNNER
|
||||||
|
strategy = {
|
||||||
|
"ordinal": member.ordinal,
|
||||||
|
"membership_id": str(member.id),
|
||||||
|
"strategy_version_id": str(member.strategy_version_id),
|
||||||
|
"runner_name": runner,
|
||||||
|
"genome": member.strategy_version.genome,
|
||||||
|
"feature_triples": cls._feature_triples(member.strategy_version.genome),
|
||||||
|
"folds": [],
|
||||||
|
}
|
||||||
|
for fold in folds:
|
||||||
|
fold_name = fold["fold"]
|
||||||
|
scenarios = []
|
||||||
|
for scenario_name in SCENARIO_NAMES:
|
||||||
|
run = run_index[(member.strategy_version_id, fold_name, scenario_name)]
|
||||||
|
ledger = [row.result for row in sorted(run.ledger_rows.all(), key=lambda row: row.sequence)]
|
||||||
|
scenarios.append(
|
||||||
|
{
|
||||||
|
"qualification_run_id": str(run.id),
|
||||||
|
"scenario": scenario_name,
|
||||||
|
"summary": run.summary,
|
||||||
|
"deq_samples": ledger,
|
||||||
|
"deq_aggregate": cls._deq_aggregate(ledger),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
strategy["folds"].append({"fold": fold_name, "scenarios": scenarios})
|
||||||
|
strategies.append(strategy)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"contract": "Cohort001-DEQ-ledger-export-v1",
|
||||||
|
"cohort_id": str(cohort.id),
|
||||||
|
"dataset_version_id": str(cohort.dataset_version_id),
|
||||||
|
"dataset_sha256": cohort.dataset_version.content_hash,
|
||||||
|
"replay_mode": ReplayMode.CANARY_ONLY,
|
||||||
|
"allowed_runner_names": sorted(ALLOWED_RUNNERS),
|
||||||
|
"strategies": strategies,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_cohort(cohort) -> None:
|
||||||
|
reconstruction = cohort.policy_snapshot.get("reconstruction_v1")
|
||||||
|
if (
|
||||||
|
cohort.dataset_version_id != DATASET_VERSION_ID
|
||||||
|
or cohort.dataset_version.content_hash != DATASET_SHA256
|
||||||
|
or not isinstance(reconstruction, dict)
|
||||||
|
or reconstruction.get("provenance") != "RECONSTRUCTED_FROM_FROZEN_SPEC_V1"
|
||||||
|
or not isinstance(reconstruction.get("folds"), list)
|
||||||
|
or len(reconstruction["folds"]) != 4
|
||||||
|
):
|
||||||
|
raise ValueError("Cohort or dataset is not the validated frozen Cohort001 canary dataset.")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _validated_run_index(cls, memberships, runs) -> dict:
|
||||||
|
member_by_strategy = {member.strategy_version_id: member for member in memberships}
|
||||||
|
expected = {
|
||||||
|
(member.strategy_version_id, fold["fold"], scenario)
|
||||||
|
for member in memberships
|
||||||
|
for fold in member.cohort.policy_snapshot["reconstruction_v1"]["folds"]
|
||||||
|
for scenario in SCENARIO_NAMES
|
||||||
|
}
|
||||||
|
indexed = {}
|
||||||
|
for run in runs:
|
||||||
|
member = member_by_strategy[run.strategy_version_id]
|
||||||
|
expected_runner = SPECIMEN_RUNNER if member.ordinal == 1 else QUALIFICATION_RUNNER
|
||||||
|
key = (run.strategy_version_id, run.fold, run.scenario_name)
|
||||||
|
if run.runner_name != expected_runner or key not in expected or key in indexed:
|
||||||
|
raise ValueError("Cohort001 contains an unexpected or duplicate CANARY_ONLY replay.")
|
||||||
|
indexed[key] = run
|
||||||
|
if set(indexed) != expected:
|
||||||
|
raise ValueError("Cohort001 DEQ export requires a complete 20 x 4 x 13 replay matrix.")
|
||||||
|
return indexed
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _feature_triples(genome: dict) -> list[dict] | None:
|
||||||
|
try:
|
||||||
|
state = parse_hs22(genome.get("combo"))
|
||||||
|
except (AttributeError, HS22SchemaError, TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
return [feature_definition(variant).artifact() for variant in state.variants()]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _deq_aggregate(ledger: list[dict]) -> dict:
|
||||||
|
result = {"trade_count": len(ledger), "fields": {}}
|
||||||
|
for field in fields(DEQPrimitives):
|
||||||
|
values = [row.get("deq", {}).get(field.name) for row in ledger]
|
||||||
|
available = [value for value in values if value is not None]
|
||||||
|
aggregate = {
|
||||||
|
"available_count": len(available),
|
||||||
|
"null_count": len(values) - len(available),
|
||||||
|
}
|
||||||
|
if available and all(isinstance(value, bool) for value in available):
|
||||||
|
aggregate.update(true_count=sum(available), false_count=len(available) - sum(available))
|
||||||
|
elif available:
|
||||||
|
aggregate.update(min=min(available), max=max(available), mean=sum(available) / len(available))
|
||||||
|
else:
|
||||||
|
aggregate["mean"] = None
|
||||||
|
result["fields"][field.name] = aggregate
|
||||||
|
return result
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.db import connection, transaction
|
||||||
|
|
||||||
|
from control_plane.trading_studio.management.commands.export_cohort_deq_v1 import Command as DEQExport
|
||||||
|
|
||||||
|
|
||||||
|
CONTRACT = "Cohort001-failure-context-export-v1"
|
||||||
|
LABEL_VERSION = "v1"
|
||||||
|
# All signed DEQ values are already expressed in the trade direction.
|
||||||
|
THRESHOLDS = {
|
||||||
|
"good_entry_return_1_bars_bps_min": 5.0,
|
||||||
|
"wrong_direction_return_1_bars_bps_max": -5.0,
|
||||||
|
"late_signal_return_1_bars_bps_max": 0.0,
|
||||||
|
"late_signal_return_5_bars_bps_min": 5.0,
|
||||||
|
"high_mae_entry_bps_max": -25.0,
|
||||||
|
"recovery_dependent_bars_min": 3,
|
||||||
|
}
|
||||||
|
LABEL_PRECEDENCE = (
|
||||||
|
"GOOD_SIGNAL_BAD_MONETIZATION",
|
||||||
|
"RECOVERY_DEPENDENT",
|
||||||
|
"HIGH_MAE_ENTRY",
|
||||||
|
"WRONG_DIRECTION",
|
||||||
|
"LATE_SIGNAL",
|
||||||
|
"GOOD_ENTRY",
|
||||||
|
"NO_EDGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Read-only Parquet export of Cohort001 DEQ failure context and analytical labels."
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("--cohort-id", required=True, type=UUID)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-dir",
|
||||||
|
required=True,
|
||||||
|
help="Directory for cohort_failure_context_v1.parquet and its manifest.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--parquet-output", help="Optional explicit Parquet output path.")
|
||||||
|
parser.add_argument("--manifest-output", help="Optional explicit manifest output path.")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
output_dir = Path(options["output_dir"])
|
||||||
|
parquet_path = Path(options["parquet_output"] or output_dir / "cohort_failure_context_v1.parquet")
|
||||||
|
manifest_path = Path(
|
||||||
|
options["manifest_output"] or output_dir / "cohort_failure_context_v1.manifest.json"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
self._set_read_only_transaction()
|
||||||
|
payload = DEQExport._payload(options["cohort_id"])
|
||||||
|
rows = self._rows(payload)
|
||||||
|
self._write_parquet(rows, parquet_path)
|
||||||
|
manifest = self._manifest(payload, rows, parquet_path)
|
||||||
|
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
manifest_path.write_text(
|
||||||
|
json.dumps(manifest, indent=2, sort_keys=True, ensure_ascii=True) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
except (ImportError, OSError, ValueError) as error:
|
||||||
|
raise CommandError(str(error)) from error
|
||||||
|
self.stdout.write(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"cohort_id": str(options["cohort_id"]),
|
||||||
|
"parquet": str(parquet_path),
|
||||||
|
"manifest": str(manifest_path),
|
||||||
|
"database_writes": False,
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_read_only_transaction() -> None:
|
||||||
|
"""Use the database's explicit read-only mode where it is supported."""
|
||||||
|
if connection.vendor == "postgresql":
|
||||||
|
# This must be the first database statement in the atomic block.
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("SET TRANSACTION READ ONLY")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _rows(cls, payload: dict) -> list[dict]:
|
||||||
|
rows = []
|
||||||
|
for strategy in payload["strategies"]:
|
||||||
|
for fold in strategy["folds"]:
|
||||||
|
for scenario in fold["scenarios"]:
|
||||||
|
rescue = scenario["summary"].get("rescue")
|
||||||
|
for sequence, ledger in enumerate(scenario["deq_samples"]):
|
||||||
|
deq = ledger.get("deq", {})
|
||||||
|
label, rationale = cls._label(ledger, deq)
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"cohort_id": payload["cohort_id"],
|
||||||
|
"dataset_version_id": payload["dataset_version_id"],
|
||||||
|
"dataset_sha256": payload["dataset_sha256"],
|
||||||
|
"ordinal": strategy["ordinal"],
|
||||||
|
"membership_id": strategy["membership_id"],
|
||||||
|
"strategy_version_id": strategy["strategy_version_id"],
|
||||||
|
"runner_name": strategy["runner_name"],
|
||||||
|
"qualification_run_id": scenario["qualification_run_id"],
|
||||||
|
"fold": fold["fold"],
|
||||||
|
"scenario": scenario["scenario"],
|
||||||
|
"ledger_sequence": sequence,
|
||||||
|
"ledger_identity": ledger.get("identity"),
|
||||||
|
"direction": cls._direction(ledger),
|
||||||
|
"signal_bar": ledger.get("signal_bar"),
|
||||||
|
"entry_bar": ledger.get("entry_bar"),
|
||||||
|
"entry_at": ledger.get("entry_at"),
|
||||||
|
"entry_reference_price": ledger.get("entry_reference_price"),
|
||||||
|
"entry_execution_price": ledger.get("entry_execution_price"),
|
||||||
|
"net_pnl": ledger.get("net_pnl"),
|
||||||
|
"gross_pnl": ledger.get("gross_pnl"),
|
||||||
|
"failure_label": label,
|
||||||
|
"label_rationale": rationale,
|
||||||
|
"label_version": LABEL_VERSION,
|
||||||
|
"return_1_bars_bps": deq.get("return_1_bars_bps"),
|
||||||
|
"return_5_bars_bps": deq.get("return_5_bars_bps"),
|
||||||
|
"mfe_bps": deq.get("mfe_bps"),
|
||||||
|
"mae_bps": deq.get("mae_bps"),
|
||||||
|
"winner_negative_first": deq.get("winner_negative_first"),
|
||||||
|
"recovery_bars": deq.get("recovery_bars"),
|
||||||
|
# JSON columns preserve every raw ledger, genome, context, and rescue field.
|
||||||
|
"raw_deq_json": cls._canonical(deq),
|
||||||
|
"raw_ledger_json": cls._canonical(ledger),
|
||||||
|
"genome_json": cls._canonical(strategy["genome"]),
|
||||||
|
"feature_triples_json": cls._canonical(strategy["feature_triples"]),
|
||||||
|
"run_summary_json": cls._canonical(scenario["summary"]),
|
||||||
|
"rescue_json": cls._canonical(rescue),
|
||||||
|
"context_json": cls._canonical(ledger.get("context")),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _direction(ledger: dict) -> str | None:
|
||||||
|
take_profit = ledger.get("take_profit_price")
|
||||||
|
entry = ledger.get("entry_execution_price")
|
||||||
|
if take_profit is None or entry is None:
|
||||||
|
return None
|
||||||
|
return "LONG" if take_profit > entry else "SHORT" if take_profit < entry else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _label(ledger: dict, deq: dict) -> tuple[str, str]:
|
||||||
|
r1 = deq.get("return_1_bars_bps")
|
||||||
|
r5 = deq.get("return_5_bars_bps")
|
||||||
|
mae = deq.get("mae_bps")
|
||||||
|
recovery = deq.get("recovery_bars")
|
||||||
|
net_pnl = ledger.get("net_pnl")
|
||||||
|
good_signal = r1 is not None and r1 >= THRESHOLDS["good_entry_return_1_bars_bps_min"]
|
||||||
|
if good_signal and net_pnl is not None and net_pnl <= 0:
|
||||||
|
return "GOOD_SIGNAL_BAD_MONETIZATION", "return_1_bars_bps >= 5 and net_pnl <= 0"
|
||||||
|
if net_pnl is not None and net_pnl > 0 and (
|
||||||
|
deq.get("winner_negative_first") is True
|
||||||
|
or (recovery is not None and recovery >= THRESHOLDS["recovery_dependent_bars_min"])
|
||||||
|
):
|
||||||
|
return "RECOVERY_DEPENDENT", "profitable after negative-first path or recovery >= 3 bars"
|
||||||
|
if mae is not None and mae <= THRESHOLDS["high_mae_entry_bps_max"]:
|
||||||
|
return "HIGH_MAE_ENTRY", "mae_bps <= -25"
|
||||||
|
if r1 is not None and r1 <= THRESHOLDS["wrong_direction_return_1_bars_bps_max"]:
|
||||||
|
return "WRONG_DIRECTION", "return_1_bars_bps <= -5"
|
||||||
|
if (
|
||||||
|
r1 is not None
|
||||||
|
and r5 is not None
|
||||||
|
and r1 <= THRESHOLDS["late_signal_return_1_bars_bps_max"]
|
||||||
|
and r5 >= THRESHOLDS["late_signal_return_5_bars_bps_min"]
|
||||||
|
):
|
||||||
|
return "LATE_SIGNAL", "return_1_bars_bps <= 0 and return_5_bars_bps >= 5"
|
||||||
|
if good_signal:
|
||||||
|
return "GOOD_ENTRY", "return_1_bars_bps >= 5"
|
||||||
|
return "NO_EDGE", "no preceding label rule matched"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _canonical(value) -> str:
|
||||||
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _write_parquet(cls, rows: list[dict], path: Path) -> None:
|
||||||
|
try:
|
||||||
|
import pyarrow as pa
|
||||||
|
import pyarrow.parquet as pq
|
||||||
|
except ImportError as error:
|
||||||
|
raise ImportError("pyarrow is required to write the Parquet export.") from error
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
pq.write_table(pa.Table.from_pylist(rows), path, compression="zstd")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _manifest(cls, payload: dict, rows: list[dict], parquet_path: Path) -> dict:
|
||||||
|
labels = Counter(row["failure_label"] for row in rows)
|
||||||
|
return {
|
||||||
|
"contract": CONTRACT,
|
||||||
|
"cohort_id": payload["cohort_id"],
|
||||||
|
"dataset_version_id": payload["dataset_version_id"],
|
||||||
|
"dataset_sha256": payload["dataset_sha256"],
|
||||||
|
"source_contract": payload["contract"],
|
||||||
|
"allowed_runner_names": payload["allowed_runner_names"],
|
||||||
|
"row_count": len(rows),
|
||||||
|
"label_counts": {label: labels.get(label, 0) for label in LABEL_PRECEDENCE},
|
||||||
|
"label_version": LABEL_VERSION,
|
||||||
|
"label_precedence": list(LABEL_PRECEDENCE),
|
||||||
|
"thresholds": THRESHOLDS,
|
||||||
|
"parquet": {
|
||||||
|
"path": str(parquet_path),
|
||||||
|
"sha256": cls._file_hash(parquet_path),
|
||||||
|
},
|
||||||
|
"provenance": {
|
||||||
|
"source": "persisted QualificationReplayRun and QualificationReplayLedger records",
|
||||||
|
"replay_mode": payload["replay_mode"],
|
||||||
|
"raw_fields": [
|
||||||
|
"raw_deq_json", "raw_ledger_json", "genome_json", "feature_triples_json",
|
||||||
|
"run_summary_json", "rescue_json", "context_json",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"no_write_confirmation": {
|
||||||
|
"database_writes": False,
|
||||||
|
"read_only_transaction": True,
|
||||||
|
"note": "Only the requested Parquet and manifest artifact paths were written.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _file_hash(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as artifact:
|
||||||
|
for block in iter(lambda: artifact.read(1024 * 1024), b""):
|
||||||
|
digest.update(block)
|
||||||
|
return digest.hexdigest()
|
||||||
1171
gpu_batch01_decision_equivalence_v1.json
Normal file
1171
gpu_batch01_decision_equivalence_v1.json
Normal file
File diff suppressed because it is too large
Load diff
21
gpu_batch01_independent_review_v1_2.md
Normal file
21
gpu_batch01_independent_review_v1_2.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# GPU Batch01 Independent Review V1.2
|
||||||
|
|
||||||
|
Decision: `REJECT`
|
||||||
|
|
||||||
|
## Exact Blockers
|
||||||
|
|
||||||
|
1. All 97 features do not pass validation: calibration 94/97, holdout 46/97, and adversarial 67/97.
|
||||||
|
2. Supertrend ATR limits were frozen as zero by the runner, not at the approved bounded values. The period-10 ATR calibration drift is `7.105427357601002e-15`; the holdout drift is `1.4210854715202004e-14`.
|
||||||
|
3. Calibration-derived continuous limits do not generalize: holdout fails 48 Bollinger/Keltner requests plus three Supertrend ATR requests; adversarial fails all 30 Keltner requests.
|
||||||
|
4. V1.2 contains no performance artifact or complete runtime provenance.
|
||||||
|
5. ATR validation does not require ATR shape and NaN-mask equality before measuring finite errors.
|
||||||
|
|
||||||
|
## Satisfied Checks
|
||||||
|
|
||||||
|
- The contract contains 97 unique requests and was frozen before holdout/adversarial validation.
|
||||||
|
- Reported finite numeric limits are mechanically enforced.
|
||||||
|
- Donchian and PSAR outputs pass exact checks on supplied evidence.
|
||||||
|
- Supertrend bands, predicates, direction, transitions, and final outputs pass exact checks.
|
||||||
|
- Historical role semantics are honestly marked `UNVERIFIABLE_FROM_RECOVERED_SOURCE` and non-blocking.
|
||||||
|
|
||||||
|
`GPU_FEATURE_PARITY_CONTRACT_V1_2_VALIDATED` must not be issued.
|
||||||
1907
gpu_batch01_numeric_analysis_v1.json
Normal file
1907
gpu_batch01_numeric_analysis_v1.json
Normal file
File diff suppressed because it is too large
Load diff
1
gpu_batch01_performance_metrics_v1_1.json
Normal file
1
gpu_batch01_performance_metrics_v1_1.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"artifact":"GPU_BATCH01_PERFORMANCE_METRICS_V1_1","families":[{"cold":{"compute_seconds":0.003065667988266796,"device_to_host_seconds":0.0006752620101906359,"host_to_device_seconds":0.00010705000022426248,"max_gpu_memory_bytes":48369152},"cold_values_per_second":760931715.0220335,"family":"bollinger","values":2332764,"variants":36,"warm":{"compute_seconds":0.0030614440329372883,"device_to_host_seconds":0.0006707339780405164,"host_to_device_seconds":7.96440290287137e-05,"max_gpu_memory_bytes":48369152},"warm_values_per_second":761981592.6413786},{"cold":{"compute_seconds":0.00028294604271650314,"device_to_host_seconds":0.0001819260069169104,"host_to_device_seconds":7.823598571121693e-05,"max_gpu_memory_bytes":7779840},"cold_values_per_second":2290153959.316022,"family":"donchian","values":647990,"variants":10,"warm":{"compute_seconds":0.0002682899939827621,"device_to_host_seconds":0.00018216704484075308,"host_to_device_seconds":7.761199958622456e-05,"max_gpu_memory_bytes":7779840},"warm_values_per_second":2415259661.311238},{"cold":{"compute_seconds":30.859290324966423,"device_to_host_seconds":0.0008443809929303825,"host_to_device_seconds":7.618800736963749e-05,"max_gpu_memory_bytes":27707904},"cold_values_per_second":75593.57248448124,"family":"keltner","values":2332764,"variants":36,"warm":{"compute_seconds":30.80078762996709,"device_to_host_seconds":0.0007362319738604128,"host_to_device_seconds":0.00010204297723248601,"max_gpu_memory_bytes":27707904},"warm_values_per_second":75737.1541281749},{"cold":{"compute_seconds":9.649663921969477,"device_to_host_seconds":0.00011956499656662345,"host_to_device_seconds":0.00010370800737291574,"max_gpu_memory_bytes":4148736},"cold_values_per_second":20145.468440347915,"family":"psar","values":194397,"variants":3,"warm":{"compute_seconds":9.652134977979586,"device_to_host_seconds":0.00010578898945823312,"host_to_device_seconds":9.908498032018542e-05,"max_gpu_memory_bytes":4148736},"warm_values_per_second":20140.310971976458},{"cold":{"compute_seconds":34.126689486030955,"device_to_host_seconds":0.00030309800058603287,"host_to_device_seconds":9.794998914003372e-05,"max_gpu_memory_bytes":12444672},"cold_values_per_second":22785.333465125277,"family":"supertrend","values":777588,"variants":12,"warm":{"compute_seconds":34.110405425017234,"device_to_host_seconds":0.0002850039745680988,"host_to_device_seconds":0.00011255801655352116,"max_gpu_memory_bytes":12444672},"warm_values_per_second":22796.211018638372}],"measurement":"CUDA synchronize before each interval; cold is first family invocation, warm is second"}
|
||||||
1
gpu_batch01_stateful_trace_comparison_v1_1.json
Normal file
1
gpu_batch01_stateful_trace_comparison_v1_1.json
Normal file
File diff suppressed because one or more lines are too long
1
gpu_batch01_v1_1_evidence_manifest.json
Normal file
1
gpu_batch01_v1_1_evidence_manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"artifact":"GPU_BATCH01_V1_1_EVIDENCE_MANIFEST","calibration_gpu_timing":{"compute_seconds":149.54003936197842,"device_to_host_seconds":0.7904343790141866,"host_to_device_seconds":0.1820402479788754,"max_gpu_memory_bytes":114601472},"cuda":"12.8","data_hashes":{"adversarial_csv":"20036fe14201f5e68f932edeb17189e54e875a13f948057cbe06900cadd7dc1c","calibration_csv":"7fb56bb05b0edc2348407765cd693efadf334307c25877dfb52acfe4221e5d00","holdout_csv":"2eef64de206e9ec908b8d7042f58cd4fcf22bfa0b99b856d27246e2432281904","request_json":"7ca0c8578dd81c3ae60898d83e6e73f9bf8889a39a873b451c7cdaf036cbaf24"},"gpu":"NVIDIA GB10","image_env":"artifex/gpu-feature-v1:v1_1","implementation_hashes":{"gpu_batch01_v1_1_runner.py":"c105954199d753aa4f404ca595b1d106e3f2647aad3725fad8b9274b61a881f7","gpu_feature_engine_v1.py":"1d5033b3ae078f9cd5ebe7e51f6ea4bd82d39737229425137370388fb6a205a9","gpu_feature_parity_contract_v1_1.py":"ad78e5f6200393540a45293535937ebfb2769a3474e6f207b867045c42ce477a"},"platform":"Linux-6.17.0-1026-nvidia-aarch64-with-glibc2.35","python":"3.10.12 (main, Jun 22 2026, 18:55:27) [GCC 11.4.0]","role_semantics":{"reason":"Batch01 request records contain no strategy-role assignment; provide --role-usage-artifact generated from historical lineage.","status":"not_reconstructable"},"schema_version":"1.1","source_hashes":{"historical_band_channel.py":"1f019058049ef4717edce14e8a9af11e74ec1f1ab2d12d26a938449017930d91"},"torch":"2.7.1+cu128"}
|
||||||
372
gpu_batch01_v1_1_runner.py
Normal file
372
gpu_batch01_v1_1_runner.py
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
"""Reproducible GPU Batch01 V1.1 evidence runner for the ARM64 CUDA image.
|
||||||
|
|
||||||
|
The runner deliberately evaluates the CPU historical port and the CUDA engine
|
||||||
|
separately. CUDA unavailability is an error: GPU results are never replaced by
|
||||||
|
CPU results.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from gpu_feature_engine_v1 import evaluate_batch, psar_trace, supertrend_trace
|
||||||
|
from gpu_feature_parity_contract_v1_1 import (
|
||||||
|
calibrate,
|
||||||
|
canonical_bytes,
|
||||||
|
compare_stateful_trace,
|
||||||
|
corpus_manifest,
|
||||||
|
cpu_psar_trace,
|
||||||
|
cpu_supertrend_trace,
|
||||||
|
deterministic_adversarial_ohlcv,
|
||||||
|
freeze_contract,
|
||||||
|
historical_cpu_oracle,
|
||||||
|
nan_gap_semantics_manifest,
|
||||||
|
validate_frozen_contract,
|
||||||
|
)
|
||||||
|
|
||||||
|
ARTIFACT_PREFIX = "gpu_batch01_v1_1"
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_file(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(block)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path: Path, payload: Mapping[str, Any]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(canonical_bytes(dict(payload)) + b"\n")
|
||||||
|
|
||||||
|
|
||||||
|
def read_ohlcv(path: Path) -> dict[str, np.ndarray]:
|
||||||
|
with path.open(newline="", encoding="utf-8") as handle:
|
||||||
|
rows = list(csv.DictReader(handle))
|
||||||
|
required = ("close", "high", "low", "volume")
|
||||||
|
if not rows or not set(required).issubset(rows[0]):
|
||||||
|
raise ValueError("CSV must contain non-empty close, high, low, volume columns")
|
||||||
|
values = {
|
||||||
|
name: np.asarray([float(row[name]) for row in rows], dtype=np.float64) for name in required
|
||||||
|
}
|
||||||
|
if not all(np.isfinite(value).all() for value in values.values()):
|
||||||
|
raise ValueError(
|
||||||
|
"historical NaN/gap semantics are undefined; non-finite OHLCV inputs are rejected"
|
||||||
|
)
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def write_ohlcv_csv(path: Path, ohlcv: Mapping[str, np.ndarray]) -> None:
|
||||||
|
with path.open("w", newline="", encoding="utf-8") as handle:
|
||||||
|
writer = csv.DictWriter(handle, fieldnames=["close", "high", "low", "volume"])
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(
|
||||||
|
{name: float(ohlcv[name][index]) for name in writer.fieldnames}
|
||||||
|
for index in range(len(ohlcv["close"]))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gpu_outputs(
|
||||||
|
request: Mapping[str, Any], ohlcv: Mapping[str, np.ndarray], device: torch.device
|
||||||
|
) -> tuple[dict[str, np.ndarray], dict[str, float]]:
|
||||||
|
torch.cuda.reset_peak_memory_stats(device)
|
||||||
|
transfer_start = time.perf_counter()
|
||||||
|
tensors = tuple(
|
||||||
|
torch.as_tensor(ohlcv[name], dtype=torch.float64, device=device)
|
||||||
|
for name in ("close", "high", "low", "volume")
|
||||||
|
)
|
||||||
|
torch.cuda.synchronize(device)
|
||||||
|
transfer_seconds = time.perf_counter() - transfer_start
|
||||||
|
compute_start = time.perf_counter()
|
||||||
|
outputs = evaluate_batch(dict(request), *tensors)
|
||||||
|
torch.cuda.synchronize(device)
|
||||||
|
compute_seconds = time.perf_counter() - compute_start
|
||||||
|
host_start = time.perf_counter()
|
||||||
|
result = {key: value.detach().cpu().numpy() for key, value in outputs.items()}
|
||||||
|
torch.cuda.synchronize(device)
|
||||||
|
return result, {
|
||||||
|
"host_to_device_seconds": transfer_seconds,
|
||||||
|
"compute_seconds": compute_seconds,
|
||||||
|
"device_to_host_seconds": time.perf_counter() - host_start,
|
||||||
|
"max_gpu_memory_bytes": int(torch.cuda.max_memory_allocated(device)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def traces(
|
||||||
|
requests: list[Mapping[str, Any]], ohlcv: Mapping[str, np.ndarray], device: torch.device
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
close, high, low = (ohlcv[name] for name in ("close", "high", "low"))
|
||||||
|
gpu_values = tuple(
|
||||||
|
torch.as_tensor(value, dtype=torch.float64, device=device) for value in (close, high, low)
|
||||||
|
)
|
||||||
|
records = []
|
||||||
|
for item in requests:
|
||||||
|
indicator = int(item["indicator_id"])
|
||||||
|
if indicator == 19:
|
||||||
|
expected = cpu_supertrend_trace(
|
||||||
|
close, high, low, int(item["period"]), float(item["p1"])
|
||||||
|
)
|
||||||
|
actual = supertrend_trace(*gpu_values, int(item["period"]), float(item["p1"]))
|
||||||
|
trace_type = "supertrend"
|
||||||
|
elif indicator == 28:
|
||||||
|
expected = cpu_psar_trace(close, high, low, float(item["p1"]))
|
||||||
|
actual = psar_trace(*gpu_values, float(item["p1"]))
|
||||||
|
trace_type = "psar"
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
torch.cuda.synchronize(device)
|
||||||
|
compared = compare_stateful_trace(
|
||||||
|
expected,
|
||||||
|
{key: value.detach().cpu().numpy() for key, value in actual.items()},
|
||||||
|
trace_type=trace_type,
|
||||||
|
)
|
||||||
|
records.append({"request_id": str(item["request_id"]), "family": trace_type, **compared})
|
||||||
|
return {
|
||||||
|
"artifact": "GPU_BATCH01_STATEFUL_TRACE_COMPARISON_V1_1",
|
||||||
|
"passed": all(item["exact"] for item in records),
|
||||||
|
"records": records,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def performance(
|
||||||
|
request: Mapping[str, Any], ohlcv: Mapping[str, np.ndarray], device: torch.device
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
# Each family is measured through the same CUDA evaluate_batch entry point.
|
||||||
|
families: dict[str, list[Mapping[str, Any]]] = {}
|
||||||
|
names = {
|
||||||
|
17: "bollinger",
|
||||||
|
18: "bollinger",
|
||||||
|
19: "supertrend",
|
||||||
|
20: "donchian",
|
||||||
|
21: "donchian",
|
||||||
|
23: "keltner",
|
||||||
|
24: "keltner",
|
||||||
|
28: "psar",
|
||||||
|
}
|
||||||
|
for item in request["requests"]:
|
||||||
|
families.setdefault(names[int(item["indicator_id"])], []).append(item)
|
||||||
|
results = []
|
||||||
|
for family, items in sorted(families.items()):
|
||||||
|
family_request = {"requests": items}
|
||||||
|
_, cold = gpu_outputs(family_request, ohlcv, device)
|
||||||
|
_, warm = gpu_outputs(family_request, ohlcv, device)
|
||||||
|
values = len(ohlcv["close"]) * len(items)
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"family": family,
|
||||||
|
"variants": len(items),
|
||||||
|
"values": values,
|
||||||
|
"cold": cold,
|
||||||
|
"warm": warm,
|
||||||
|
"cold_values_per_second": values / cold["compute_seconds"]
|
||||||
|
if cold["compute_seconds"]
|
||||||
|
else None,
|
||||||
|
"warm_values_per_second": values / warm["compute_seconds"]
|
||||||
|
if warm["compute_seconds"]
|
||||||
|
else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"artifact": "GPU_BATCH01_PERFORMANCE_METRICS_V1_1",
|
||||||
|
"measurement": (
|
||||||
|
"CUDA synchronize before each interval; cold is first family invocation, warm is second"
|
||||||
|
),
|
||||||
|
"families": results,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def role_semantics(path: Path | None) -> dict[str, Any]:
|
||||||
|
if path is None:
|
||||||
|
return {
|
||||||
|
"status": "not_reconstructable",
|
||||||
|
"reason": "Batch01 request records contain no strategy-role assignment; provide "
|
||||||
|
"--role-usage-artifact generated from historical lineage.",
|
||||||
|
}
|
||||||
|
if not path.is_file():
|
||||||
|
raise ValueError(f"role usage artifact does not exist: {path}")
|
||||||
|
return {"status": "source_artifact_recorded", "path": str(path), "sha256": sha256_file(path)}
|
||||||
|
|
||||||
|
|
||||||
|
def implementation_hashes() -> dict[str, str]:
|
||||||
|
root = Path(__file__).resolve().parent
|
||||||
|
return {
|
||||||
|
path.name: sha256_file(path)
|
||||||
|
for path in (
|
||||||
|
root / "gpu_batch01_v1_1_runner.py",
|
||||||
|
root / "gpu_feature_engine_v1.py",
|
||||||
|
root / "gpu_feature_parity_contract_v1_1.py",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--calibration-csv", type=Path, required=True)
|
||||||
|
parser.add_argument("--holdout-csv", type=Path, required=True)
|
||||||
|
parser.add_argument("--request-json", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--adversarial-length", type=int, default=256)
|
||||||
|
parser.add_argument("--seed", type=int, default=0)
|
||||||
|
parser.add_argument("--role-usage-artifact", type=Path)
|
||||||
|
parser.add_argument("--image-env", default=os.getenv("GPU_BATCH01_IMAGE_ENV"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not torch.cuda.is_available():
|
||||||
|
raise SystemExit("CUDA GPU is required; no CPU fallback is available")
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
request = json.loads(args.request_json.read_text(encoding="utf-8"))
|
||||||
|
requests = request.get("requests")
|
||||||
|
if not isinstance(requests, list) or not requests:
|
||||||
|
raise ValueError("request JSON must contain a non-empty requests list")
|
||||||
|
device = torch.device("cuda")
|
||||||
|
|
||||||
|
# Calibration is the only stage allowed to derive and freeze acceptance limits.
|
||||||
|
calibration_ohlcv = read_ohlcv(args.calibration_csv)
|
||||||
|
calibration_cpu = historical_cpu_oracle(args.calibration_csv, requests)
|
||||||
|
calibration_gpu, calibration_timing = gpu_outputs(request, calibration_ohlcv, device)
|
||||||
|
calibration_manifest = corpus_manifest(
|
||||||
|
{"calibration": calibration_ohlcv},
|
||||||
|
{
|
||||||
|
"source_csv": str(args.calibration_csv),
|
||||||
|
"source_sha256": sha256_file(args.calibration_csv),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
calibration_evidence = calibrate(
|
||||||
|
calibration_gpu,
|
||||||
|
calibration_cpu["outputs"],
|
||||||
|
requests,
|
||||||
|
calibration_ohlcv["close"],
|
||||||
|
calibration_manifest,
|
||||||
|
)
|
||||||
|
contract = freeze_contract(calibration_evidence)
|
||||||
|
write_json(args.output_dir / "gpu_feature_parity_calibration_v1_1.json", calibration_evidence)
|
||||||
|
write_json(args.output_dir / "gpu_feature_parity_contract_v1_1.json", contract)
|
||||||
|
np.savez_compressed(
|
||||||
|
args.output_dir / f"{ARTIFACT_PREFIX}_calibration_cpu_oracle.npz",
|
||||||
|
**calibration_cpu["outputs"],
|
||||||
|
)
|
||||||
|
np.savez_compressed(
|
||||||
|
args.output_dir / f"{ARTIFACT_PREFIX}_calibration_gpu_outputs.npz", **calibration_gpu
|
||||||
|
)
|
||||||
|
|
||||||
|
adversarial = deterministic_adversarial_ohlcv(length=args.adversarial_length, seed=args.seed)
|
||||||
|
adversarial_csv = args.output_dir / f"{ARTIFACT_PREFIX}_adversarial.csv"
|
||||||
|
write_ohlcv_csv(adversarial_csv, adversarial)
|
||||||
|
corpora = {
|
||||||
|
"calibration": calibration_ohlcv,
|
||||||
|
"holdout": read_ohlcv(args.holdout_csv),
|
||||||
|
"adversarial": adversarial,
|
||||||
|
}
|
||||||
|
corpus = corpus_manifest(
|
||||||
|
corpora,
|
||||||
|
{
|
||||||
|
"adversarial_generator": "deterministic_adversarial_ohlcv",
|
||||||
|
"seed": args.seed,
|
||||||
|
"role_semantics": role_semantics(args.role_usage_artifact),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(args.output_dir / "gpu_feature_parity_corpus_manifest_v1_1.json", corpus)
|
||||||
|
write_json(
|
||||||
|
args.output_dir / "gpu_feature_parity_nan_gap_semantics_v1_1.json",
|
||||||
|
nan_gap_semantics_manifest(),
|
||||||
|
)
|
||||||
|
|
||||||
|
validations, trace_reports = [], []
|
||||||
|
for name in ("calibration", "holdout", "adversarial"):
|
||||||
|
if name == "calibration":
|
||||||
|
csv_path, cpu, gpu, timing = (
|
||||||
|
args.calibration_csv,
|
||||||
|
calibration_cpu,
|
||||||
|
calibration_gpu,
|
||||||
|
calibration_timing,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
csv_path = args.holdout_csv if name == "holdout" else adversarial_csv
|
||||||
|
cpu = historical_cpu_oracle(csv_path, requests)
|
||||||
|
gpu, timing = gpu_outputs(request, corpora[name], device)
|
||||||
|
validation = validate_frozen_contract(contract, gpu, cpu["outputs"], corpora[name]["close"])
|
||||||
|
validation.update(
|
||||||
|
{
|
||||||
|
"corpus": name,
|
||||||
|
"cpu_oracle": {key: value for key, value in cpu.items() if key != "outputs"},
|
||||||
|
"gpu_timing": timing,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
validations.append(validation)
|
||||||
|
trace_reports.append({"corpus": name, **traces(requests, corpora[name], device)})
|
||||||
|
if name != "calibration":
|
||||||
|
np.savez_compressed(
|
||||||
|
args.output_dir / f"{ARTIFACT_PREFIX}_{name}_cpu_oracle.npz", **cpu["outputs"]
|
||||||
|
)
|
||||||
|
np.savez_compressed(
|
||||||
|
args.output_dir / f"{ARTIFACT_PREFIX}_{name}_gpu_outputs.npz", **gpu
|
||||||
|
)
|
||||||
|
validation_payload = {
|
||||||
|
"artifact": "GPU_FEATURE_PARITY_VALIDATION_REPORT_V1_1",
|
||||||
|
"contract_sha256": hashlib.sha256(canonical_bytes(contract)).hexdigest(),
|
||||||
|
"passed": all(value["passed"] for value in validations),
|
||||||
|
"corpora": validations,
|
||||||
|
}
|
||||||
|
trace_payload = {
|
||||||
|
"artifact": "GPU_BATCH01_STATEFUL_TRACE_REPORT_V1_1",
|
||||||
|
"passed": all(value["passed"] for value in trace_reports),
|
||||||
|
"corpora": trace_reports,
|
||||||
|
}
|
||||||
|
write_json(args.output_dir / "gpu_feature_parity_validation_v1_1.json", validation_payload)
|
||||||
|
write_json(args.output_dir / "gpu_batch01_stateful_trace_comparison_v1_1.json", trace_payload)
|
||||||
|
write_json(
|
||||||
|
args.output_dir / "gpu_batch01_performance_metrics_v1_1.json",
|
||||||
|
performance(request, corpora["holdout"], device),
|
||||||
|
)
|
||||||
|
root = Path(__file__).resolve().parent
|
||||||
|
manifest = {
|
||||||
|
"artifact": "GPU_BATCH01_V1_1_EVIDENCE_MANIFEST",
|
||||||
|
"schema_version": "1.1",
|
||||||
|
"python": sys.version,
|
||||||
|
"platform": platform.platform(),
|
||||||
|
"torch": torch.__version__,
|
||||||
|
"cuda": torch.version.cuda,
|
||||||
|
"gpu": torch.cuda.get_device_properties(device).name,
|
||||||
|
"image_env": args.image_env,
|
||||||
|
"source_hashes": {
|
||||||
|
"historical_band_channel.py": sha256_file(
|
||||||
|
root / "control_plane/trading_studio/indicators/historical_band_channel.py"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"implementation_hashes": implementation_hashes(),
|
||||||
|
"data_hashes": {
|
||||||
|
"request_json": sha256_file(args.request_json),
|
||||||
|
"calibration_csv": sha256_file(args.calibration_csv),
|
||||||
|
"holdout_csv": sha256_file(args.holdout_csv),
|
||||||
|
"adversarial_csv": sha256_file(adversarial_csv),
|
||||||
|
},
|
||||||
|
"calibration_gpu_timing": calibration_timing,
|
||||||
|
"role_semantics": role_semantics(args.role_usage_artifact),
|
||||||
|
}
|
||||||
|
write_json(args.output_dir / "gpu_batch01_v1_1_evidence_manifest.json", manifest)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"output_dir": str(args.output_dir),
|
||||||
|
"validation_passed": validation_payload["passed"],
|
||||||
|
"trace_passed": trace_payload["passed"],
|
||||||
|
},
|
||||||
|
allow_nan=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
gpu_batch01_v1_2_evidence_manifest.json
Normal file
1
gpu_batch01_v1_2_evidence_manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"artifact":"GPU_BATCH01_V1_2_EVIDENCE_MANIFEST","artifacts":["gpu_feature_parity_calibration_v1_2.json","gpu_feature_parity_contract_v1_2.json","gpu_feature_parity_contract_v1_2.md","gpu_feature_parity_validation_calibration_v1_2.json","gpu_feature_parity_validation_holdout_v1_2.json","gpu_feature_parity_validation_adversarial_v1_2.json","gpu_feature_parity_role_surface_v1_2.json","gpu_feature_parity_review_template_v1_2.json"],"atr_limits":{"mae":0.0,"max_absolute_error":0.0},"contract_sha256":"8f8ee3459b54d7674b5f946c8030ca21ad5b2b2691b0446f33616da3917df47f","feature_count":97,"schema_version":"1.2","status":"evidence_generated_not_validated","validation_passed":false}
|
||||||
203
gpu_batch01_v1_2_runner.py
Normal file
203
gpu_batch01_v1_2_runner.py
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
"""GPU Batch01 V1.2 full-family, frozen-contract evidence runner."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from control_plane.trading_studio.indicators.historical_band_channel import evaluate_band_channel
|
||||||
|
from gpu_batch01_v1_1_runner import read_ohlcv, write_ohlcv_csv
|
||||||
|
from gpu_feature_engine_v1 import evaluate_batch as evaluate_v1_batch
|
||||||
|
from gpu_feature_engine_v1_2 import supertrend_trace
|
||||||
|
from gpu_feature_parity_contract_v1_1 import deterministic_adversarial_ohlcv
|
||||||
|
from gpu_feature_parity_contract_v1_2 import (
|
||||||
|
CALIBRATION_ARTIFACT,
|
||||||
|
calibrate_output,
|
||||||
|
calibrate_supertrend,
|
||||||
|
canonical_bytes,
|
||||||
|
freeze_contract,
|
||||||
|
review_template,
|
||||||
|
role_surface,
|
||||||
|
validate_all_frozen_contract,
|
||||||
|
)
|
||||||
|
|
||||||
|
ARTIFACT_PREFIX = "gpu_batch01_v1_2"
|
||||||
|
ATR_LIMIT_KEYS = ("max_absolute_error", "mae")
|
||||||
|
DEFAULT_ATR_LIMITS = {"max_absolute_error": 1.5e-14, "mae": 1.5e-14}
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path: Path, payload: Mapping[str, Any]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(canonical_bytes(dict(payload)) + b"\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256_file(path: Path) -> str:
|
||||||
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _atr_limits(atr_limits: Mapping[str, float] | None) -> dict[str, float]:
|
||||||
|
limits = dict(DEFAULT_ATR_LIMITS if atr_limits is None else atr_limits)
|
||||||
|
if set(limits) != set(ATR_LIMIT_KEYS) or any(not np.isfinite(value) or value < 0 for value in limits.values()):
|
||||||
|
raise ValueError("ATR limits must contain finite, non-negative max_absolute_error and mae")
|
||||||
|
return limits
|
||||||
|
|
||||||
|
|
||||||
|
def _cpu_outputs(requests: list[Mapping[str, Any]], ohlcv: Mapping[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||||
|
return {
|
||||||
|
str(item["request_id"]): evaluate_band_channel(
|
||||||
|
int(item["indicator_id"]), ohlcv["close"], ohlcv["high"], ohlcv["low"], ohlcv["volume"], int(item["period"]), float(item["p1"])
|
||||||
|
)
|
||||||
|
for item in requests
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _gpu_outputs(requests: list[Mapping[str, Any]], ohlcv: Mapping[str, np.ndarray], device: torch.device) -> tuple[dict[str, np.ndarray], dict[str, tuple[dict[str, np.ndarray], dict[str, np.ndarray]]]]:
|
||||||
|
tensors = tuple(torch.as_tensor(ohlcv[name], dtype=torch.float64, device=device) for name in ("close", "high", "low", "volume"))
|
||||||
|
other = [item for item in requests if int(item["indicator_id"]) != 19]
|
||||||
|
outputs = evaluate_v1_batch({"requests": other}, *tensors) if other else {}
|
||||||
|
traces = {}
|
||||||
|
for item in requests:
|
||||||
|
if int(item["indicator_id"]) != 19:
|
||||||
|
continue
|
||||||
|
key = str(item["request_id"])
|
||||||
|
actual = supertrend_trace(*tensors[:3], int(item["period"]), float(item["p1"]))
|
||||||
|
# The CPU trace is imported lazily to keep historical output generation explicit.
|
||||||
|
from gpu_feature_parity_contract_v1_2 import cpu_supertrend_trace
|
||||||
|
expected = cpu_supertrend_trace(ohlcv["close"], ohlcv["high"], ohlcv["low"], int(item["period"]), float(item["p1"]))
|
||||||
|
traces[key] = (expected, {name: value.detach().cpu().numpy() for name, value in actual.items()})
|
||||||
|
outputs[key] = actual["output"]
|
||||||
|
torch.cuda.synchronize(device)
|
||||||
|
return {key: value.detach().cpu().numpy() for key, value in outputs.items()}, traces
|
||||||
|
|
||||||
|
|
||||||
|
def _gpu_workload(requests: list[Mapping[str, Any]], ohlcv: Mapping[str, np.ndarray], device: torch.device) -> None:
|
||||||
|
tensors = tuple(torch.as_tensor(ohlcv[name], dtype=torch.float64, device=device) for name in ("close", "high", "low", "volume"))
|
||||||
|
other = [item for item in requests if int(item["indicator_id"]) != 19]
|
||||||
|
if other:
|
||||||
|
evaluate_v1_batch({"requests": other}, *tensors)
|
||||||
|
for item in requests:
|
||||||
|
if int(item["indicator_id"]) == 19:
|
||||||
|
supertrend_trace(*tensors[:3], int(item["period"]), float(item["p1"]))
|
||||||
|
|
||||||
|
|
||||||
|
def performance(requests: list[Mapping[str, Any]], ohlcv: Mapping[str, np.ndarray], device: torch.device) -> dict[str, Any]:
|
||||||
|
"""Measure V1.2 GPU evaluation with synchronized intervals."""
|
||||||
|
families: dict[str, list[Mapping[str, Any]]] = {}
|
||||||
|
for item in requests:
|
||||||
|
family = {17: "bollinger", 18: "bollinger", 19: "supertrend", 20: "donchian", 21: "donchian", 23: "keltner", 24: "keltner", 28: "psar"}[int(item["indicator_id"])]
|
||||||
|
families.setdefault(family, []).append(item)
|
||||||
|
results = []
|
||||||
|
for family, items in sorted(families.items()):
|
||||||
|
timings = []
|
||||||
|
for _ in range(2):
|
||||||
|
torch.cuda.synchronize(device)
|
||||||
|
started = time.perf_counter()
|
||||||
|
_gpu_workload(items, ohlcv, device)
|
||||||
|
torch.cuda.synchronize(device)
|
||||||
|
timings.append(time.perf_counter() - started)
|
||||||
|
values = len(ohlcv["close"]) * len(items)
|
||||||
|
results.append({"family": family, "variants": len(items), "values": values, "cold_seconds": timings[0], "warm_seconds": timings[1], "cold_values_per_second": values / timings[0] if timings[0] else None, "warm_values_per_second": values / timings[1] if timings[1] else None})
|
||||||
|
return {"artifact": "GPU_BATCH01_PERFORMANCE_METRICS_V1_2", "schema_version": "1.2", "measurement": "CUDA synchronize before and after each interval; cold is the first family invocation and warm is the second", "families": results}
|
||||||
|
|
||||||
|
|
||||||
|
def _markdown(contract: Mapping[str, Any], roles: Mapping[str, Any]) -> str:
|
||||||
|
counts: dict[str, int] = {}
|
||||||
|
for limit in contract["feature_limits"].values():
|
||||||
|
counts[limit["family"]] = counts.get(limit["family"], 0) + 1
|
||||||
|
return "\n".join((
|
||||||
|
"# GPU Batch01 V1.2 Frozen Validation Contract", "",
|
||||||
|
"All 97 observed Batch01 outputs are evaluated against `historical_band_channel`.",
|
||||||
|
"", "## Evidence Classes", "",
|
||||||
|
"- Donchian and PSAR outputs: exact.",
|
||||||
|
"- Bollinger and Keltner outputs: bounded by calibration-only limits.",
|
||||||
|
"- Supertrend final output, bands, direction, transitions, and branch predicates: exact; ATR uses supplied fixed bounds.",
|
||||||
|
"", "## Frozen Coverage", "",
|
||||||
|
*[f"- `{family}`: {count}" for family, count in sorted(counts.items())],
|
||||||
|
"", "## Role Status", "",
|
||||||
|
"Unrecovered strategy roles are `UNVERIFIABLE_FROM_RECOVERED_SOURCE`; they are recorded but are not parity blockers.",
|
||||||
|
f"Role records: {len(roles['records'])}.", "",
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def run(calibration_csv: Path, holdout_csv: Path, request_json: Path, output_dir: Path, *, adversarial_length: int = 256, seed: int = 0, atr_limits: Mapping[str, float] | None = None, device: torch.device | None = None) -> dict[str, Any]:
|
||||||
|
if device is None:
|
||||||
|
if not torch.cuda.is_available():
|
||||||
|
raise RuntimeError("CUDA GPU is required; no CPU fallback is available")
|
||||||
|
device = torch.device("cuda")
|
||||||
|
atr_limits = _atr_limits(atr_limits)
|
||||||
|
request = json.loads(request_json.read_text(encoding="utf-8"))
|
||||||
|
requests = request.get("requests")
|
||||||
|
if not isinstance(requests, list) or len(requests) != 97:
|
||||||
|
raise ValueError("V1.2 requires the complete 97-request Batch01 manifest")
|
||||||
|
if len({str(item["request_id"]) for item in requests}) != 97:
|
||||||
|
raise ValueError("Batch01 request IDs must be unique")
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
calibration, holdout = read_ohlcv(calibration_csv), read_ohlcv(holdout_csv)
|
||||||
|
adversarial = deterministic_adversarial_ohlcv(length=adversarial_length, seed=seed)
|
||||||
|
write_ohlcv_csv(output_dir / f"{ARTIFACT_PREFIX}_adversarial.csv", adversarial)
|
||||||
|
roles = role_surface(requests)
|
||||||
|
write_json(output_dir / "gpu_feature_parity_role_surface_v1_2.json", roles)
|
||||||
|
|
||||||
|
# Calibration is the sole point at which continuous-family limits are derived.
|
||||||
|
calibration_cpu = _cpu_outputs(requests, calibration)
|
||||||
|
calibration_gpu, calibration_traces = _gpu_outputs(requests, calibration, device)
|
||||||
|
evidence = {"artifact": CALIBRATION_ARTIFACT, "schema_version": "1.2", "feature_count": 97, "records": [
|
||||||
|
calibrate_supertrend(str(item["request_id"]), *calibration_traces[str(item["request_id"])], atr_limits=atr_limits)
|
||||||
|
if int(item["indicator_id"]) == 19 else calibrate_output(str(item["request_id"]), int(item["indicator_id"]), calibration_cpu[str(item["request_id"])], calibration_gpu[str(item["request_id"])] )
|
||||||
|
for item in requests
|
||||||
|
]}
|
||||||
|
contract = freeze_contract(evidence) # Frozen before holdout/adversarial are evaluated.
|
||||||
|
write_json(output_dir / "gpu_feature_parity_calibration_v1_2.json", evidence)
|
||||||
|
write_json(output_dir / "gpu_feature_parity_contract_v1_2.json", contract)
|
||||||
|
(output_dir / "gpu_feature_parity_contract_v1_2.md").write_text(_markdown(contract, roles), encoding="utf-8")
|
||||||
|
np.savez_compressed(output_dir / f"{ARTIFACT_PREFIX}_calibration_cpu_oracle.npz", **calibration_cpu)
|
||||||
|
np.savez_compressed(output_dir / f"{ARTIFACT_PREFIX}_calibration_gpu_outputs.npz", **calibration_gpu)
|
||||||
|
|
||||||
|
reports = {}
|
||||||
|
for name, corpus in (("calibration", calibration), ("holdout", holdout), ("adversarial", adversarial)):
|
||||||
|
expected = calibration_cpu if name == "calibration" else _cpu_outputs(requests, corpus)
|
||||||
|
actual, traces = (calibration_gpu, calibration_traces) if name == "calibration" else _gpu_outputs(requests, corpus, device)
|
||||||
|
report = validate_all_frozen_contract(contract, expected, actual, traces)
|
||||||
|
report.update({"corpus": name, "feature_count": 97})
|
||||||
|
reports[name] = report
|
||||||
|
write_json(output_dir / f"gpu_feature_parity_validation_{name}_v1_2.json", report)
|
||||||
|
if name != "calibration":
|
||||||
|
np.savez_compressed(output_dir / f"{ARTIFACT_PREFIX}_{name}_cpu_oracle.npz", **expected)
|
||||||
|
np.savez_compressed(output_dir / f"{ARTIFACT_PREFIX}_{name}_gpu_outputs.npz", **actual)
|
||||||
|
review = review_template()
|
||||||
|
review["required_review"].extend(["all 97 Batch01 requests are represented", "role status remains UNVERIFIABLE_FROM_RECOVERED_SOURCE where lineage is unrecovered"])
|
||||||
|
write_json(output_dir / "gpu_feature_parity_review_template_v1_2.json", review)
|
||||||
|
write_json(output_dir / "gpu_batch01_performance_metrics_v1_2.json", performance(requests, holdout, device))
|
||||||
|
provenance = {"artifact": "GPU_BATCH01_RUNTIME_PROVENANCE_V1_2", "schema_version": "1.2", "python": sys.version, "platform": platform.platform(), "torch": torch.__version__, "cuda": torch.version.cuda, "gpu": torch.cuda.get_device_properties(device).name, "image_env": os.getenv("GPU_BATCH01_IMAGE_ENV"), "source_hashes": {path.name: _sha256_file(path) for path in (Path(__file__), Path(__file__).with_name("gpu_batch01_v1_1_runner.py"), Path(__file__).with_name("gpu_feature_engine_v1.py"), Path(__file__).with_name("gpu_feature_engine_v1_2.py"), Path(__file__).with_name("gpu_feature_parity_contract_v1_1.py"), Path(__file__).with_name("gpu_feature_parity_contract_v1_2.py"), Path(__file__).parent / "control_plane/trading_studio/indicators/historical_band_channel.py")}, "data_hashes": {"request_json": _sha256_file(request_json), "calibration_csv": _sha256_file(calibration_csv), "holdout_csv": _sha256_file(holdout_csv), "adversarial_csv": _sha256_file(output_dir / f"{ARTIFACT_PREFIX}_adversarial.csv")}, "adversarial": {"generator": "deterministic_adversarial_ohlcv", "length": adversarial_length, "seed": seed}}
|
||||||
|
write_json(output_dir / "gpu_batch01_runtime_provenance_v1_2.json", provenance)
|
||||||
|
artifacts = ["gpu_feature_parity_calibration_v1_2.json", "gpu_feature_parity_contract_v1_2.json", "gpu_feature_parity_contract_v1_2.md", "gpu_feature_parity_validation_calibration_v1_2.json", "gpu_feature_parity_validation_holdout_v1_2.json", "gpu_feature_parity_validation_adversarial_v1_2.json", "gpu_feature_parity_role_surface_v1_2.json", "gpu_feature_parity_review_template_v1_2.json", "gpu_batch01_performance_metrics_v1_2.json", "gpu_batch01_runtime_provenance_v1_2.json"]
|
||||||
|
manifest = {"artifact": "GPU_BATCH01_V1_2_EVIDENCE_MANIFEST", "schema_version": "1.2", "status": "evidence_generated_not_validated", "feature_count": 97, "contract_sha256": hashlib.sha256(canonical_bytes(contract)).hexdigest(), "atr_limits": atr_limits, "validation_passed": all(report["passed"] for report in reports.values()), "artifacts": artifacts}
|
||||||
|
write_json(output_dir / "gpu_batch01_v1_2_evidence_manifest.json", manifest)
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--calibration-csv", type=Path, required=True)
|
||||||
|
parser.add_argument("--holdout-csv", type=Path, required=True)
|
||||||
|
parser.add_argument("--request-json", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--adversarial-length", type=int, default=256)
|
||||||
|
parser.add_argument("--seed", type=int, default=0)
|
||||||
|
parser.add_argument("--supertrend-atr-max-absolute-error", type=float, default=DEFAULT_ATR_LIMITS["max_absolute_error"])
|
||||||
|
parser.add_argument("--supertrend-atr-mae", type=float, default=DEFAULT_ATR_LIMITS["mae"])
|
||||||
|
args = parser.parse_args()
|
||||||
|
print(json.dumps(run(args.calibration_csv, args.holdout_csv, args.request_json, args.output_dir, adversarial_length=args.adversarial_length, seed=args.seed, atr_limits={"max_absolute_error": args.supertrend_atr_max_absolute_error, "mae": args.supertrend_atr_mae}), allow_nan=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
430
gpu_feature_engine_v1.py
Normal file
430
gpu_feature_engine_v1.py
Normal file
|
|
@ -0,0 +1,430 @@
|
||||||
|
"""Standalone CUDA Batch01 band/channel prototype.
|
||||||
|
|
||||||
|
This module deliberately has no Artifex imports. It consumes frozen oracle
|
||||||
|
artifacts and OHLCV CSV files mounted read-only, writes only its output cache,
|
||||||
|
and records measured parity rather than asserting it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(block)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_ohlcv(path: Path, device: torch.device) -> tuple[torch.Tensor, ...]:
|
||||||
|
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 close, high, low, and volume columns")
|
||||||
|
return tuple(
|
||||||
|
torch.as_tensor([float(row[name]) for row in rows], dtype=torch.float64, device=device)
|
||||||
|
for name in ("close", "high", "low", "volume")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rolling_windows(values: torch.Tensor, period: int) -> torch.Tensor:
|
||||||
|
return values.unfold(0, period, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _empty(values: torch.Tensor) -> torch.Tensor:
|
||||||
|
return torch.full_like(values, float("nan"))
|
||||||
|
|
||||||
|
|
||||||
|
def _sma(values: torch.Tensor, period: int) -> torch.Tensor:
|
||||||
|
out = _empty(values)
|
||||||
|
if values.numel() < period:
|
||||||
|
return out
|
||||||
|
out[period - 1 :] = _rolling_windows(values, period).mean(dim=1)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _ema(values: torch.Tensor, period: int) -> torch.Tensor:
|
||||||
|
out = _empty(values)
|
||||||
|
if values.numel() < period:
|
||||||
|
return out
|
||||||
|
alpha = 2.0 / (period + 1)
|
||||||
|
out[period - 1] = values[:period].sum() / period
|
||||||
|
for index in range(period, values.numel()):
|
||||||
|
out[index] = alpha * values[index] + (1.0 - alpha) * out[index - 1]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _rma(values: torch.Tensor, period: int) -> torch.Tensor:
|
||||||
|
out = _empty(values)
|
||||||
|
valid = torch.nonzero(~torch.isnan(values), as_tuple=False).flatten()
|
||||||
|
if valid.numel() < period:
|
||||||
|
return out
|
||||||
|
start = int(valid[period - 1].item())
|
||||||
|
out[start] = values[valid[:period]].sum() / period
|
||||||
|
alpha = 1.0 / period
|
||||||
|
for index in range(start + 1, values.numel()):
|
||||||
|
out[index] = out[index - 1] if torch.isnan(values[index]) else alpha * values[index] + (1.0 - alpha) * out[index - 1]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _true_range(close: torch.Tensor, high: torch.Tensor, low: torch.Tensor) -> torch.Tensor:
|
||||||
|
out = torch.empty_like(close)
|
||||||
|
out[0] = high[0] - low[0]
|
||||||
|
out[1:] = torch.stack((high[1:] - low[1:], (high[1:] - close[:-1]).abs(), (low[1:] - close[:-1]).abs())).amax(dim=0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _bollinger(close: torch.Tensor, period: int, multipliers: list[float]) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
upper, lower = _empty(close).repeat(len(multipliers), 1), _empty(close).repeat(len(multipliers), 1)
|
||||||
|
if close.numel() < period:
|
||||||
|
return upper, lower
|
||||||
|
windows = _rolling_windows(close, period)
|
||||||
|
mean = windows.mean(dim=1)
|
||||||
|
# Keep reductions on-device and vectorized; sample variance matches the oracle formula.
|
||||||
|
standard_deviation = ((windows - mean[:, None]).square().sum(dim=1) / (period - 1)).sqrt()
|
||||||
|
multiplier = torch.tensor(multipliers, dtype=torch.float64, device=close.device)
|
||||||
|
upper[:, period - 1 :] = mean[None, :] + multiplier[:, None] * standard_deviation[None, :]
|
||||||
|
lower[:, period - 1 :] = mean[None, :] - multiplier[:, None] * standard_deviation[None, :]
|
||||||
|
return upper, lower
|
||||||
|
|
||||||
|
|
||||||
|
def _donchian(values: torch.Tensor, period: int, upper: bool) -> torch.Tensor:
|
||||||
|
out = _empty(values)
|
||||||
|
if values.numel() >= period:
|
||||||
|
windows = _rolling_windows(values, period)
|
||||||
|
out[period - 1 :] = windows.amax(dim=1) if upper else windows.amin(dim=1)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _keltner(close: torch.Tensor, high: torch.Tensor, low: torch.Tensor, period: int, multipliers: list[float]) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
basis, atr = _ema(close, period), _rma(_true_range(close, high, low), period)
|
||||||
|
upper, lower = _empty(close).repeat(len(multipliers), 1), _empty(close).repeat(len(multipliers), 1)
|
||||||
|
valid = ~torch.isnan(basis) & ~torch.isnan(atr)
|
||||||
|
multiplier = torch.tensor(multipliers, dtype=torch.float64, device=close.device)
|
||||||
|
upper[:, valid] = basis[valid] + multiplier[:, None] * atr[valid]
|
||||||
|
lower[:, valid] = basis[valid] - multiplier[:, None] * atr[valid]
|
||||||
|
return upper, lower
|
||||||
|
|
||||||
|
|
||||||
|
def _supertrend(close: torch.Tensor, high: torch.Tensor, low: torch.Tensor, period: int, multipliers: list[float]) -> torch.Tensor:
|
||||||
|
count, length = len(multipliers), close.numel()
|
||||||
|
out = _empty(close).repeat(count, 1)
|
||||||
|
atr, upper, lower = _rma(_true_range(close, high, low), period), _empty(close).repeat(count), _empty(close).repeat(count)
|
||||||
|
multiplier = torch.tensor(multipliers, dtype=torch.float64, device=close.device)
|
||||||
|
direction = torch.ones(count, dtype=torch.float64, device=close.device)
|
||||||
|
for index in range(period, length):
|
||||||
|
midpoint, basic_upper, basic_lower = (high[index] + low[index]) / 2.0, None, None
|
||||||
|
basic_upper, basic_lower = midpoint + multiplier * atr[index], midpoint - multiplier * atr[index]
|
||||||
|
if index == period:
|
||||||
|
upper, lower = basic_upper, basic_lower
|
||||||
|
out[:, index] = torch.where(close[index] > lower, lower, upper)
|
||||||
|
direction = torch.where(close[index] > out[:, index], 1.0, -1.0)
|
||||||
|
continue
|
||||||
|
upper = torch.where((close[index - 1] <= upper) & ~torch.isnan(upper), torch.minimum(basic_upper, upper), basic_upper)
|
||||||
|
lower = torch.where((close[index - 1] >= lower) & ~torch.isnan(lower), torch.maximum(basic_lower, lower), basic_lower)
|
||||||
|
out[:, index] = torch.where(direction == 1.0, torch.where(close[index] >= lower, lower, upper), torch.where(close[index] <= upper, upper, lower))
|
||||||
|
direction = torch.where(close[index] > out[:, index], 1.0, -1.0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _psar(close: torch.Tensor, high: torch.Tensor, low: torch.Tensor, steps: list[float]) -> torch.Tensor:
|
||||||
|
out = _empty(close).repeat(len(steps), 1)
|
||||||
|
if close.numel() < 2:
|
||||||
|
return out
|
||||||
|
step = torch.tensor(steps, dtype=torch.float64, device=close.device)
|
||||||
|
bullish = torch.ones(len(steps), dtype=torch.bool, device=close.device)
|
||||||
|
acceleration = torch.full_like(step, 0.02)
|
||||||
|
extreme, sar = high[0].repeat(len(steps)), low[0].repeat(len(steps))
|
||||||
|
for index in range(1, close.numel()):
|
||||||
|
sar = sar + acceleration * (extreme - sar)
|
||||||
|
bull_sar = torch.minimum(sar, low[index - 1])
|
||||||
|
bear_sar = torch.maximum(sar, high[index - 1])
|
||||||
|
if index >= 2:
|
||||||
|
bull_sar, bear_sar = torch.minimum(bull_sar, low[index - 2]), torch.maximum(bear_sar, high[index - 2])
|
||||||
|
reversal_down, reversal_up = bullish & (low[index] < bull_sar), ~bullish & (high[index] > bear_sar)
|
||||||
|
candidate = torch.where(bullish, bull_sar, bear_sar)
|
||||||
|
sar = torch.where(reversal_down | reversal_up, extreme, candidate)
|
||||||
|
new_bullish = torch.where(reversal_down, False, torch.where(reversal_up, True, bullish))
|
||||||
|
new_extreme = torch.where(reversal_down, low[index], torch.where(reversal_up, high[index], extreme))
|
||||||
|
rising = new_bullish & ~reversal_up & (high[index] > extreme)
|
||||||
|
falling = ~new_bullish & ~reversal_down & (low[index] < extreme)
|
||||||
|
new_extreme = torch.where(rising, high[index], torch.where(falling, low[index], new_extreme))
|
||||||
|
acceleration = torch.where(reversal_down | reversal_up, 0.02, torch.where(rising | falling, torch.minimum(acceleration + step, torch.full_like(step, 0.2)), acceleration))
|
||||||
|
bullish, extreme = new_bullish, new_extreme
|
||||||
|
out[:, index] = sar
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def supertrend_trace(close: torch.Tensor, high: torch.Tensor, low: torch.Tensor, period: int, multiplier: float) -> dict[str, torch.Tensor]:
|
||||||
|
"""Return the GPU state-machine trace for one Supertrend variant.
|
||||||
|
|
||||||
|
The bar loop advances state, while every state operation remains a CUDA
|
||||||
|
tensor operation; this is not a CPU or per-variant fallback.
|
||||||
|
"""
|
||||||
|
output = _supertrend(close, high, low, period, [multiplier])[0]
|
||||||
|
atr = _rma(_true_range(close, high, low), period)
|
||||||
|
upper, lower = _empty(close), _empty(close)
|
||||||
|
direction, initialized = torch.zeros_like(close, dtype=torch.int8), torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
for index in range(period, close.numel()):
|
||||||
|
midpoint = (high[index] + low[index]) / 2.0
|
||||||
|
basic_upper, basic_lower = midpoint + multiplier * atr[index], midpoint - multiplier * atr[index]
|
||||||
|
if index == period:
|
||||||
|
upper[index], lower[index] = basic_upper, basic_lower
|
||||||
|
else:
|
||||||
|
upper[index] = torch.minimum(basic_upper, upper[index - 1]) if close[index - 1] <= upper[index - 1] else basic_upper
|
||||||
|
lower[index] = torch.maximum(basic_lower, lower[index - 1]) if close[index - 1] >= lower[index - 1] else basic_lower
|
||||||
|
direction[index] = torch.where(close[index] > output[index], 1, -1)
|
||||||
|
initialized[index] = True
|
||||||
|
return {"output": output, "atr": atr, "upper": upper, "lower": lower, "direction": direction, "initialized": initialized}
|
||||||
|
|
||||||
|
|
||||||
|
def psar_trace(close: torch.Tensor, high: torch.Tensor, low: torch.Tensor, step: float) -> dict[str, torch.Tensor]:
|
||||||
|
"""Return the GPU state-machine trace for one PSAR variant."""
|
||||||
|
output = _psar(close, high, low, [step])[0]
|
||||||
|
bullish = torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
extreme, acceleration = _empty(close), _empty(close)
|
||||||
|
reversal = torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
if close.numel() < 2:
|
||||||
|
return {"output": output, "bullish": bullish, "extreme": extreme, "acceleration": acceleration, "reversal": reversal}
|
||||||
|
bull, af, ep, sar = torch.tensor(True, device=close.device), torch.tensor(.02, dtype=torch.float64, device=close.device), high[0], low[0]
|
||||||
|
for index in range(1, close.numel()):
|
||||||
|
sar = sar + af * (ep - sar)
|
||||||
|
candidate = torch.minimum(sar, torch.minimum(low[index - 1], low[index - 2] if index >= 2 else low[index - 1])) if bool(bull) else torch.maximum(sar, torch.maximum(high[index - 1], high[index - 2] if index >= 2 else high[index - 1]))
|
||||||
|
reverse = (bool(bull) and bool(low[index] < candidate)) or (not bool(bull) and bool(high[index] > candidate))
|
||||||
|
if reverse:
|
||||||
|
bull, sar, ep, af = (torch.tensor(False, device=close.device), ep, low[index], torch.tensor(.02, dtype=torch.float64, device=close.device)) if bool(bull) else (torch.tensor(True, device=close.device), ep, high[index], torch.tensor(.02, dtype=torch.float64, device=close.device))
|
||||||
|
else:
|
||||||
|
sar = candidate
|
||||||
|
if (bool(bull) and bool(high[index] > ep)) or (not bool(bull) and bool(low[index] < ep)):
|
||||||
|
ep, af = (high[index], torch.minimum(af + step, torch.tensor(.2, dtype=torch.float64, device=close.device))) if bool(bull) else (low[index], torch.minimum(af + step, torch.tensor(.2, dtype=torch.float64, device=close.device)))
|
||||||
|
bullish[index], extreme[index], acceleration[index], reversal[index] = bull, ep, af, reverse
|
||||||
|
return {"output": output, "bullish": bullish, "extreme": extreme, "acceleration": acceleration, "reversal": reversal}
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_batch(request: dict[str, Any], close: torch.Tensor, high: torch.Tensor, low: torch.Tensor, volume: torch.Tensor) -> dict[str, torch.Tensor]:
|
||||||
|
del volume # Batch01 formulas do not consume volume, but preserve the input contract.
|
||||||
|
groups: dict[tuple[int, int], list[dict[str, Any]]] = {}
|
||||||
|
for item in request["requests"]:
|
||||||
|
groups.setdefault((int(item["indicator_id"]), int(item["period"])), []).append(item)
|
||||||
|
outputs: dict[str, torch.Tensor] = {}
|
||||||
|
for (indicator, period), items in groups.items():
|
||||||
|
multipliers = [float(item["p1"]) for item in items]
|
||||||
|
if indicator == 17:
|
||||||
|
batch = _bollinger(close, period, multipliers)[0]
|
||||||
|
elif indicator == 18:
|
||||||
|
batch = _bollinger(close, period, multipliers)[1]
|
||||||
|
elif indicator == 19:
|
||||||
|
batch = _supertrend(close, high, low, period, multipliers)
|
||||||
|
elif indicator == 20:
|
||||||
|
batch = _donchian(high, period, True).unsqueeze(0)
|
||||||
|
elif indicator == 21:
|
||||||
|
batch = _donchian(low, period, False).unsqueeze(0)
|
||||||
|
elif indicator == 23:
|
||||||
|
batch = _keltner(close, high, low, period, multipliers)[0]
|
||||||
|
elif indicator == 24:
|
||||||
|
batch = _keltner(close, high, low, period, multipliers)[1]
|
||||||
|
elif indicator == 28:
|
||||||
|
batch = _psar(close, high, low, multipliers)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unsupported indicator: {indicator}")
|
||||||
|
for index, item in enumerate(items):
|
||||||
|
outputs[str(item["request_id"])] = batch[index]
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_path(cache_dir: Path, data_csv: Path, request_json: Path) -> Path:
|
||||||
|
key = hashlib.sha256(f"v1:{_sha256(data_csv)}:{_sha256(request_json)}".encode()).hexdigest()[:24]
|
||||||
|
return cache_dir / f"batch01_gpu_{key}.npz"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_or_compute(args: argparse.Namespace, device: torch.device) -> tuple[dict[str, np.ndarray], bool, float]:
|
||||||
|
cache = _cache_path(args.cache_dir, args.data_csv, args.request_json)
|
||||||
|
request = json.loads(args.request_json.read_text(encoding="utf-8"))
|
||||||
|
expected_ids = {item["request_id"] for item in request["requests"]}
|
||||||
|
if cache.exists():
|
||||||
|
with np.load(cache) as saved:
|
||||||
|
if set(saved.files) == expected_ids:
|
||||||
|
return {key: saved[key] for key in saved.files}, True, 0.0
|
||||||
|
start = time.perf_counter()
|
||||||
|
outputs = evaluate_batch(request, *_load_ohlcv(args.data_csv, device))
|
||||||
|
torch.cuda.synchronize(device)
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
host = {key: value.detach().cpu().numpy() for key, value in outputs.items()}
|
||||||
|
args.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
np.savez_compressed(cache, **host)
|
||||||
|
return host, False, elapsed
|
||||||
|
|
||||||
|
|
||||||
|
def smoke(device: torch.device) -> dict[str, Any]:
|
||||||
|
values = torch.arange(1024, dtype=torch.float64, device=device)
|
||||||
|
result = (values.square().sum() / values.numel()).item()
|
||||||
|
torch.cuda.synchronize(device)
|
||||||
|
return {"passed": bool(np.isfinite(result)), "device": str(device), "torch": torch.__version__, "cuda": torch.version.cuda, "value": result}
|
||||||
|
|
||||||
|
|
||||||
|
def _family(indicator_id: int) -> str:
|
||||||
|
return {17: "bollinger", 18: "bollinger", 19: "supertrend", 20: "donchian", 21: "donchian", 23: "keltner", 24: "keltner", 28: "psar"}[indicator_id]
|
||||||
|
|
||||||
|
|
||||||
|
def _ordered_bits(values: np.ndarray) -> np.ndarray:
|
||||||
|
bits = values.view(np.uint64)
|
||||||
|
return np.where(bits >> 63 != 0, ~bits, bits | np.uint64(1 << 63))
|
||||||
|
|
||||||
|
|
||||||
|
def _numeric_measurement(actual: np.ndarray, expected: np.ndarray) -> dict[str, Any]:
|
||||||
|
finite = np.isfinite(actual) & np.isfinite(expected)
|
||||||
|
difference = np.abs(actual[finite] - expected[finite])
|
||||||
|
differing = difference != 0
|
||||||
|
relative = difference[np.abs(expected[finite]) != 0] / np.abs(expected[finite][np.abs(expected[finite]) != 0])
|
||||||
|
ulps = np.abs(_ordered_bits(actual[finite]).astype(object) - _ordered_bits(expected[finite]).astype(object))
|
||||||
|
ulps = np.asarray(ulps, dtype=np.float64)
|
||||||
|
return {
|
||||||
|
"finite_compared_count": int(finite.sum()),
|
||||||
|
"finite_differing_count": int(differing.sum()),
|
||||||
|
"max_absolute_error": float(difference.max()) if difference.size else 0.0,
|
||||||
|
"max_relative_error": float(relative.max()) if relative.size else 0.0,
|
||||||
|
"mae": float(difference.mean()) if difference.size else 0.0,
|
||||||
|
"max_ulp": int(ulps.max()) if ulps.size else 0,
|
||||||
|
"p50_ulp": float(np.percentile(ulps, 50)) if ulps.size else 0.0,
|
||||||
|
"p95_ulp": float(np.percentile(ulps, 95)) if ulps.size else 0.0,
|
||||||
|
"p99_ulp": float(np.percentile(ulps, 99)) if ulps.size else 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _decision_measurement(close: np.ndarray, actual: np.ndarray, expected: np.ndarray) -> dict[str, Any]:
|
||||||
|
valid = np.isfinite(close) & np.isfinite(actual) & np.isfinite(expected)
|
||||||
|
oracle_sign = np.sign(close[valid] - expected[valid]).astype(np.int8)
|
||||||
|
gpu_sign = np.sign(close[valid] - actual[valid]).astype(np.int8)
|
||||||
|
sign_exact = bool(np.array_equal(oracle_sign, gpu_sign))
|
||||||
|
oracle_above = oracle_sign > 0
|
||||||
|
gpu_above = gpu_sign > 0
|
||||||
|
oracle_below = oracle_sign < 0
|
||||||
|
gpu_below = gpu_sign < 0
|
||||||
|
return {
|
||||||
|
"valid_count": int(valid.sum()),
|
||||||
|
"price_comparison_signs_exact": sign_exact,
|
||||||
|
"price_comparison_sign_mismatches": int(np.count_nonzero(oracle_sign != gpu_sign)),
|
||||||
|
"crossings_above_exact": bool(np.array_equal((~oracle_above[:-1]) & oracle_above[1:], (~gpu_above[:-1]) & gpu_above[1:])),
|
||||||
|
"crossings_below_exact": bool(np.array_equal((~oracle_below[:-1]) & oracle_below[1:], (~gpu_below[:-1]) & gpu_below[1:])),
|
||||||
|
"state_transition_conditions_exact": sign_exact
|
||||||
|
and bool(np.array_equal((~oracle_above[:-1]) & oracle_above[1:], (~gpu_above[:-1]) & gpu_above[1:]))
|
||||||
|
and bool(np.array_equal((~oracle_below[:-1]) & oracle_below[1:], (~gpu_below[:-1]) & gpu_below[1:])),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _band_relationship(actual_upper: np.ndarray, actual_lower: np.ndarray, expected_upper: np.ndarray, expected_lower: np.ndarray) -> dict[str, Any]:
|
||||||
|
valid = np.isfinite(actual_upper) & np.isfinite(actual_lower) & np.isfinite(expected_upper) & np.isfinite(expected_lower)
|
||||||
|
oracle_relation = np.sign(expected_upper[valid] - expected_lower[valid]).astype(np.int8)
|
||||||
|
gpu_relation = np.sign(actual_upper[valid] - actual_lower[valid]).astype(np.int8)
|
||||||
|
return {"valid_count": int(valid.sum()), "upper_lower_relationship_exact": bool(np.array_equal(oracle_relation, gpu_relation)), "upper_lower_relationship_mismatches": int(np.count_nonzero(oracle_relation != gpu_relation))}
|
||||||
|
|
||||||
|
|
||||||
|
def _write_json(path: Path, value: dict[str, Any]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(value, indent=2, allow_nan=False) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _contract_markdown(contract: dict[str, Any]) -> str:
|
||||||
|
lines = ["# GPU Feature Parity Contract V1", "", f"Status: `{contract['status']}`.", "", "The contract requires exact structure and NaN placement, exact decision/state-transition conditions, and per-family measured numeric limits. Numeric drift is never accepted when it changes a decision.", "", "## Family Limits", ""]
|
||||||
|
for family, limit in contract["family_tolerances"].items():
|
||||||
|
lines.append(f"- `{family}`: max abs {limit['max_absolute_error']:.17g}, max rel {limit['max_relative_error']:.17g}, max ULP {limit['max_ulp']}, MAE {limit['mae']:.17g}.")
|
||||||
|
lines.extend(["", "## Gates", "", "- Shapes, dtypes, and NaN masks must match exactly.", "- Price-comparison signs, crossings above/below, and state-transition conditions must match exactly.", "- Bollinger and Keltner upper/lower relationships must match exactly.", ""])
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def parity_report(args: argparse.Namespace, device: torch.device) -> dict[str, Any]:
|
||||||
|
outputs, cache_hit, elapsed = _load_or_compute(args, device)
|
||||||
|
request = json.loads(args.request_json.read_text(encoding="utf-8"))
|
||||||
|
close = _load_ohlcv(args.data_csv, torch.device("cpu"))[0].numpy()
|
||||||
|
failures, variants, decisions = [], [], []
|
||||||
|
family_values: dict[str, list[tuple[np.ndarray, np.ndarray]]] = {}
|
||||||
|
band_pairs: dict[tuple[int, float], dict[int, tuple[np.ndarray, np.ndarray]]] = {}
|
||||||
|
with np.load(args.oracle_npz) as oracle:
|
||||||
|
for item in request["requests"]:
|
||||||
|
key, actual, expected = item["request_id"], outputs[item["request_id"]], oracle[item["request_id"]]
|
||||||
|
shape_ok, dtype_ok = actual.shape == expected.shape, actual.dtype == expected.dtype
|
||||||
|
equal = (actual == expected) | (np.isnan(actual) & np.isnan(expected)) if shape_ok else np.array([False])
|
||||||
|
mismatches = int(equal.size - equal.sum())
|
||||||
|
nan_exact = bool(shape_ok and np.array_equal(np.isnan(actual), np.isnan(expected)))
|
||||||
|
numeric = _numeric_measurement(actual, expected) if shape_ok else _numeric_measurement(np.array([], dtype=np.float64), np.array([], dtype=np.float64))
|
||||||
|
decision = _decision_measurement(close, actual, expected) if shape_ok else {"state_transition_conditions_exact": False}
|
||||||
|
family = _family(int(item["indicator_id"]))
|
||||||
|
record = {"request_id": key, "indicator_id": int(item["indicator_id"]), "family": family, "passed": bool(shape_ok and dtype_ok and nan_exact and mismatches == 0), "shape_match": shape_ok, "dtype_match": dtype_ok, "nan_mask_exact": nan_exact, "value_mismatches": mismatches, **numeric}
|
||||||
|
variants.append(record)
|
||||||
|
decisions.append({"request_id": key, "family": family, **decision})
|
||||||
|
family_values.setdefault(family, []).append((actual, expected))
|
||||||
|
if int(item["indicator_id"]) in {17, 18, 23, 24}:
|
||||||
|
band_pairs.setdefault((int(item["period"]), float(item["p1"])), {})[int(item["indicator_id"])] = (actual, expected)
|
||||||
|
if not record["passed"]:
|
||||||
|
failures.append(record)
|
||||||
|
family_tolerances = {family: _numeric_measurement(np.concatenate([pair[0] for pair in pairs]), np.concatenate([pair[1] for pair in pairs])) for family, pairs in family_values.items()}
|
||||||
|
relationships = []
|
||||||
|
for (period, multiplier), pair in band_pairs.items():
|
||||||
|
upper_id, lower_id = (17, 18) if 17 in pair else (23, 24)
|
||||||
|
actual_upper, expected_upper = pair[upper_id]
|
||||||
|
actual_lower, expected_lower = pair[lower_id]
|
||||||
|
relationships.append({"family": "bollinger" if upper_id == 17 else "keltner", "period": period, "p1": multiplier, **_band_relationship(actual_upper, actual_lower, expected_upper, expected_lower)})
|
||||||
|
decision_exact = all(item["state_transition_conditions_exact"] for item in decisions) and all(item["upper_lower_relationship_exact"] for item in relationships)
|
||||||
|
structural_exact = all(item["shape_match"] and item["dtype_match"] and item["nan_mask_exact"] for item in variants)
|
||||||
|
numeric_analysis = {"artifact": "GPU_BATCH01_NUMERIC_ANALYSIS_V1", "oracle_npz_sha256": _sha256(args.oracle_npz), "data_csv_sha256": _sha256(args.data_csv), "families": family_tolerances, "variants": variants}
|
||||||
|
decision_report = {"artifact": "GPU_BATCH01_DECISION_EQUIVALENCE_V1", "price_series": "close", "passed": decision_exact, "variants": decisions, "band_relationships": relationships}
|
||||||
|
contract = {"artifact": "GPU_FEATURE_PARITY_CONTRACT_V1", "schema_version": 1, "status": "accepted" if structural_exact and decision_exact else "rejected", "structural_nan_exact": structural_exact, "state_transition_conditions_exact": decision_exact, "numeric_limits_derived_from_measurement": True, "family_tolerances": family_tolerances, "numeric_errors_may_not_change_decisions": True, "oracle_npz_sha256": _sha256(args.oracle_npz), "data_csv_sha256": _sha256(args.data_csv)}
|
||||||
|
_write_json(args.output_dir / "gpu_batch01_numeric_analysis_v1.json", numeric_analysis)
|
||||||
|
_write_json(args.output_dir / "gpu_batch01_decision_equivalence_v1.json", decision_report)
|
||||||
|
_write_json(args.output_dir / "gpu_feature_parity_contract_v1.json", contract)
|
||||||
|
(args.output_dir / "gpu_feature_parity_contract_v1.md").write_text(_contract_markdown(contract), encoding="utf-8")
|
||||||
|
return {"artifact": "BATCH01_GPU_PARITY_REPORT_V1", "prototype": True, "parity_claimed": False, "smoke": smoke(device), "device": str(device), "variant_count": len(variants), "passed": structural_exact and decision_exact, "failure_count": len(failures), "cache_hit": cache_hit, "compute_seconds": elapsed, "artifacts": [str(args.output_dir / name) for name in ("gpu_batch01_numeric_analysis_v1.json", "gpu_batch01_decision_equivalence_v1.json", "gpu_feature_parity_contract_v1.json", "gpu_feature_parity_contract_v1.md")], "failures": failures}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--mode", choices=("smoke", "parity"), default="smoke")
|
||||||
|
parser.add_argument(
|
||||||
|
"--data-csv",
|
||||||
|
type=Path,
|
||||||
|
default=Path(os.getenv("GPU_FEATURE_DATA_CSV", "/data/binance_btcusdt_spot_2m_180d.csv")),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--oracle-npz",
|
||||||
|
type=Path,
|
||||||
|
default=Path(os.getenv("GPU_FEATURE_ORACLE_NPZ", "/oracle/batch01_oracle_outputs.npz")),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--request-json",
|
||||||
|
type=Path,
|
||||||
|
default=Path(os.getenv("GPU_FEATURE_REQUEST_JSON", "/oracle/batch01_oracle_request.json")),
|
||||||
|
)
|
||||||
|
parser.add_argument("--cache-dir", type=Path, default=Path(os.getenv("GPU_FEATURE_CACHE_DIR", "/cache")))
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-dir",
|
||||||
|
type=Path,
|
||||||
|
default=Path(os.getenv("GPU_FEATURE_OUTPUT_DIR", "/cache")),
|
||||||
|
help="directory for gpu_batch01_* analysis and gpu_feature_parity_contract_v1 artifacts",
|
||||||
|
)
|
||||||
|
parser.add_argument("--report", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not torch.cuda.is_available():
|
||||||
|
raise SystemExit("CUDA GPU is required; torch.cuda.is_available() is false")
|
||||||
|
device = torch.device("cuda")
|
||||||
|
report = smoke(device) if args.mode == "smoke" else parity_report(args, device)
|
||||||
|
encoded = json.dumps(report, indent=2, allow_nan=False) + "\n"
|
||||||
|
if args.report:
|
||||||
|
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.report.write_text(encoded, encoding="utf-8")
|
||||||
|
print(encoded, end="")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
78
gpu_feature_engine_v1_2.py
Normal file
78
gpu_feature_engine_v1_2.py
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
"""V1.2 GPU feature engine.
|
||||||
|
|
||||||
|
This module intentionally leaves ``gpu_feature_engine_v1`` unchanged. Only
|
||||||
|
Supertrend uses the V1.2 ordered, on-device RMA seed implementation.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from gpu_feature_engine_v1 import _empty, _true_range, evaluate_batch as _v1_evaluate_batch
|
||||||
|
|
||||||
|
|
||||||
|
def rma_ordered_seed(values: torch.Tensor, period: int) -> torch.Tensor:
|
||||||
|
"""RMA with the historical oracle's left-to-right seed association."""
|
||||||
|
out = _empty(values)
|
||||||
|
valid = torch.nonzero(~torch.isnan(values), as_tuple=False).flatten()
|
||||||
|
if valid.numel() < period:
|
||||||
|
return out
|
||||||
|
seed_bar = int(valid[period - 1].item())
|
||||||
|
total = torch.zeros((), dtype=values.dtype, device=values.device)
|
||||||
|
for index in valid[:period]:
|
||||||
|
total = total + values[index]
|
||||||
|
out[seed_bar] = total / period
|
||||||
|
alpha = 1.0 / period
|
||||||
|
for index in range(seed_bar + 1, values.numel()):
|
||||||
|
out[index] = out[index - 1] if torch.isnan(values[index]) else alpha * values[index] + (1.0 - alpha) * out[index - 1]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def supertrend_trace(close: torch.Tensor, high: torch.Tensor, low: torch.Tensor, period: int, multiplier: float) -> dict[str, torch.Tensor]:
|
||||||
|
"""Return complete V1.2 Supertrend state, including branch predicates."""
|
||||||
|
true_range = _true_range(close, high, low)
|
||||||
|
atr, output, upper, lower = rma_ordered_seed(true_range, period), _empty(close), _empty(close), _empty(close)
|
||||||
|
basic_upper, basic_lower = _empty(close), _empty(close)
|
||||||
|
direction = torch.zeros_like(close, dtype=torch.int8)
|
||||||
|
initialized = torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
prior_close_le_upper = torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
prior_close_ge_lower = torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
active_long = torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
close_ge_lower = torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
close_le_upper = torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
output_uses_lower = torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
direction_transition = torch.zeros_like(close, dtype=torch.bool)
|
||||||
|
for index in range(period, close.numel()):
|
||||||
|
midpoint = (high[index] + low[index]) / 2.0
|
||||||
|
basic_upper[index], basic_lower[index] = midpoint + multiplier * atr[index], midpoint - multiplier * atr[index]
|
||||||
|
if index == period:
|
||||||
|
upper[index], lower[index] = basic_upper[index], basic_lower[index]
|
||||||
|
close_ge_lower[index] = close[index] > lower[index]
|
||||||
|
output_uses_lower[index] = close_ge_lower[index]
|
||||||
|
output[index] = lower[index] if output_uses_lower[index] else upper[index]
|
||||||
|
else:
|
||||||
|
prior_close_le_upper[index] = close[index - 1] <= upper[index - 1]
|
||||||
|
prior_close_ge_lower[index] = close[index - 1] >= lower[index - 1]
|
||||||
|
upper[index] = torch.minimum(basic_upper[index], upper[index - 1]) if prior_close_le_upper[index] else basic_upper[index]
|
||||||
|
lower[index] = torch.maximum(basic_lower[index], lower[index - 1]) if prior_close_ge_lower[index] else basic_lower[index]
|
||||||
|
active_long[index] = direction[index - 1] == 1
|
||||||
|
close_ge_lower[index] = close[index] >= lower[index]
|
||||||
|
close_le_upper[index] = close[index] <= upper[index]
|
||||||
|
output_uses_lower[index] = active_long[index] and close_ge_lower[index] or not active_long[index] and not close_le_upper[index]
|
||||||
|
output[index] = lower[index] if output_uses_lower[index] else upper[index]
|
||||||
|
direction[index] = torch.where(close[index] > output[index], 1, -1)
|
||||||
|
initialized[index] = True
|
||||||
|
if index > period:
|
||||||
|
direction_transition[index] = direction[index] != direction[index - 1]
|
||||||
|
return {"output": output, "true_range": true_range, "atr": atr, "basic_upper": basic_upper, "basic_lower": basic_lower, "upper": upper, "lower": lower, "direction": direction, "initialized": initialized, "prior_close_le_upper": prior_close_le_upper, "prior_close_ge_lower": prior_close_ge_lower, "active_long": active_long, "close_ge_lower": close_ge_lower, "close_le_upper": close_le_upper, "output_uses_lower": output_uses_lower, "direction_transition": direction_transition}
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_batch(request: dict[str, Any], close: torch.Tensor, high: torch.Tensor, low: torch.Tensor, volume: torch.Tensor) -> dict[str, torch.Tensor]:
|
||||||
|
"""Evaluate Batch01, replacing only Supertrend requests with V1.2 logic."""
|
||||||
|
other = {"requests": [item for item in request["requests"] if int(item["indicator_id"]) != 19]}
|
||||||
|
outputs = _v1_evaluate_batch(other, close, high, low, volume) if other["requests"] else {}
|
||||||
|
for item in request["requests"]:
|
||||||
|
if int(item["indicator_id"]) == 19:
|
||||||
|
outputs[str(item["request_id"])] = supertrend_trace(close, high, low, int(item["period"]), float(item["p1"]))["output"]
|
||||||
|
return outputs
|
||||||
1
gpu_feature_parity_calibration_v1_2.json
Normal file
1
gpu_feature_parity_calibration_v1_2.json
Normal file
File diff suppressed because one or more lines are too long
68
gpu_feature_parity_contract_v1.json
Normal file
68
gpu_feature_parity_contract_v1.json
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"artifact": "GPU_FEATURE_PARITY_CONTRACT_V1",
|
||||||
|
"schema_version": 1,
|
||||||
|
"status": "accepted",
|
||||||
|
"structural_nan_exact": true,
|
||||||
|
"state_transition_conditions_exact": true,
|
||||||
|
"numeric_limits_derived_from_measurement": true,
|
||||||
|
"family_tolerances": {
|
||||||
|
"bollinger": {
|
||||||
|
"finite_compared_count": 4664916,
|
||||||
|
"finite_differing_count": 4632967,
|
||||||
|
"max_absolute_error": 1.82626536116004e-09,
|
||||||
|
"max_relative_error": 2.989992025370508e-14,
|
||||||
|
"mae": 6.429624976711859e-10,
|
||||||
|
"max_ulp": 251,
|
||||||
|
"p50_ulp": 46.0,
|
||||||
|
"p95_ulp": 202.0,
|
||||||
|
"p99_ulp": 229.0
|
||||||
|
},
|
||||||
|
"supertrend": {
|
||||||
|
"finite_compared_count": 1555056,
|
||||||
|
"finite_differing_count": 0,
|
||||||
|
"max_absolute_error": 0.0,
|
||||||
|
"max_relative_error": 0.0,
|
||||||
|
"mae": 0.0,
|
||||||
|
"max_ulp": 0,
|
||||||
|
"p50_ulp": 0.0,
|
||||||
|
"p95_ulp": 0.0,
|
||||||
|
"p99_ulp": 0.0
|
||||||
|
},
|
||||||
|
"donchian": {
|
||||||
|
"finite_compared_count": 1295820,
|
||||||
|
"finite_differing_count": 0,
|
||||||
|
"max_absolute_error": 0.0,
|
||||||
|
"max_relative_error": 0.0,
|
||||||
|
"mae": 0.0,
|
||||||
|
"max_ulp": 0,
|
||||||
|
"p50_ulp": 0.0,
|
||||||
|
"p95_ulp": 0.0,
|
||||||
|
"p99_ulp": 0.0
|
||||||
|
},
|
||||||
|
"keltner": {
|
||||||
|
"finite_compared_count": 4664916,
|
||||||
|
"finite_differing_count": 222,
|
||||||
|
"max_absolute_error": 2.9103830456733704e-11,
|
||||||
|
"max_relative_error": 4.285296050513011e-16,
|
||||||
|
"mae": 9.919812152288828e-16,
|
||||||
|
"max_ulp": 2,
|
||||||
|
"p50_ulp": 0.0,
|
||||||
|
"p95_ulp": 0.0,
|
||||||
|
"p99_ulp": 0.0
|
||||||
|
},
|
||||||
|
"psar": {
|
||||||
|
"finite_compared_count": 388794,
|
||||||
|
"finite_differing_count": 0,
|
||||||
|
"max_absolute_error": 0.0,
|
||||||
|
"max_relative_error": 0.0,
|
||||||
|
"mae": 0.0,
|
||||||
|
"max_ulp": 0,
|
||||||
|
"p50_ulp": 0.0,
|
||||||
|
"p95_ulp": 0.0,
|
||||||
|
"p99_ulp": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"numeric_errors_may_not_change_decisions": true,
|
||||||
|
"oracle_npz_sha256": "e1f2268b634e50eb65b9b5175ea369e52bf0a2039b18049030849948a45fd149",
|
||||||
|
"data_csv_sha256": "7fb56bb05b0edc2348407765cd693efadf334307c25877dfb52acfe4221e5d00"
|
||||||
|
}
|
||||||
19
gpu_feature_parity_contract_v1.md
Normal file
19
gpu_feature_parity_contract_v1.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# GPU Feature Parity Contract V1
|
||||||
|
|
||||||
|
Status: `accepted`.
|
||||||
|
|
||||||
|
The contract requires exact structure and NaN placement, exact decision/state-transition conditions, and per-family measured numeric limits. Numeric drift is never accepted when it changes a decision.
|
||||||
|
|
||||||
|
## Family Limits
|
||||||
|
|
||||||
|
- `bollinger`: max abs 1.8262653611600399e-09, max rel 2.9899920253705082e-14, max ULP 251, MAE 6.4296249767118589e-10.
|
||||||
|
- `supertrend`: max abs 0, max rel 0, max ULP 0, MAE 0.
|
||||||
|
- `donchian`: max abs 0, max rel 0, max ULP 0, MAE 0.
|
||||||
|
- `keltner`: max abs 2.9103830456733704e-11, max rel 4.2852960505130111e-16, max ULP 2, MAE 9.9198121522888284e-16.
|
||||||
|
- `psar`: max abs 0, max rel 0, max ULP 0, MAE 0.
|
||||||
|
|
||||||
|
## Gates
|
||||||
|
|
||||||
|
- Shapes, dtypes, and NaN masks must match exactly.
|
||||||
|
- Price-comparison signs, crossings above/below, and state-transition conditions must match exactly.
|
||||||
|
- Bollinger and Keltner upper/lower relationships must match exactly.
|
||||||
244
gpu_feature_parity_contract_v1_1.py
Normal file
244
gpu_feature_parity_contract_v1_1.py
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
"""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}
|
||||||
1
gpu_feature_parity_contract_v1_2.json
Normal file
1
gpu_feature_parity_contract_v1_2.json
Normal file
File diff suppressed because one or more lines are too long
22
gpu_feature_parity_contract_v1_2.md
Normal file
22
gpu_feature_parity_contract_v1_2.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# GPU Batch01 V1.2 Frozen Validation Contract
|
||||||
|
|
||||||
|
All 97 observed Batch01 outputs are evaluated against `historical_band_channel`.
|
||||||
|
|
||||||
|
## Evidence Classes
|
||||||
|
|
||||||
|
- Donchian and PSAR outputs: exact.
|
||||||
|
- Bollinger and Keltner outputs: bounded by calibration-only limits.
|
||||||
|
- Supertrend final output, bands, direction, transitions, and branch predicates: exact; ATR uses supplied fixed bounds.
|
||||||
|
|
||||||
|
## Frozen Coverage
|
||||||
|
|
||||||
|
- `bollinger`: 36
|
||||||
|
- `donchian`: 10
|
||||||
|
- `keltner`: 36
|
||||||
|
- `psar`: 3
|
||||||
|
- `supertrend`: 12
|
||||||
|
|
||||||
|
## Role Status
|
||||||
|
|
||||||
|
Unrecovered strategy roles are `UNVERIFIABLE_FROM_RECOVERED_SOURCE`; they are recorded but are not parity blockers.
|
||||||
|
Role records: 97.
|
||||||
243
gpu_feature_parity_contract_v1_2.py
Normal file
243
gpu_feature_parity_contract_v1_2.py
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
"""GPU_FEATURE_PARITY_CONTRACT_V1_2 state-first validation contract."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
ARTIFACT = "GPU_FEATURE_PARITY_CONTRACT_V1_2"
|
||||||
|
CALIBRATION_ARTIFACT = "GPU_FEATURE_PARITY_CALIBRATION_V1_2"
|
||||||
|
VALIDATION_ARTIFACT = "GPU_FEATURE_PARITY_VALIDATION_V1_2"
|
||||||
|
ROLE_SURFACE_ARTIFACT = "GPU_FEATURE_PARITY_ROLE_SURFACE_V1_2"
|
||||||
|
REVIEW_TEMPLATE_ARTIFACT = "GPU_FEATURE_PARITY_REVIEW_TEMPLATE_V1_2"
|
||||||
|
ROLE_A_EXACT = "A_EXACT"
|
||||||
|
ROLE_B_BOUNDED = "B_BOUNDED"
|
||||||
|
ROLE_C_UNVERIFIABLE = "C_UNVERIFIABLE"
|
||||||
|
UNVERIFIABLE = "UNVERIFIABLE_FROM_RECOVERED_SOURCE"
|
||||||
|
SUPERTREND_TRACE_KEYS = frozenset(("output", "true_range", "atr", "basic_upper", "basic_lower", "upper", "lower", "direction", "initialized", "prior_close_le_upper", "prior_close_ge_lower", "active_long", "close_ge_lower", "close_le_upper", "output_uses_lower", "direction_transition"))
|
||||||
|
EXACT_TRACE_KEYS = SUPERTREND_TRACE_KEYS - {"true_range", "atr", "basic_upper", "basic_lower"}
|
||||||
|
PREDICATE_KEYS = EXACT_TRACE_KEYS - {"output", "upper", "lower", "direction", "initialized", "direction_transition"}
|
||||||
|
|
||||||
|
|
||||||
|
class FrozenContractError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_bytes(value: object) -> bytes:
|
||||||
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
|
||||||
|
|
||||||
|
|
||||||
|
def _digest(value: object) -> str:
|
||||||
|
return hashlib.sha256(canonical_bytes(value)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _same(expected: np.ndarray, actual: np.ndarray) -> bool:
|
||||||
|
return expected.shape == actual.shape and np.array_equal(expected, actual, equal_nan=True)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_supertrend_trace(expected: Mapping[str, np.ndarray], actual: Mapping[str, np.ndarray]) -> dict[str, Any]:
|
||||||
|
"""Check all discrete states and branch predicates exactly; ATR is excluded."""
|
||||||
|
required = EXACT_TRACE_KEYS | {"atr"}
|
||||||
|
missing = sorted(required - set(expected) | required - set(actual))
|
||||||
|
mismatched = sorted(key for key in EXACT_TRACE_KEYS if key in expected and key in actual and not _same(np.asarray(expected[key]), np.asarray(actual[key])))
|
||||||
|
atr = exact_output(np.asarray(expected["atr"]), np.asarray(actual["atr"])) if "atr" in expected and "atr" in actual else None
|
||||||
|
atr_structure_exact = atr is not None and atr["shape_exact"] and atr["nan_mask_exact"]
|
||||||
|
state_exact = not missing and not mismatched
|
||||||
|
return {"state_exact": state_exact, "required_keys": sorted(required), "mismatched_exact_keys": mismatched, "missing_keys": missing, "atr": atr, "atr_structure_exact": atr_structure_exact, "branch_predicates_exact": state_exact and all(key not in mismatched for key in PREDICATE_KEYS), "transition_checks_exact": state_exact}
|
||||||
|
|
||||||
|
|
||||||
|
def atr_metrics(expected: np.ndarray, actual: np.ndarray) -> dict[str, float]:
|
||||||
|
finite = np.isfinite(expected) & np.isfinite(actual)
|
||||||
|
delta = np.abs(np.asarray(actual)[finite] - np.asarray(expected)[finite])
|
||||||
|
return {"max_absolute_error": float(delta.max()) if delta.size else 0.0, "mae": float(delta.mean()) if delta.size else 0.0}
|
||||||
|
|
||||||
|
|
||||||
|
def output_metrics(expected: np.ndarray, actual: np.ndarray) -> dict[str, float]:
|
||||||
|
"""Stable, JSON-safe continuous-output measurements."""
|
||||||
|
finite = np.isfinite(expected) & np.isfinite(actual)
|
||||||
|
delta = np.abs(np.asarray(actual)[finite] - np.asarray(expected)[finite])
|
||||||
|
denominator = np.abs(np.asarray(expected)[finite])
|
||||||
|
relative = delta[denominator != 0] / denominator[denominator != 0]
|
||||||
|
return {
|
||||||
|
"max_absolute_error": float(delta.max()) if delta.size else 0.0,
|
||||||
|
"max_relative_error": float(relative.max()) if relative.size else 0.0,
|
||||||
|
"mae": float(delta.mean()) if delta.size else 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def exact_output(expected: np.ndarray, actual: np.ndarray) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"shape_exact": expected.shape == actual.shape,
|
||||||
|
"dtype_exact": expected.dtype == actual.dtype,
|
||||||
|
"nan_mask_exact": expected.shape == actual.shape
|
||||||
|
and np.array_equal(np.isnan(expected), np.isnan(actual)),
|
||||||
|
"values_exact": _same(expected, actual),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def family_for_indicator(indicator_id: int) -> str:
|
||||||
|
return {17: "bollinger", 18: "bollinger", 19: "supertrend", 20: "donchian", 21: "donchian", 23: "keltner", 24: "keltner", 28: "psar"}[indicator_id]
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_supertrend_trace(close: np.ndarray, high: np.ndarray, low: np.ndarray, period: int, multiplier: float) -> dict[str, np.ndarray]:
|
||||||
|
"""Historical Supertrend semantics with explicit V1.2 branch evidence."""
|
||||||
|
from control_plane.trading_studio.indicators.historical_band_channel import _rma, _true_range
|
||||||
|
n = len(close)
|
||||||
|
true_range = _true_range(high, low, close)
|
||||||
|
atr = _rma(true_range, period)
|
||||||
|
output = np.full(n, np.nan)
|
||||||
|
upper, lower, basic_upper, basic_lower = output.copy(), output.copy(), output.copy(), output.copy()
|
||||||
|
direction = np.zeros(n, dtype=np.int8)
|
||||||
|
initialized = np.zeros(n, dtype=bool)
|
||||||
|
predicates = {name: np.zeros(n, dtype=bool) for name in PREDICATE_KEYS}
|
||||||
|
direction_transition = np.zeros(n, dtype=bool)
|
||||||
|
for index in range(period, n):
|
||||||
|
midpoint = (high[index] + low[index]) / 2.0
|
||||||
|
basic_upper[index], basic_lower[index] = midpoint + multiplier * atr[index], midpoint - multiplier * atr[index]
|
||||||
|
if index == period:
|
||||||
|
upper[index], lower[index] = basic_upper[index], basic_lower[index]
|
||||||
|
predicates["close_ge_lower"][index] = close[index] > lower[index]
|
||||||
|
predicates["output_uses_lower"][index] = predicates["close_ge_lower"][index]
|
||||||
|
output[index] = lower[index] if predicates["output_uses_lower"][index] else upper[index]
|
||||||
|
else:
|
||||||
|
predicates["prior_close_le_upper"][index] = close[index - 1] <= upper[index - 1]
|
||||||
|
predicates["prior_close_ge_lower"][index] = close[index - 1] >= lower[index - 1]
|
||||||
|
upper[index] = min(basic_upper[index], upper[index - 1]) if predicates["prior_close_le_upper"][index] else basic_upper[index]
|
||||||
|
lower[index] = max(basic_lower[index], lower[index - 1]) if predicates["prior_close_ge_lower"][index] else basic_lower[index]
|
||||||
|
predicates["active_long"][index] = direction[index - 1] == 1
|
||||||
|
predicates["close_ge_lower"][index] = close[index] >= lower[index]
|
||||||
|
predicates["close_le_upper"][index] = close[index] <= upper[index]
|
||||||
|
predicates["output_uses_lower"][index] = predicates["active_long"][index] and predicates["close_ge_lower"][index] or not predicates["active_long"][index] and not predicates["close_le_upper"][index]
|
||||||
|
output[index] = lower[index] if predicates["output_uses_lower"][index] else upper[index]
|
||||||
|
direction[index] = 1 if close[index] > output[index] else -1
|
||||||
|
initialized[index] = True
|
||||||
|
if index > period:
|
||||||
|
direction_transition[index] = direction[index] != direction[index - 1]
|
||||||
|
return {
|
||||||
|
"output": output,
|
||||||
|
"true_range": true_range,
|
||||||
|
"atr": atr,
|
||||||
|
"basic_upper": basic_upper,
|
||||||
|
"basic_lower": basic_lower,
|
||||||
|
"upper": upper,
|
||||||
|
"lower": lower,
|
||||||
|
"direction": direction,
|
||||||
|
"initialized": initialized,
|
||||||
|
"direction_transition": direction_transition,
|
||||||
|
**predicates,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calibrate_supertrend(request_id: str, expected: Mapping[str, np.ndarray], actual: Mapping[str, np.ndarray], *, atr_limits: Mapping[str, float] | None = None) -> dict[str, Any]:
|
||||||
|
trace = compare_supertrend_trace(expected, actual)
|
||||||
|
measured = atr_metrics(np.asarray(expected["atr"]), np.asarray(actual["atr"])) if trace["state_exact"] and trace["atr_structure_exact"] else None
|
||||||
|
return {"request_id": request_id, "family": "supertrend", "role": ROLE_A_EXACT, "role_classification": {"discrete_state_and_transitions": ROLE_A_EXACT, "atr": ROLE_B_BOUNDED}, "trace": trace, "atr": measured, "atr_limits": dict(atr_limits) if atr_limits is not None else measured}
|
||||||
|
|
||||||
|
|
||||||
|
def calibrate_output(request_id: str, indicator_id: int, expected: np.ndarray, actual: np.ndarray) -> dict[str, Any]:
|
||||||
|
"""Calibrate a non-Supertrend Batch01 output under its family evidence class."""
|
||||||
|
family = family_for_indicator(indicator_id)
|
||||||
|
exact = family in {"donchian", "psar"}
|
||||||
|
result = exact_output(expected, actual)
|
||||||
|
return {
|
||||||
|
"request_id": request_id,
|
||||||
|
"indicator_id": indicator_id,
|
||||||
|
"family": family,
|
||||||
|
"role": ROLE_A_EXACT if exact else ROLE_B_BOUNDED,
|
||||||
|
"output": result,
|
||||||
|
# Bounds are deliberately emitted only for continuous families.
|
||||||
|
"limits": None if exact else output_metrics(expected, actual),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def freeze_contract(calibration: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
if calibration.get("artifact") != CALIBRATION_ARTIFACT:
|
||||||
|
raise FrozenContractError("only V1.2 calibration evidence can be frozen")
|
||||||
|
limits = {}
|
||||||
|
for record in calibration.get("records", []):
|
||||||
|
if record.get("family") == "supertrend":
|
||||||
|
if not record["trace"]["state_exact"] or not record["trace"]["atr_structure_exact"] or record["atr"] is None:
|
||||||
|
raise FrozenContractError("cannot freeze a state-inexact calibration")
|
||||||
|
limits[record["request_id"]] = {"family": "supertrend", "role": ROLE_A_EXACT, "atr_limits": dict(record["atr_limits"])}
|
||||||
|
elif record.get("family") in {"donchian", "psar"}:
|
||||||
|
if not all(record["output"].values()):
|
||||||
|
raise FrozenContractError("cannot freeze an inexact discrete/state calibration")
|
||||||
|
limits[record["request_id"]] = {"family": record["family"], "role": ROLE_A_EXACT}
|
||||||
|
elif record.get("family") in {"bollinger", "keltner"}:
|
||||||
|
if not record["output"]["shape_exact"] or not record["output"]["nan_mask_exact"]:
|
||||||
|
raise FrozenContractError("cannot freeze a structurally inexact continuous calibration")
|
||||||
|
limits[record["request_id"]] = {"family": record["family"], "role": ROLE_B_BOUNDED, "output_limits": dict(record["limits"])}
|
||||||
|
else:
|
||||||
|
raise FrozenContractError("unknown Batch01 family in calibration")
|
||||||
|
return {"artifact": ARTIFACT, "schema_version": "1.2", "status": "frozen", "role_classification": {"discrete_state_and_transitions": ROLE_A_EXACT, "atr": ROLE_B_BOUNDED, "unrecovered_batch01_roles": ROLE_C_UNVERIFIABLE}, "calibration_sha256": _digest(calibration), "feature_limits": limits}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_all_frozen_contract(contract: Mapping[str, Any], expected_outputs: Mapping[str, np.ndarray], actual_outputs: Mapping[str, np.ndarray], traces: Mapping[str, tuple[Mapping[str, np.ndarray], Mapping[str, np.ndarray]]]) -> dict[str, Any]:
|
||||||
|
"""Validate every frozen Batch01 feature without deriving new limits."""
|
||||||
|
if contract.get("artifact") != ARTIFACT or contract.get("status") != "frozen":
|
||||||
|
raise FrozenContractError("provided contract is not frozen GPU_FEATURE_PARITY_CONTRACT_V1_2")
|
||||||
|
limits = contract.get("feature_limits", {})
|
||||||
|
if set(limits) != set(expected_outputs) or set(limits) != set(actual_outputs):
|
||||||
|
raise FrozenContractError("contract and outputs must name exactly the same features")
|
||||||
|
records = []
|
||||||
|
for request_id, limit in limits.items():
|
||||||
|
family = limit["family"]
|
||||||
|
expected, actual = np.asarray(expected_outputs[request_id]), np.asarray(actual_outputs[request_id])
|
||||||
|
if family == "supertrend":
|
||||||
|
if request_id not in traces:
|
||||||
|
raise FrozenContractError("Supertrend trace missing")
|
||||||
|
trace = compare_supertrend_trace(*traces[request_id])
|
||||||
|
atr = atr_metrics(np.asarray(traces[request_id][0]["atr"]), np.asarray(traces[request_id][1]["atr"])) if trace["state_exact"] and trace["atr_structure_exact"] else None
|
||||||
|
bounded = atr is not None and all(atr[key] <= float(limit["atr_limits"][key]) for key in atr)
|
||||||
|
passed = trace["state_exact"] and trace["atr_structure_exact"] and bounded
|
||||||
|
records.append({"request_id": request_id, "family": family, "role": ROLE_A_EXACT, "trace": trace, "atr": atr, "atr_bounded": bounded, "passed": passed})
|
||||||
|
elif family in {"donchian", "psar"}:
|
||||||
|
output = exact_output(expected, actual)
|
||||||
|
records.append({"request_id": request_id, "family": family, "role": ROLE_A_EXACT, "output": output, "passed": all(output.values())})
|
||||||
|
else:
|
||||||
|
output = exact_output(expected, actual)
|
||||||
|
metrics = output_metrics(expected, actual)
|
||||||
|
bounded = all(metrics[key] <= float(limit["output_limits"][key]) for key in metrics)
|
||||||
|
passed = output["shape_exact"] and output["nan_mask_exact"] and bounded
|
||||||
|
records.append({"request_id": request_id, "family": family, "role": ROLE_B_BOUNDED, "output": output, "metrics": metrics, "bounded": bounded, "passed": passed})
|
||||||
|
return {"artifact": VALIDATION_ARTIFACT, "schema_version": "1.2", "contract_sha256": _digest(contract), "passed": all(record["passed"] for record in records), "records": records}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_frozen_contract(contract: Mapping[str, Any], traces: Mapping[str, tuple[Mapping[str, np.ndarray], Mapping[str, np.ndarray]]]) -> dict[str, Any]:
|
||||||
|
"""Mechanical validation only: it never derives or expands frozen limits."""
|
||||||
|
if contract.get("artifact") != ARTIFACT or contract.get("schema_version") != "1.2" or contract.get("status") != "frozen":
|
||||||
|
raise FrozenContractError("provided contract is not frozen GPU_FEATURE_PARITY_CONTRACT_V1_2")
|
||||||
|
limits = contract.get("feature_limits")
|
||||||
|
if not isinstance(limits, dict) or set(limits) != set(traces):
|
||||||
|
raise FrozenContractError("contract and traces must name exactly the same features")
|
||||||
|
records = []
|
||||||
|
for request_id, (expected, actual) in traces.items():
|
||||||
|
trace = compare_supertrend_trace(expected, actual)
|
||||||
|
metric = atr_metrics(np.asarray(expected["atr"]), np.asarray(actual["atr"])) if trace["state_exact"] and trace["atr_structure_exact"] else None
|
||||||
|
bounded = metric is not None and all(metric[name] <= float(limits[request_id]["atr_limits"][name]) for name in metric)
|
||||||
|
records.append({"request_id": request_id, "role": ROLE_A_EXACT, "role_classification": {"discrete_state_and_transitions": ROLE_A_EXACT, "atr": ROLE_B_BOUNDED}, "trace": trace, "atr": metric, "atr_bounded": bounded, "passed": trace["state_exact"] and trace["atr_structure_exact"] and bounded})
|
||||||
|
return {"artifact": VALIDATION_ARTIFACT, "schema_version": "1.2", "contract_sha256": _digest(contract), "passed": all(record["passed"] for record in records), "records": records}
|
||||||
|
|
||||||
|
|
||||||
|
def role_surface(requests: list[Mapping[str, Any]]) -> dict[str, Any]:
|
||||||
|
"""Classify only recovered Cohort001 role assignments; unknown Batch01 roles do not block parity."""
|
||||||
|
try:
|
||||||
|
from control_plane.trading_studio.indicators.registry import _HS22_REQUIRED_TRIPLES
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
recovered = {}
|
||||||
|
unavailable_reason = "control_plane.trading_studio.indicators.registry is unavailable"
|
||||||
|
else:
|
||||||
|
recovered = {(indicator, period, p1): slot for indicator, period, p1, slot in _HS22_REQUIRED_TRIPLES}
|
||||||
|
unavailable_reason = None
|
||||||
|
records = []
|
||||||
|
for item in requests:
|
||||||
|
key = (int(item["indicator_id"]), int(item["period"]), float(item["p1"]))
|
||||||
|
slot = recovered.get(key)
|
||||||
|
records.append({"request_id": str(item["request_id"]), "indicator_id": key[0], "period": key[1], "p1": key[2], "cohort001_role": slot, "classification": ROLE_A_EXACT if key[0] == 19 and slot else ROLE_C_UNVERIFIABLE, "role_status": "RECOVERED_COHORT001" if slot else UNVERIFIABLE, "reason": unavailable_reason if not slot else None, "parity_blocker": False if not slot else True})
|
||||||
|
return {"artifact": ROLE_SURFACE_ARTIFACT, "schema_version": "1.2", "source": "Cohort001 recovered role surface", "records": records}
|
||||||
|
|
||||||
|
|
||||||
|
def review_template() -> dict[str, Any]:
|
||||||
|
return {"artifact": REVIEW_TEMPLATE_ARTIFACT, "schema_version": "1.2", "status": "review_required_not_validated", "required_review": ["calibration evidence is separate from holdout and adversarial evidence", "exact state, transition, and branch-predicate checks pass", "ATR bounded checks were evaluated only after exact state validation", "C-unverifiable roles are recorded and excluded as parity blockers"], "decision": None}
|
||||||
1
gpu_feature_parity_role_surface_v1_2.json
Normal file
1
gpu_feature_parity_role_surface_v1_2.json
Normal file
File diff suppressed because one or more lines are too long
1
gpu_feature_parity_validation_adversarial_v1_2.json
Normal file
1
gpu_feature_parity_validation_adversarial_v1_2.json
Normal file
File diff suppressed because one or more lines are too long
1
gpu_feature_parity_validation_calibration_v1_2.json
Normal file
1
gpu_feature_parity_validation_calibration_v1_2.json
Normal file
File diff suppressed because one or more lines are too long
1
gpu_feature_parity_validation_holdout_v1_2.json
Normal file
1
gpu_feature_parity_validation_holdout_v1_2.json
Normal file
File diff suppressed because one or more lines are too long
1
gpu_feature_parity_validation_v1_1.json
Normal file
1
gpu_feature_parity_validation_v1_1.json
Normal file
File diff suppressed because one or more lines are too long
144
gpu_supertrend_v1_2_diagnostic.py
Normal file
144
gpu_supertrend_v1_2_diagnostic.py
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
"""V1.2-only semantic diagnostic for CPU and GPU Supertrend traces."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from gpu_feature_engine_v1_2 import supertrend_trace
|
||||||
|
from gpu_feature_parity_contract_v1_1 import deterministic_adversarial_ohlcv
|
||||||
|
from gpu_feature_parity_contract_v1_2 import PREDICATE_KEYS, SUPERTREND_TRACE_KEYS, cpu_supertrend_trace
|
||||||
|
|
||||||
|
ARTIFACT = "GPU_SUPERTREND_ATR_SEMANTIC_DIAGNOSTIC_V1_2"
|
||||||
|
PERIOD = 10
|
||||||
|
MULTIPLIERS = (2.0, 3.0, 4.0)
|
||||||
|
NUMERIC_FIELDS = ("output", "true_range", "atr", "basic_upper", "basic_lower", "upper", "lower")
|
||||||
|
EXACT_FIELDS = tuple(sorted((*PREDICATE_KEYS, "direction", "direction_transition")))
|
||||||
|
|
||||||
|
|
||||||
|
def _read_ohlcv(path: Path) -> dict[str, np.ndarray]:
|
||||||
|
with path.open(newline="", encoding="utf-8") as handle:
|
||||||
|
rows = list(csv.DictReader(handle))
|
||||||
|
names = ("close", "high", "low")
|
||||||
|
if not rows or not set(names).issubset(rows[0]):
|
||||||
|
raise ValueError("CSV must contain non-empty close, high, and low columns")
|
||||||
|
values = {name: np.asarray([float(row[name]) for row in rows], dtype=np.float64) for name in names}
|
||||||
|
if not all(np.isfinite(value).all() for value in values.values()):
|
||||||
|
raise ValueError("diagnostic requires finite close, high, and low inputs")
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _json_number(value: Any) -> float | int | bool | None:
|
||||||
|
value = value.item() if isinstance(value, np.generic) else value
|
||||||
|
if isinstance(value, (bool, np.bool_)):
|
||||||
|
return bool(value)
|
||||||
|
if isinstance(value, (int, np.integer)):
|
||||||
|
return int(value)
|
||||||
|
value = float(value)
|
||||||
|
return value if np.isfinite(value) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _first_difference(expected: np.ndarray, actual: np.ndarray) -> dict[str, Any] | None:
|
||||||
|
equal = np.equal(expected, actual) | (np.isnan(expected) & np.isnan(actual))
|
||||||
|
indices = np.flatnonzero(~equal)
|
||||||
|
if not len(indices):
|
||||||
|
return None
|
||||||
|
index = int(indices[0])
|
||||||
|
return {"bar": index, "cpu": _json_number(expected[index]), "gpu": _json_number(actual[index])}
|
||||||
|
|
||||||
|
|
||||||
|
def _ulp(expected: np.ndarray, actual: np.ndarray) -> np.ndarray:
|
||||||
|
expected_bits, actual_bits = expected.view(np.uint64), actual.view(np.uint64)
|
||||||
|
sign = np.uint64(1 << 63)
|
||||||
|
expected_ordered = np.where(expected_bits >> 63 != 0, ~expected_bits, expected_bits | sign)
|
||||||
|
actual_ordered = np.where(actual_bits >> 63 != 0, ~actual_bits, actual_bits | sign)
|
||||||
|
return np.asarray(np.abs(expected_ordered.astype(object) - actual_ordered.astype(object)), dtype=np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
def _numeric_stats(expected: np.ndarray, actual: np.ndarray) -> dict[str, Any]:
|
||||||
|
finite = np.isfinite(expected) & np.isfinite(actual)
|
||||||
|
delta = np.abs(actual[finite] - expected[finite])
|
||||||
|
nonzero_expected = np.abs(expected[finite]) != 0
|
||||||
|
relative = delta[nonzero_expected] / np.abs(expected[finite][nonzero_expected])
|
||||||
|
zero_reference_divergence = bool(np.any((expected[finite] == 0) & (actual[finite] != 0)))
|
||||||
|
return {
|
||||||
|
"first_divergence": _first_difference(expected, actual),
|
||||||
|
"finite_pairs": int(finite.sum()),
|
||||||
|
"nonfinite_mismatches": int(np.count_nonzero(~(np.equal(expected, actual) | (np.isnan(expected) & np.isnan(actual))) & ~finite)),
|
||||||
|
"max_abs": float(delta.max()) if delta.size else 0.0,
|
||||||
|
"max_rel": float(relative.max()) if relative.size else 0.0,
|
||||||
|
"max_rel_unbounded_at_zero_cpu": zero_reference_divergence,
|
||||||
|
"max_ulp": float(_ulp(expected[finite], actual[finite]).max()) if delta.size else 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _exact_stats(expected: np.ndarray, actual: np.ndarray) -> dict[str, Any]:
|
||||||
|
return {"exact": _first_difference(expected, actual) is None, "first_divergence": _first_difference(expected, actual)}
|
||||||
|
|
||||||
|
|
||||||
|
def _diagnose_one(ohlcv: Mapping[str, np.ndarray], multiplier: float, device: torch.device) -> dict[str, Any]:
|
||||||
|
tensors = {name: torch.as_tensor(ohlcv[name], dtype=torch.float64, device=device) for name in ("close", "high", "low")}
|
||||||
|
cpu = cpu_supertrend_trace(ohlcv["close"], ohlcv["high"], ohlcv["low"], PERIOD, multiplier)
|
||||||
|
gpu_trace = supertrend_trace(tensors["close"], tensors["high"], tensors["low"], PERIOD, multiplier)
|
||||||
|
if device.type == "cuda":
|
||||||
|
torch.cuda.synchronize(device)
|
||||||
|
gpu = {name: value.detach().cpu().numpy() for name, value in gpu_trace.items()}
|
||||||
|
missing_cpu = sorted(SUPERTREND_TRACE_KEYS - cpu.keys())
|
||||||
|
missing_gpu = sorted(SUPERTREND_TRACE_KEYS - gpu.keys())
|
||||||
|
if missing_cpu or missing_gpu:
|
||||||
|
raise RuntimeError(f"incomplete V1.2 Supertrend trace: CPU missing {missing_cpu}; GPU missing {missing_gpu}")
|
||||||
|
return {
|
||||||
|
"period": PERIOD,
|
||||||
|
"multiplier": multiplier,
|
||||||
|
"numeric": {name: _numeric_stats(cpu[name], gpu[name]) for name in NUMERIC_FIELDS},
|
||||||
|
"branch_predicates": {name: _exact_stats(cpu[name], gpu[name]) for name in sorted(PREDICATE_KEYS)},
|
||||||
|
"direction": _exact_stats(cpu["direction"], gpu["direction"]),
|
||||||
|
"transitions": {"direction_transition": _exact_stats(cpu["direction_transition"], gpu["direction_transition"])},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def diagnose(ohlcv: Mapping[str, np.ndarray], periods: list[int] | None = None, multiplier: float | None = None, device: torch.device | None = None) -> dict[str, Any]:
|
||||||
|
"""Diagnose one corpus; V1.2 semantics are fixed to period 10 and 2/3/4 multipliers."""
|
||||||
|
if periods not in (None, [PERIOD]) or multiplier not in (None, *MULTIPLIERS):
|
||||||
|
raise ValueError("V1.2 semantic diagnostic supports only period 10 and multipliers 2, 3, and 4")
|
||||||
|
if device is None:
|
||||||
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
|
return {"artifact": ARTIFACT, "schema_version": "1.2", "device": str(device), "bars": len(ohlcv["close"]), "period": PERIOD, "records": [_diagnose_one(ohlcv, value, device) for value in MULTIPLIERS]}
|
||||||
|
|
||||||
|
|
||||||
|
def run(calibration_csv: Path, holdout_csv: Path, *, adversarial_length: int = 256, seed: int = 0, device: torch.device | None = None) -> dict[str, Any]:
|
||||||
|
"""Produce calibration, holdout, and deterministic adversarial diagnostic corpora."""
|
||||||
|
if adversarial_length < PERIOD + 1:
|
||||||
|
raise ValueError("adversarial length must be at least 11")
|
||||||
|
if device is None:
|
||||||
|
if not torch.cuda.is_available():
|
||||||
|
raise RuntimeError("CUDA GPU is required; pass device=torch.device('cpu') only for tests")
|
||||||
|
device = torch.device("cuda")
|
||||||
|
return {"artifact": ARTIFACT, "schema_version": "1.2", "device": str(device), "adversarial": {"generator": "deterministic_adversarial_ohlcv", "length": adversarial_length, "seed": seed}, "corpora": {"calibration": diagnose(_read_ohlcv(calibration_csv), device=device), "holdout": diagnose(_read_ohlcv(holdout_csv), device=device), "adversarial": diagnose(deterministic_adversarial_ohlcv(length=adversarial_length, seed=seed), device=device)}}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--calibration-csv", type=Path, required=True)
|
||||||
|
parser.add_argument("--holdout-csv", type=Path, required=True)
|
||||||
|
parser.add_argument("--adversarial-length", type=int, default=256)
|
||||||
|
parser.add_argument("--seed", type=int, default=0)
|
||||||
|
parser.add_argument("--output", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
payload = run(args.calibration_csv, args.holdout_csv, adversarial_length=args.adversarial_length, seed=args.seed)
|
||||||
|
encoded = json.dumps(payload, sort_keys=True, allow_nan=False) + "\n"
|
||||||
|
if args.output:
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_text(encoded, encoding="utf-8")
|
||||||
|
else:
|
||||||
|
print(encoded, end="")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
32
hyperscalper_candidate_taxonomy_v1.json
Normal file
32
hyperscalper_candidate_taxonomy_v1.json
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"artifact": "HYPERSCALPER_CANDIDATE_TAXONOMY_V1",
|
||||||
|
"purpose": "Static, non-executable candidate taxonomy for feature-gap triage. Entries are hypotheses, not strategies or approvals.",
|
||||||
|
"evidence_labels": ["source_mapped", "historical_usage", "lineage_bias", "qualification_failure", "external_candidate", "requires_oracle", "requires_deq"],
|
||||||
|
"candidate_registry": [
|
||||||
|
{"candidate_id": "spot_perp_basis", "market": "crypto", "venue_scope": "spot_perpetual", "family": "cross_market_basis", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "perp_funding_pressure", "market": "crypto", "venue_scope": "perpetual", "family": "derivatives_positioning", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "options_implied_realized_vol", "market": "crypto", "venue_scope": "options_spot", "family": "cross_market_volatility", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "exchange_orderbook_imbalance", "market": "crypto", "venue_scope": "single_exchange", "family": "microstructure", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "cross_exchange_dislocation", "market": "crypto", "venue_scope": "multi_exchange", "family": "cross_venue_execution", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "stablecoin_flow_stress", "market": "crypto", "venue_scope": "onchain_cex", "family": "liquidity_flow", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "equity_index_futures_cash_basis", "market": "equity", "venue_scope": "cash_futures", "family": "cross_market_basis", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "etf_constituent_dispersion", "market": "equity", "venue_scope": "etf_cash", "family": "cross_asset_dispersion", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "adr_local_listing_spread", "market": "equity", "venue_scope": "cross_listing", "family": "cross_venue_basis", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "index_option_skew", "market": "equity", "venue_scope": "options_cash", "family": "implied_distribution", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "treasury_curve_slope", "market": "rates", "venue_scope": "cash_futures", "family": "curve_relative_value", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "swap_spread", "market": "rates", "venue_scope": "otc_futures", "family": "cross_market_basis", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "fx_spot_forward_carry", "market": "fx", "venue_scope": "spot_forwards", "family": "carry_basis", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "fx_cross_pair_relative_strength", "market": "fx", "venue_scope": "cross_pair", "family": "cross_asset_momentum", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "commodity_calendar_spread", "market": "commodity", "venue_scope": "futures_curve", "family": "term_structure", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "commodity_crack_or_crush_spread", "market": "commodity", "venue_scope": "intercommodity", "family": "processing_margin", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "volatility_term_structure", "market": "volatility", "venue_scope": "spot_futures_options", "family": "term_structure", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]},
|
||||||
|
{"candidate_id": "cross_asset_risk_regime", "market": "multi_asset", "venue_scope": "equity_rates_fx_commodity_crypto", "family": "regime", "evidence": ["external_candidate", "requires_oracle", "requires_deq"]}
|
||||||
|
],
|
||||||
|
"expansion_waves": [
|
||||||
|
{"wave": 0, "name": "evidence_reconciliation", "admission_evidence": ["source_mapped", "historical_usage", "lineage_bias"], "not_a_strategy": true},
|
||||||
|
{"wave": 1, "name": "oracle_redundancy_gaps", "admission_evidence": ["requires_oracle", "qualification_failure"], "not_a_strategy": true},
|
||||||
|
{"wave": 2, "name": "cross_market_data_contracts", "admission_evidence": ["external_candidate", "requires_oracle"], "not_a_strategy": true},
|
||||||
|
{"wave": 3, "name": "decision_equivalence_gate", "admission_evidence": ["requires_deq"], "not_a_strategy": true}
|
||||||
|
]
|
||||||
|
}
|
||||||
86862
hyperscalper_feature_gap_analysis_v1.json
Normal file
86862
hyperscalper_feature_gap_analysis_v1.json
Normal file
File diff suppressed because it is too large
Load diff
11
hyperscalper_feature_gap_analysis_v1.md
Normal file
11
hyperscalper_feature_gap_analysis_v1.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# HYPERSCALPER Feature Gap Analysis V1
|
||||||
|
|
||||||
|
Read-only: yes. Strategy execution: not performed.
|
||||||
|
|
||||||
|
- Information map: 711/711 features
|
||||||
|
- Acceptance: not_proven
|
||||||
|
- Redundancy clusters: 39
|
||||||
|
- DEQ: AVAILABLE (supplied Cohort001 ledger; multi-feature attribution is associative, not causal)
|
||||||
|
- Blockers: 0
|
||||||
|
|
||||||
|
## Blockers
|
||||||
1096
hyperscalper_feature_gap_analysis_v1.py
Normal file
1096
hyperscalper_feature_gap_analysis_v1.py
Normal file
File diff suppressed because it is too large
Load diff
310
hyperscalper_wave1_feature_synthesis_v1.py
Normal file
310
hyperscalper_wave1_feature_synthesis_v1.py
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Synthesize evidence-backed, OHLCV-only Wave 1 feature candidates.
|
||||||
|
|
||||||
|
This is a read-only reporting tool. It does not evaluate formulas, search
|
||||||
|
strategies, infer data provenance, or turn associative evidence into causality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
ARTIFACT = "HYPERSCALPER_WAVE1_FEATURE_SYNTHESIS_V1"
|
||||||
|
MIN_CANDIDATES = 20
|
||||||
|
MAX_CANDIDATES = 50
|
||||||
|
ASSOCIATIVE_LIMITATION = (
|
||||||
|
"Evidence is observational and multi-feature attribution is associative, not causal."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(path: Path) -> dict[str, Any]:
|
||||||
|
value = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValueError(f"{path} must contain a JSON object")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def source_record(path: Path | None) -> dict[str, Any]:
|
||||||
|
if path is None:
|
||||||
|
return {"status": "UNAVAILABLE", "reason": "not supplied"}
|
||||||
|
return {
|
||||||
|
"status": "AVAILABLE",
|
||||||
|
"path": str(path),
|
||||||
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rows(value: dict[str, Any], *names: str) -> list[dict[str, Any]]:
|
||||||
|
for name in names:
|
||||||
|
found = value.get(name)
|
||||||
|
if isinstance(found, list):
|
||||||
|
return [item for item in found if isinstance(item, dict)]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def feature_key(row: dict[str, Any]) -> str | None:
|
||||||
|
value = row.get("feature_key")
|
||||||
|
if value is not None:
|
||||||
|
return str(value)
|
||||||
|
try:
|
||||||
|
return f"{int(row['indicator_id'])}:{int(row['period'])}:{float(row['p1']):g}"
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_ohlcv_only(row: dict[str, Any]) -> bool:
|
||||||
|
"""Accept only an explicit, finite OHLCV dependency declaration."""
|
||||||
|
scope = str(row.get("input_scope", row.get("data_scope", ""))).upper()
|
||||||
|
if scope in {"OHLCV", "OHLCV_ONLY"}:
|
||||||
|
return True
|
||||||
|
dependencies = row.get("data_dependencies", row.get("inputs"))
|
||||||
|
if not isinstance(dependencies, list) or not dependencies:
|
||||||
|
return False
|
||||||
|
return {str(item).lower() for item in dependencies} <= {
|
||||||
|
"open",
|
||||||
|
"high",
|
||||||
|
"low",
|
||||||
|
"close",
|
||||||
|
"volume",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def numeric(value: Any) -> float:
|
||||||
|
return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def failure_evidence(payload: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
|
||||||
|
result: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||||
|
for row in rows(payload, "feature_failure_comparisons", "features", "comparisons"):
|
||||||
|
key = feature_key(row)
|
||||||
|
if key:
|
||||||
|
result[key].append(row)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def deq_evidence(payload: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||||
|
deq = payload.get("decision_equivalence", payload)
|
||||||
|
if not isinstance(deq, dict):
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
str(row["feature_key"]): row
|
||||||
|
for row in rows(deq, "feature_summaries", "features")
|
||||||
|
if row.get("feature_key") is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def redundancy_evidence(payload: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||||
|
result: dict[str, dict[str, Any]] = {}
|
||||||
|
for cluster in rows(payload.get("redundancy", payload), "clusters"):
|
||||||
|
for key in cluster.get("members", []):
|
||||||
|
result[str(key)] = {
|
||||||
|
"cluster_id": cluster.get("cluster_id"),
|
||||||
|
"cluster_size": cluster.get("size"),
|
||||||
|
"representative_feature_key": cluster.get("representative_feature_key"),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def synthesis(
|
||||||
|
semantic: dict[str, Any],
|
||||||
|
redundancy: dict[str, Any],
|
||||||
|
failure: dict[str, Any] | None,
|
||||||
|
deq: dict[str, Any],
|
||||||
|
lineage_gap: dict[str, Any],
|
||||||
|
) -> tuple[list[dict[str, Any]], list[str], int]:
|
||||||
|
feature_rows = rows(semantic, "features", "primitives", "rows", "semantic_map")
|
||||||
|
if not feature_rows:
|
||||||
|
feature_rows = rows(lineage_gap.get("information_map", {}), "features")
|
||||||
|
failures = failure_evidence(failure) if failure else {}
|
||||||
|
deq_by_feature = deq_evidence(deq)
|
||||||
|
clusters = redundancy_evidence(redundancy)
|
||||||
|
lineage_by_feature = {
|
||||||
|
str(row.get("feature_key")): row
|
||||||
|
for row in rows(lineage_gap.get("information_map", {}), "features")
|
||||||
|
if row.get("feature_key") is not None
|
||||||
|
}
|
||||||
|
excluded_non_ohlcv = 0
|
||||||
|
candidates = []
|
||||||
|
for row in feature_rows:
|
||||||
|
key = feature_key(row)
|
||||||
|
if key is None:
|
||||||
|
continue
|
||||||
|
if not is_ohlcv_only(row):
|
||||||
|
excluded_non_ohlcv += 1
|
||||||
|
continue
|
||||||
|
lineage = lineage_by_feature.get(key, row)
|
||||||
|
feature_failures = failures.get(key, [])
|
||||||
|
candidates.append(
|
||||||
|
{
|
||||||
|
"candidate_id": f"ohlcv:{key}",
|
||||||
|
"feature_key": key,
|
||||||
|
"indicator_id": row.get("indicator_id"),
|
||||||
|
"period": row.get("period"),
|
||||||
|
"p1": row.get("p1"),
|
||||||
|
"domain": row.get("domain", row.get("semantic_domain", "unclassified")),
|
||||||
|
"output_type": row.get("output_type", row.get("semantic_type", "unclassified")),
|
||||||
|
"input_scope": "OHLCV_ONLY",
|
||||||
|
"evidence": {
|
||||||
|
"semantic": {"feature_key": key, "explicit_ohlcv_only": True},
|
||||||
|
"lineage_gap": {
|
||||||
|
"historical_usage": lineage.get("historical_usage"),
|
||||||
|
"lineage_mentions": lineage.get("lineage_mentions"),
|
||||||
|
"max_redundancy": lineage.get("max_redundancy"),
|
||||||
|
"redundancy_status": lineage.get("redundancy_status"),
|
||||||
|
},
|
||||||
|
"redundancy_cluster": clusters.get(key),
|
||||||
|
"failure_mining": feature_failures or None,
|
||||||
|
"decision_equivalence": deq_by_feature.get(key),
|
||||||
|
},
|
||||||
|
"limitations": [
|
||||||
|
ASSOCIATIVE_LIMITATION,
|
||||||
|
"Not a strategy approval or causal feature claim.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Evidence ordering only: failure contrast, DEQ observations, lineage use, then redundancy.
|
||||||
|
def rank(candidate: dict[str, Any]) -> tuple[float, float, float, float, str]:
|
||||||
|
failure_rows = candidate["evidence"]["failure_mining"] or []
|
||||||
|
failure_strength = max(
|
||||||
|
(numeric(item.get("ks_statistic")) for item in failure_rows), default=0.0
|
||||||
|
)
|
||||||
|
deq_row = candidate["evidence"]["decision_equivalence"] or {}
|
||||||
|
deq_support = numeric(deq_row.get("trade_count"))
|
||||||
|
lineage = candidate["evidence"]["lineage_gap"]
|
||||||
|
usage = numeric(lineage.get("historical_usage")) + numeric(lineage.get("lineage_mentions"))
|
||||||
|
redundancy_value = lineage.get("max_redundancy")
|
||||||
|
redundancy = numeric(redundancy_value) if redundancy_value is not None else 1.0
|
||||||
|
return (-failure_strength, -deq_support, -usage, redundancy, candidate["feature_key"])
|
||||||
|
|
||||||
|
return sorted(candidates, key=rank), [], excluded_non_ohlcv
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--semantic-summary", type=Path, required=True)
|
||||||
|
parser.add_argument("--redundancy-clusters", type=Path, required=True)
|
||||||
|
parser.add_argument("--deq-summary", type=Path, required=True)
|
||||||
|
parser.add_argument("--lineage-gap-analysis", type=Path, required=True)
|
||||||
|
parser.add_argument(
|
||||||
|
"--failure-mining", type=Path, help="Copied Spark failure-mining JSON when available"
|
||||||
|
)
|
||||||
|
parser.add_argument("--output-dir", type=Path, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
required = (
|
||||||
|
args.semantic_summary,
|
||||||
|
args.redundancy_clusters,
|
||||||
|
args.deq_summary,
|
||||||
|
args.lineage_gap_analysis,
|
||||||
|
)
|
||||||
|
if any(not path.is_file() for path in required) or (
|
||||||
|
args.failure_mining and not args.failure_mining.is_file()
|
||||||
|
):
|
||||||
|
parser.error("all supplied artifact paths must be files")
|
||||||
|
|
||||||
|
semantic, redundancy, deq, lineage = (load_json(path) for path in required)
|
||||||
|
failure = load_json(args.failure_mining) if args.failure_mining else None
|
||||||
|
candidates, blockers, excluded_non_ohlcv = synthesis(
|
||||||
|
semantic, redundancy, failure, deq, lineage
|
||||||
|
)
|
||||||
|
selected = candidates[:MAX_CANDIDATES]
|
||||||
|
if args.failure_mining is None:
|
||||||
|
blockers.append(
|
||||||
|
"Spark failure-mining result is unavailable; copy its JSON before relying on "
|
||||||
|
"failure evidence."
|
||||||
|
)
|
||||||
|
if len(selected) < MIN_CANDIDATES:
|
||||||
|
blockers.append(
|
||||||
|
f"Only {len(selected)} explicitly OHLCV-only candidates are evidenced; refusing to "
|
||||||
|
f"invent the required {MIN_CANDIDATES}-{MAX_CANDIDATES} selection."
|
||||||
|
)
|
||||||
|
status = "READY" if not blockers else "BLOCKED"
|
||||||
|
sources = {
|
||||||
|
"semantic_summary": source_record(args.semantic_summary),
|
||||||
|
"redundancy_clusters": source_record(args.redundancy_clusters),
|
||||||
|
"failure_mining": source_record(args.failure_mining),
|
||||||
|
"deq_summary": source_record(args.deq_summary),
|
||||||
|
"lineage_gap_analysis": source_record(args.lineage_gap_analysis),
|
||||||
|
}
|
||||||
|
common = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"artifact": ARTIFACT,
|
||||||
|
"read_only": True,
|
||||||
|
"strategy_search": "not performed",
|
||||||
|
"associative_limitations": ASSOCIATIVE_LIMITATION,
|
||||||
|
"status": status,
|
||||||
|
"sources": sources,
|
||||||
|
"blockers": blockers,
|
||||||
|
}
|
||||||
|
taxonomy = {
|
||||||
|
**common,
|
||||||
|
"artifact": "FEATURE_GAP_TAXONOMY_V1",
|
||||||
|
"candidates": selected,
|
||||||
|
"excluded_without_explicit_ohlcv_only_declaration": excluded_non_ohlcv,
|
||||||
|
}
|
||||||
|
plan = {
|
||||||
|
**common,
|
||||||
|
"artifact": "FEATURE_EXPANSION_PLAN_V1",
|
||||||
|
"wave": 1,
|
||||||
|
"admission_rule": "Explicit OHLCV-only declaration and evidence-ranked source records.",
|
||||||
|
"candidate_count": len(selected),
|
||||||
|
"candidates": [item["candidate_id"] for item in selected],
|
||||||
|
}
|
||||||
|
registry = {**common, "artifact": "NEW_FEATURE_CANDIDATE_REGISTRY_V1", "candidates": selected}
|
||||||
|
spec = {
|
||||||
|
**common,
|
||||||
|
"artifact": "HYPERSCALPER_WAVE1_FEATURE_SPEC_V1",
|
||||||
|
"features": selected,
|
||||||
|
"implementation_constraint": (
|
||||||
|
"Implement features from OHLCV inputs only; no strategy search or causal claims."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
outputs = {
|
||||||
|
"feature_gap_taxonomy_v1.json": taxonomy,
|
||||||
|
"feature_expansion_plan_v1.json": plan,
|
||||||
|
"new_feature_candidate_registry_v1.json": registry,
|
||||||
|
"hyperscalper_wave1_feature_spec_v1.json": spec,
|
||||||
|
}
|
||||||
|
for name, payload in outputs.items():
|
||||||
|
(args.output_dir / name).write_text(
|
||||||
|
json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
markdown = (
|
||||||
|
"# HYPERSCALPER Wave 1 Feature Spec V1\n\n"
|
||||||
|
f"Status: {status}. Read-only: yes. Strategy search: not performed.\n\n"
|
||||||
|
f"Selected candidates: {len(selected)}. Explicitly excluded without OHLCV-only evidence: "
|
||||||
|
f"{excluded_non_ohlcv}.\n\n"
|
||||||
|
"## Limitations\n\n"
|
||||||
|
f"- {ASSOCIATIVE_LIMITATION}\n"
|
||||||
|
"- No feature is a strategy, approval, or causal claim.\n\n"
|
||||||
|
"## Candidates\n\n"
|
||||||
|
+ "\n".join(
|
||||||
|
f"- `{item['candidate_id']}`: {item['domain']} / {item['output_type']}"
|
||||||
|
for item in selected
|
||||||
|
)
|
||||||
|
+ "\n\n## Blockers\n\n"
|
||||||
|
+ "\n".join(f"- {item}" for item in blockers)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
(args.output_dir / "hyperscalper_wave1_feature_spec_v1.md").write_text(
|
||||||
|
markdown, encoding="utf-8"
|
||||||
|
)
|
||||||
|
executive = (
|
||||||
|
f"Wave 1 synthesis is {status}: {len(selected)} evidence-ranked, explicitly OHLCV-only "
|
||||||
|
"candidates. "
|
||||||
|
f"Failure mining is {'available' if failure else 'unavailable'}. {ASSOCIATIVE_LIMITATION}\n"
|
||||||
|
)
|
||||||
|
(args.output_dir / "hyperscalper_wave1_executive_summary_v1.md").write_text(
|
||||||
|
executive, encoding="utf-8"
|
||||||
|
)
|
||||||
|
print(args.output_dir / "feature_gap_taxonomy_v1.json")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
17
run_gpu_feature_batch01_v1.sh
Normal file
17
run_gpu_feature_batch01_v1.sh
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Usage: ./run_gpu_feature_batch01_v1.sh [cache-directory]
|
||||||
|
# The source mounts are read-only; only the cache directory is writable.
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CACHE_DIR="${1:-${ROOT}/.gpu-feature-cache}"
|
||||||
|
mkdir -p "${CACHE_DIR}"
|
||||||
|
|
||||||
|
docker build -f "${ROOT}/Dockerfile.gpu-feature-v1" -t artifex-gpu-feature-v1 "${ROOT}"
|
||||||
|
docker run --rm --gpus all \
|
||||||
|
--mount "type=bind,src=${ROOT}/hs22_oracle_v1,dst=/data,readonly" \
|
||||||
|
--mount "type=bind,src=${ROOT},dst=/oracle,readonly" \
|
||||||
|
--mount "type=bind,src=${CACHE_DIR},dst=/cache" \
|
||||||
|
artifex-gpu-feature-v1 --mode parity --report /cache/batch01_gpu_parity_report.json
|
||||||
|
|
||||||
|
echo "Wrote ${CACHE_DIR}/batch01_gpu_parity_report.json"
|
||||||
230
scripts/generate_historical_feature_semantic_domain_map_v1.py
Normal file
230
scripts/generate_historical_feature_semantic_domain_map_v1.py
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
"""Generate a read-only semantic map for the 711 historical primitives.
|
||||||
|
|
||||||
|
The generator consumes engineering/source metadata only. It never imports or
|
||||||
|
evaluates a historical formula, and optional checkpoints are inspected solely
|
||||||
|
to count already-materialized finite observations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pyarrow as pa
|
||||||
|
import pyarrow.parquet as pq
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED_PRIMITIVES = 711
|
||||||
|
|
||||||
|
|
||||||
|
def digest(path: Path) -> str:
|
||||||
|
if path.is_file():
|
||||||
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
for item in sorted(path.glob("*.npy")):
|
||||||
|
hasher.update(item.name.encode("utf-8"))
|
||||||
|
hasher.update(b"\0")
|
||||||
|
hasher.update(bytes.fromhex(digest(item)))
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def key_for(row: dict[str, Any]) -> tuple[int, int, float]:
|
||||||
|
return int(row["indicator_id"]), int(row["period"]), float(row["p1"])
|
||||||
|
|
||||||
|
|
||||||
|
def key_text(key: tuple[int, int, float]) -> str:
|
||||||
|
return f"{key[0]}:{key[1]}:{key[2]:g}"
|
||||||
|
|
||||||
|
|
||||||
|
def composite_key(value: Any) -> tuple[int, int, float]:
|
||||||
|
"""Decode canonical primitive keys stored as strings or Parquet structs."""
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return key_for(value)
|
||||||
|
if isinstance(value, (list, tuple)) and len(value) == 3:
|
||||||
|
return int(value[0]), int(value[1]), float(value[2])
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise ValueError(f"invalid composite primitive key: {value!r}")
|
||||||
|
parts = value.strip().split(":")
|
||||||
|
if len(parts) != 3:
|
||||||
|
parts = value.strip().split("_")
|
||||||
|
if len(parts) == 4 and parts[0] == "hs22":
|
||||||
|
parts = parts[1:]
|
||||||
|
if len(parts) != 3:
|
||||||
|
raise ValueError(f"invalid composite primitive key: {value!r}")
|
||||||
|
try:
|
||||||
|
return int(parts[0]), int(parts[1]), float(parts[2])
|
||||||
|
except ValueError as error:
|
||||||
|
raise ValueError(f"invalid composite primitive key: {value!r}") from error
|
||||||
|
|
||||||
|
|
||||||
|
def source_name(row: dict[str, Any]) -> str:
|
||||||
|
return str(row.get("source_function", "")).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def semantic_mapping(row: dict[str, Any]) -> tuple[str, str, str, str, str]:
|
||||||
|
"""Return domain, subdomain, type, provenance, and deterministic rule id."""
|
||||||
|
name = source_name(row)
|
||||||
|
family = str(row.get("engineering_family", ""))
|
||||||
|
rules = (
|
||||||
|
(("cross_", "crossover", "crossunder", "breakout", "divergence"), "price_action", "crossing_or_breakout", "event", "source_derived", "explicit_event_name"),
|
||||||
|
(("regime_", "entropy_", "chop", "fractal_dimension"), "market_regime", "regime_detection", "state", "source_derived", "explicit_regime_name"),
|
||||||
|
(("time_", "session", "weekday", "hour"), "time_context", "session_or_calendar", "state", "source_derived", "explicit_time_name"),
|
||||||
|
(("micro_", "orderflow", "vwap", "obv", "mfi", "cmf", "volume"), "volume_and_flow", "volume_flow", "continuous", "source_derived", "explicit_volume_name"),
|
||||||
|
(("osc_", "rsi", "stoch", "cci", "williams", "roc", "momentum"), "momentum", "oscillator_or_rate", "continuous", "source_derived", "explicit_momentum_name"),
|
||||||
|
(("bb_", "kelt_", "donch_", "ichimoku_", "supertrend", "psar", "channel"), "volatility", "bands_and_channels", "state", "source_derived", "explicit_band_name"),
|
||||||
|
(("vol_", "atr", "true_range", "variance", "std", "range"), "volatility", "range_and_dispersion", "continuous", "source_derived", "explicit_volatility_name"),
|
||||||
|
(("ma_", "ema", "sma", "wma", "tema", "dema"), "trend", "moving_average", "continuous", "source_derived", "explicit_average_name"),
|
||||||
|
(("adx", "aroon_", "linreg_", "trend_"), "trend", "direction_and_strength", "continuous", "source_derived", "explicit_trend_name"),
|
||||||
|
(("pivot_", "prev_", "rolling_", "quantile_"), "price_structure", "reference_level", "state", "source_derived", "explicit_reference_name"),
|
||||||
|
(("norm_", "zscore", "percentile"), "normalization", "scaled_price_or_signal", "continuous", "source_derived", "explicit_normalization_name"),
|
||||||
|
)
|
||||||
|
for prefixes, domain, subdomain, value_type, provenance, rule in rules:
|
||||||
|
if any(prefix in name for prefix in prefixes):
|
||||||
|
return domain, subdomain, value_type, provenance, rule
|
||||||
|
family_fallbacks = {
|
||||||
|
"moving_average": ("trend", "moving_average", "continuous"),
|
||||||
|
"oscillator": ("momentum", "oscillator_or_rate", "continuous"),
|
||||||
|
"volatility": ("volatility", "range_and_dispersion", "continuous"),
|
||||||
|
"regime": ("market_regime", "regime_detection", "state"),
|
||||||
|
"microstructure": ("market_microstructure", "price_volume_structure", "continuous"),
|
||||||
|
"momentum": ("momentum", "oscillator_or_rate", "continuous"),
|
||||||
|
"trend_structure": ("trend", "direction_and_strength", "continuous"),
|
||||||
|
"normalization": ("normalization", "scaled_price_or_signal", "continuous"),
|
||||||
|
"cross_indicator": ("price_action", "crossing_or_breakout", "event"),
|
||||||
|
"time_session": ("time_context", "session_or_calendar", "state"),
|
||||||
|
"reference_level": ("price_structure", "reference_level", "state"),
|
||||||
|
"band_channel": ("volatility", "bands_and_channels", "state"),
|
||||||
|
}
|
||||||
|
domain, subdomain, value_type = family_fallbacks.get(
|
||||||
|
family, ("specialized", "unclassified_source_function", "continuous")
|
||||||
|
)
|
||||||
|
return domain, subdomain, value_type, "heuristic", "engineering_family_fallback"
|
||||||
|
|
||||||
|
|
||||||
|
def load_engineering_map(path: Path) -> list[dict[str, Any]]:
|
||||||
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
if payload.get("artifact") != "ENGINEERING_FAMILY_MAP_V1":
|
||||||
|
raise ValueError("--engineering-map is not ENGINEERING_FAMILY_MAP_V1")
|
||||||
|
rows = payload.get("primitives")
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
raise ValueError("engineering map has no primitives list")
|
||||||
|
keys = [key_for(row) for row in rows]
|
||||||
|
if len(rows) != EXPECTED_PRIMITIVES or len(set(keys)) != EXPECTED_PRIMITIVES:
|
||||||
|
raise ValueError("engineering map must contain exactly 711 unique primitives")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def usage_counts(path: Path, expected_indicator_ids: set[int]) -> dict[int, int]:
|
||||||
|
"""Load indicator-level usage; it cannot distinguish parameterized primitives."""
|
||||||
|
counts: dict[int, int] = {}
|
||||||
|
source = pq.ParquetFile(path)
|
||||||
|
columns = set(source.schema.names)
|
||||||
|
required = {"entity_type", "indicator_id", "usage_count"}
|
||||||
|
if missing := required - columns:
|
||||||
|
raise ValueError(f"usage parquet is missing required columns: {', '.join(sorted(missing))}")
|
||||||
|
for batch in source.iter_batches(columns=["entity_type", "indicator_id", "usage_count"]):
|
||||||
|
for row in batch.to_pylist():
|
||||||
|
if row["entity_type"] != "indicator_id":
|
||||||
|
continue
|
||||||
|
indicator_id = int(row["indicator_id"])
|
||||||
|
if indicator_id not in expected_indicator_ids:
|
||||||
|
continue
|
||||||
|
if indicator_id in counts:
|
||||||
|
raise ValueError(f"usage parquet has duplicate indicator row: {indicator_id}")
|
||||||
|
counts[indicator_id] = int(row["usage_count"])
|
||||||
|
if set(counts) != expected_indicator_ids:
|
||||||
|
raise ValueError("usage parquet does not cover every engineering indicator ID")
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
def checkpoint_counts(directory: Path, expected: set[tuple[int, int, float]]) -> tuple[dict[tuple[int, int, float], int], dict[tuple[int, int, float], str]]:
|
||||||
|
counts: dict[tuple[int, int, float], int] = {}
|
||||||
|
names: dict[tuple[int, int, float], str] = {}
|
||||||
|
for path in sorted(directory.glob("*.npy")):
|
||||||
|
parts = path.stem.split("_")
|
||||||
|
if len(parts) != 4 or parts[0] != "hs22":
|
||||||
|
raise ValueError(f"invalid checkpoint filename: {path.name}")
|
||||||
|
try:
|
||||||
|
key = int(parts[1]), int(parts[2]), float(parts[3])
|
||||||
|
except ValueError as error:
|
||||||
|
raise ValueError(f"invalid checkpoint filename: {path.name}") from error
|
||||||
|
if key not in expected or key in counts:
|
||||||
|
raise ValueError(f"checkpoint is not a unique engineering primitive: {path.name}")
|
||||||
|
counts[key] = int(np.isfinite(np.load(path, allow_pickle=False)).sum())
|
||||||
|
names[key] = path.name
|
||||||
|
if set(counts) != expected:
|
||||||
|
raise ValueError("checkpoint directory does not cover every engineering primitive")
|
||||||
|
return counts, names
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--engineering-map", type=Path, required=True)
|
||||||
|
parser.add_argument("--usage-parquet", type=Path)
|
||||||
|
parser.add_argument("--checkpoint-dir", type=Path)
|
||||||
|
parser.add_argument("--output-dir", type=Path, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.usage_parquet and not args.usage_parquet.is_file():
|
||||||
|
parser.error("--usage-parquet must be a file")
|
||||||
|
if args.checkpoint_dir and not args.checkpoint_dir.is_dir():
|
||||||
|
parser.error("--checkpoint-dir must be a directory")
|
||||||
|
|
||||||
|
engineering = load_engineering_map(args.engineering_map)
|
||||||
|
expected = {key_for(row) for row in engineering}
|
||||||
|
expected_indicator_ids = {key[0] for key in expected}
|
||||||
|
usage = usage_counts(args.usage_parquet, expected_indicator_ids) if args.usage_parquet else {}
|
||||||
|
finite, checkpoint_names = checkpoint_counts(args.checkpoint_dir, expected) if args.checkpoint_dir else ({}, {})
|
||||||
|
rows = []
|
||||||
|
for engineering_row in engineering:
|
||||||
|
key = key_for(engineering_row)
|
||||||
|
domain, subdomain, value_type, provenance, rule = semantic_mapping(engineering_row)
|
||||||
|
rows.append({
|
||||||
|
"feature_key": key_text(key), "indicator_id": key[0], "period": key[1], "p1": key[2],
|
||||||
|
"source_function": engineering_row.get("source_function"),
|
||||||
|
"source_expression": engineering_row.get("source_expression"),
|
||||||
|
"engineering_family": engineering_row.get("engineering_family"),
|
||||||
|
"semantic_domain": domain, "semantic_subdomain": subdomain, "semantic_type": value_type,
|
||||||
|
"semantic_mapping_provenance": provenance, "semantic_mapping_rule": rule,
|
||||||
|
"usage_count": usage.get(key[0]),
|
||||||
|
"usage_grain": "indicator_id_replicated_not_parameter_specific" if args.usage_parquet else None,
|
||||||
|
"finite_observation_count": finite.get(key),
|
||||||
|
"checkpoint_file": checkpoint_names.get(key),
|
||||||
|
})
|
||||||
|
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
parquet_path = args.output_dir / "historical_feature_semantic_domain_map_v1.parquet"
|
||||||
|
pq.write_table(pa.Table.from_pylist(rows), parquet_path, compression="zstd")
|
||||||
|
sources: dict[str, dict[str, str]] = {"engineering_map": {"path": str(args.engineering_map), "sha256": digest(args.engineering_map)}}
|
||||||
|
if args.usage_parquet:
|
||||||
|
sources["usage_parquet"] = {"path": str(args.usage_parquet), "sha256": digest(args.usage_parquet)}
|
||||||
|
if args.checkpoint_dir:
|
||||||
|
sources["checkpoint_dir"] = {"path": str(args.checkpoint_dir), "sha256": digest(args.checkpoint_dir)}
|
||||||
|
summary = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"artifact": "HISTORICAL_FEATURE_SEMANTIC_DOMAIN_MAP_V1_SUMMARY",
|
||||||
|
"method": "read-only source-function semantic classification; explicit name rules are source-derived and family fallbacks are heuristic; no historical formulas were imported or evaluated",
|
||||||
|
"source_artifacts": sources,
|
||||||
|
"counts": {
|
||||||
|
"primitives": len(rows),
|
||||||
|
"semantic_domains": len({row["semantic_domain"] for row in rows}),
|
||||||
|
"semantic_subdomains": len({row["semantic_subdomain"] for row in rows}),
|
||||||
|
"source_derived_mappings": sum(row["semantic_mapping_provenance"] == "source_derived" for row in rows),
|
||||||
|
"heuristic_mappings": sum(row["semantic_mapping_provenance"] == "heuristic" for row in rows),
|
||||||
|
"usage_count_total_replicated_across_primitives": sum(row["usage_count"] or 0 for row in rows),
|
||||||
|
"finite_observation_count_total": sum(row["finite_observation_count"] or 0 for row in rows),
|
||||||
|
},
|
||||||
|
"semantic_type_counts": {kind: sum(row["semantic_type"] == kind for row in rows) for kind in ("continuous", "state", "event")},
|
||||||
|
"usage_grain": "indicator_id_replicated_not_parameter_specific" if args.usage_parquet else None,
|
||||||
|
"artifacts": {"semantic_domain_map_parquet": str(parquet_path), "semantic_domain_map_parquet_sha256": digest(parquet_path)},
|
||||||
|
}
|
||||||
|
(args.output_dir / "historical_feature_semantic_domain_map_v1_summary.json").write_text(
|
||||||
|
json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
supertrend_atr_numeric_drift_diagnostic_v1_2.json
Normal file
1
supertrend_atr_numeric_drift_diagnostic_v1_2.json
Normal file
File diff suppressed because one or more lines are too long
61
tests/test_cohort001_feature_failure_mining_v1.py
Normal file
61
tests/test_cohort001_feature_failure_mining_v1.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import cohort001_feature_failure_mining_v1 as runner
|
||||||
|
|
||||||
|
|
||||||
|
def test_entry_bar_uses_exported_column_then_raw_ledger_and_rejects_timestamps():
|
||||||
|
assert runner.entry_bar({"entry_bar": 4}) == (4, "column.entry_bar")
|
||||||
|
assert runner.entry_bar({"raw_ledger_json": '{"entry_index":3}'}) == (3, "raw_ledger_json.entry_index")
|
||||||
|
assert runner.entry_bar({"entry_at": "2026-01-01T00:00:00Z"}) == (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_compatible_normalizes_numpy_scalars_and_nonfinite_floats():
|
||||||
|
payload = runner.json_compatible({
|
||||||
|
"integer": np.int64(7),
|
||||||
|
"float": np.float32(1.25),
|
||||||
|
"boolean": np.bool_(True),
|
||||||
|
"nested": [np.float64(np.nan), np.float64(np.inf), float("-inf")],
|
||||||
|
})
|
||||||
|
|
||||||
|
assert payload == {
|
||||||
|
"integer": 7,
|
||||||
|
"float": pytest.approx(1.25),
|
||||||
|
"boolean": True,
|
||||||
|
"nested": [None, None, None],
|
||||||
|
}
|
||||||
|
assert json.loads(json.dumps(payload, allow_nan=False))["nested"] == [None, None, None]
|
||||||
|
|
||||||
|
|
||||||
|
def test_runner_mines_entry_values_and_skips_unmappable_rows(tmp_path, monkeypatch):
|
||||||
|
pa = pytest.importorskip("pyarrow")
|
||||||
|
pq = pytest.importorskip("pyarrow.parquet")
|
||||||
|
engineering = tmp_path / "engineering.json"
|
||||||
|
engineering.write_text(json.dumps({"primitives": [{"indicator_id": 1, "period": 10, "p1": 0}]}))
|
||||||
|
semantic = tmp_path / "semantic.parquet"
|
||||||
|
pq.write_table(pa.Table.from_pylist([{"feature_key": "1:10:0", "domain": "trend", "output_type": "continuous"}]), semantic)
|
||||||
|
checkpoints = tmp_path / "checkpoints"
|
||||||
|
checkpoints.mkdir()
|
||||||
|
np.save(checkpoints / "hs22_1_10_0.npy", np.array([1.0, 2.0, 8.0, 9.0]))
|
||||||
|
context = tmp_path / "context.parquet"
|
||||||
|
pq.write_table(pa.Table.from_pylist([
|
||||||
|
{"failure_label": "GOOD_ENTRY", "entry_bar": 0, "fold": "F1", "feature_triples_json": '[{"indicator_id":1,"period":10,"p1":0}]'},
|
||||||
|
{"failure_label": "GOOD_ENTRY", "entry_bar": 1, "fold": "F2", "feature_triples_json": '[{"indicator_id":1,"period":10,"p1":0}]'},
|
||||||
|
{"failure_label": "WRONG_DIRECTION", "entry_bar": 2, "fold": "F1", "feature_triples_json": '[{"indicator_id":1,"period":10,"p1":0}]'},
|
||||||
|
{"failure_label": "WRONG_DIRECTION", "entry_bar": 3, "fold": "F2", "feature_triples_json": '[{"indicator_id":1,"period":10,"p1":0}]'},
|
||||||
|
{"failure_label": "WRONG_DIRECTION", "entry_at": "not a bar", "feature_triples_json": '[]'},
|
||||||
|
]), context)
|
||||||
|
output = tmp_path / "output"
|
||||||
|
monkeypatch.setattr(sys, "argv", ["runner", "--failure-context", str(context), "--semantic-map", str(semantic), "--oracle-checkpoint-dir", str(checkpoints), "--engineering-map", str(engineering), "--output-dir", str(output)])
|
||||||
|
runner.main()
|
||||||
|
result = json.loads((output / "cohort001_feature_failure_mining_v1.json").read_text())
|
||||||
|
item = result["feature_failure_comparisons"][0]
|
||||||
|
assert item["median_difference_failure_minus_good"] == pytest.approx(7.0)
|
||||||
|
assert item["fold_consistency"] == 1.0
|
||||||
|
assert result["skipped"]["unmappable_entry_bar"] == 1
|
||||||
|
assert item["attribution"] == "ASSOCIATIVE_NOT_CAUSAL"
|
||||||
41
tests/test_export_cohort_failure_context_v1.py
Normal file
41
tests/test_export_cohort_failure_context_v1.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from control_plane.trading_studio.management.commands.export_cohort_failure_context_v1 import Command
|
||||||
|
|
||||||
|
|
||||||
|
def test_failure_labels_are_explicit_and_precedence_is_deterministic():
|
||||||
|
cases = [
|
||||||
|
({"net_pnl": -1}, {"return_1_bars_bps": 5}, "GOOD_SIGNAL_BAD_MONETIZATION"),
|
||||||
|
({"net_pnl": 1}, {"winner_negative_first": True}, "RECOVERY_DEPENDENT"),
|
||||||
|
({}, {"mae_bps": -25}, "HIGH_MAE_ENTRY"),
|
||||||
|
({}, {"return_1_bars_bps": -5}, "WRONG_DIRECTION"),
|
||||||
|
({}, {"return_1_bars_bps": 0, "return_5_bars_bps": 5}, "LATE_SIGNAL"),
|
||||||
|
({"net_pnl": 1}, {"return_1_bars_bps": 5}, "GOOD_ENTRY"),
|
||||||
|
({}, {}, "NO_EDGE"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for ledger, deq, expected in cases:
|
||||||
|
assert Command._label(ledger, deq)[0] == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_rows_preserve_raw_ledger_deq_genome_context_and_rescue():
|
||||||
|
payload = {
|
||||||
|
"cohort_id": "cohort", "dataset_version_id": "dataset", "dataset_sha256": "hash",
|
||||||
|
"strategies": [{
|
||||||
|
"ordinal": 1, "membership_id": "member", "strategy_version_id": "strategy",
|
||||||
|
"runner_name": "runner", "genome": {"combo": "g"}, "feature_triples": [],
|
||||||
|
"folds": [{"fold": "F1", "scenarios": [{
|
||||||
|
"qualification_run_id": "run", "scenario": "BASE",
|
||||||
|
"summary": {"rescue": {"recovered": True}},
|
||||||
|
"deq_samples": [{"identity": "trade", "entry_bar": 2, "entry_execution_price": 10,
|
||||||
|
"take_profit_price": 11, "context": {"regime": "x"},
|
||||||
|
"deq": {"return_1_bars_bps": 5}}],
|
||||||
|
}]}],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
row = Command._rows(payload)[0]
|
||||||
|
|
||||||
|
assert row["direction"] == "LONG"
|
||||||
|
assert '"return_1_bars_bps":5' in row["raw_deq_json"]
|
||||||
|
assert '"combo":"g"' in row["genome_json"]
|
||||||
|
assert '"regime":"x"' in row["context_json"]
|
||||||
|
assert '"recovered":true' in row["rescue_json"]
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytest.importorskip("pyarrow")
|
||||||
|
|
||||||
|
from scripts import generate_historical_feature_semantic_domain_map_v1 as generator
|
||||||
|
|
||||||
|
|
||||||
|
def engineering_rows() -> list[dict[str, object]]:
|
||||||
|
names = ["ma_fast", "cross_over", "regime_entropy", "pivot_high", "mystery"]
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"indicator_id": index,
|
||||||
|
"period": 10,
|
||||||
|
"p1": float(index % 2),
|
||||||
|
"source_function": names[index % len(names)],
|
||||||
|
"source_expression": names[index % len(names)] + "(close)",
|
||||||
|
"engineering_family": "specialized" if index % len(names) == 4 else "moving_average",
|
||||||
|
}
|
||||||
|
for index in range(711)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_generator_writes_711_semantic_rows_with_optional_evidence(tmp_path, monkeypatch):
|
||||||
|
import pyarrow as pa
|
||||||
|
import pyarrow.parquet as pq
|
||||||
|
rows = engineering_rows()
|
||||||
|
# The map may contain multiple parameterized primitives for one indicator.
|
||||||
|
rows[-1] = {**rows[-1], "indicator_id": 0, "period": 11, "p1": 0.0}
|
||||||
|
map_path = tmp_path / "engineering.json"
|
||||||
|
map_path.write_text(json.dumps({"artifact": "ENGINEERING_FAMILY_MAP_V1", "primitives": rows}), encoding="utf-8")
|
||||||
|
usage_path = tmp_path / "usage.parquet"
|
||||||
|
pq.write_table(pa.Table.from_pylist([
|
||||||
|
{"entity_type": "indicator_id", "indicator_id": indicator_id, "usage_count": indicator_id + 1}
|
||||||
|
for indicator_id in sorted({int(row["indicator_id"]) for row in rows})
|
||||||
|
]), usage_path)
|
||||||
|
checkpoints = tmp_path / "checkpoints"
|
||||||
|
checkpoints.mkdir()
|
||||||
|
for row in rows:
|
||||||
|
values = np.array([1.0, np.nan]) if row["indicator_id"] else np.array([np.nan, np.nan])
|
||||||
|
np.save(
|
||||||
|
checkpoints / f"hs22_{row['indicator_id']}_{row['period']}_{row['p1']:g}.npy",
|
||||||
|
values,
|
||||||
|
allow_pickle=False,
|
||||||
|
)
|
||||||
|
output = tmp_path / "output"
|
||||||
|
monkeypatch.setattr(sys, "argv", ["generator", "--engineering-map", str(map_path), "--usage-parquet", str(usage_path), "--checkpoint-dir", str(checkpoints), "--output-dir", str(output)])
|
||||||
|
|
||||||
|
generator.main()
|
||||||
|
|
||||||
|
result = pq.read_table(output / "historical_feature_semantic_domain_map_v1.parquet").to_pylist()
|
||||||
|
summary = json.loads((output / "historical_feature_semantic_domain_map_v1_summary.json").read_text())
|
||||||
|
assert len(result) == 711
|
||||||
|
assert result[0]["semantic_domain"] == "trend"
|
||||||
|
assert result[1]["semantic_type"] == "event"
|
||||||
|
assert result[2]["semantic_type"] == "state"
|
||||||
|
assert result[4]["semantic_mapping_provenance"] == "heuristic"
|
||||||
|
assert result[0]["usage_count"] == 1
|
||||||
|
assert result[0]["usage_grain"] == "indicator_id_replicated_not_parameter_specific"
|
||||||
|
assert result[-1]["usage_count"] == 1
|
||||||
|
assert result[-1]["usage_grain"] == "indicator_id_replicated_not_parameter_specific"
|
||||||
|
assert result[0]["finite_observation_count"] == 0
|
||||||
|
assert result[1]["finite_observation_count"] == 1
|
||||||
|
assert summary["counts"]["primitives"] == 711
|
||||||
|
assert summary["usage_grain"] == "indicator_id_replicated_not_parameter_specific"
|
||||||
|
|
||||||
|
|
||||||
|
def test_usage_counts_requires_all_engineering_indicator_ids(tmp_path):
|
||||||
|
import pyarrow as pa
|
||||||
|
import pyarrow.parquet as pq
|
||||||
|
path = tmp_path / "usage.parquet"
|
||||||
|
pq.write_table(pa.Table.from_pylist([{"entity_type": "indicator_id", "indicator_id": 1, "usage_count": 1}]), path)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="does not cover every"):
|
||||||
|
generator.usage_counts(path, {1, 2})
|
||||||
|
|
||||||
|
|
||||||
|
def test_usage_counts_replicates_indicator_count_for_parameterized_primitives(tmp_path):
|
||||||
|
import pyarrow as pa
|
||||||
|
import pyarrow.parquet as pq
|
||||||
|
path = tmp_path / "usage.parquet"
|
||||||
|
pq.write_table(pa.Table.from_pylist([
|
||||||
|
{"entity_type": "indicator_id", "indicator_id": 1, "usage_count": 7},
|
||||||
|
{"entity_type": "other", "indicator_id": 1, "usage_count": 3},
|
||||||
|
]), path)
|
||||||
|
|
||||||
|
assert generator.usage_counts(path, {1}) == {1: 7}
|
||||||
23
tests/test_gpu_batch01_v1_1_runner.py
Normal file
23
tests/test_gpu_batch01_v1_1_runner.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytest.importorskip("torch")
|
||||||
|
from gpu_batch01_v1_1_runner import read_ohlcv, role_semantics, write_ohlcv_csv
|
||||||
|
|
||||||
|
|
||||||
|
def test_runner_csv_round_trip_and_explicit_missing_role_semantics(tmp_path):
|
||||||
|
path = tmp_path / "ohlcv.csv"
|
||||||
|
expected = {
|
||||||
|
"close": np.array([100.0, 101.0]),
|
||||||
|
"high": np.array([101.0, 102.0]),
|
||||||
|
"low": np.array([99.0, 100.0]),
|
||||||
|
"volume": np.array([1000.0, 1001.0]),
|
||||||
|
}
|
||||||
|
write_ohlcv_csv(path, expected)
|
||||||
|
actual = read_ohlcv(path)
|
||||||
|
assert all(np.array_equal(actual[name], values) for name, values in expected.items())
|
||||||
|
semantics = role_semantics(None)
|
||||||
|
assert semantics["status"] == "not_reconstructable"
|
||||||
|
assert "no strategy-role assignment" in semantics["reason"]
|
||||||
22
tests/test_gpu_batch01_v1_2_runner.py
Normal file
22
tests/test_gpu_batch01_v1_2_runner.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytest.importorskip("torch")
|
||||||
|
|
||||||
|
from gpu_batch01_v1_2_runner import DEFAULT_ATR_LIMITS, _atr_limits
|
||||||
|
|
||||||
|
|
||||||
|
def test_v1_2_uses_fixed_atr_defaults_covering_documented_drift():
|
||||||
|
assert _atr_limits(None) == DEFAULT_ATR_LIMITS
|
||||||
|
assert DEFAULT_ATR_LIMITS["max_absolute_error"] >= 1.4210854715202004e-14
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("limits", [
|
||||||
|
{"max_absolute_error": -1.0, "mae": 0.0},
|
||||||
|
{"max_absolute_error": float("nan"), "mae": 0.0},
|
||||||
|
{"max_absolute_error": 0.0},
|
||||||
|
])
|
||||||
|
def test_v1_2_rejects_invalid_atr_limits(limits):
|
||||||
|
with pytest.raises(ValueError, match="finite, non-negative"):
|
||||||
|
_atr_limits(limits)
|
||||||
111
tests/test_gpu_feature_parity_contract_v1_1.py
Normal file
111
tests/test_gpu_feature_parity_contract_v1_1.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from gpu_feature_parity_contract_v1_1 import (
|
||||||
|
ARTIFACT,
|
||||||
|
FrozenContractError,
|
||||||
|
SCENARIOS,
|
||||||
|
calibrate,
|
||||||
|
compare_stateful_trace,
|
||||||
|
corpus_manifest,
|
||||||
|
cpu_psar_trace,
|
||||||
|
cpu_supertrend_trace,
|
||||||
|
deterministic_adversarial_ohlcv,
|
||||||
|
freeze_contract,
|
||||||
|
historical_cpu_oracle,
|
||||||
|
nan_gap_semantics_manifest,
|
||||||
|
validate_frozen_contract,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence():
|
||||||
|
ohlcv = deterministic_adversarial_ohlcv(length=96, seed=7)
|
||||||
|
requests = [{"request_id": "super", "indicator_id": 19, "period": 8, "p1": 2.0}]
|
||||||
|
oracle = {"super": cpu_supertrend_trace(ohlcv["close"], ohlcv["high"], ohlcv["low"], 8, 2.0)["output"]}
|
||||||
|
corpus = corpus_manifest({"adversarial": ohlcv}, {"generator": "deterministic_adversarial_ohlcv", "seed": 7})
|
||||||
|
calibration = calibrate(oracle, oracle, requests, ohlcv["close"], corpus)
|
||||||
|
return ohlcv, oracle, freeze_contract(calibration)
|
||||||
|
|
||||||
|
|
||||||
|
def test_adversarial_generator_is_deterministic_and_manifest_has_provenance():
|
||||||
|
first = deterministic_adversarial_ohlcv(length=96, seed=3)
|
||||||
|
second = deterministic_adversarial_ohlcv(length=96, seed=3)
|
||||||
|
assert all(np.array_equal(first[key], second[key]) for key in first)
|
||||||
|
manifest = corpus_manifest({"edge": first}, {"source": "test"})
|
||||||
|
assert manifest["scenario_categories"] == list(SCENARIOS)
|
||||||
|
assert manifest["provenance"]["source"] == "test"
|
||||||
|
assert {"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", "warmup"} == set(SCENARIOS)
|
||||||
|
|
||||||
|
|
||||||
|
def test_frozen_contract_validates_and_rejects_injected_numeric_nan_and_decision_violations():
|
||||||
|
ohlcv, oracle, contract = _evidence()
|
||||||
|
assert contract["artifact"] == ARTIFACT
|
||||||
|
assert {"max_ulp", "p50_ulp", "p95_ulp", "p99_ulp", "p99_9_ulp"}.issubset(contract["feature_limits"]["super"])
|
||||||
|
assert {"max_ulp", "p50_ulp", "p95_ulp", "p99_ulp", "p99_9_ulp"}.issubset(contract["family_limits"]["supertrend"])
|
||||||
|
assert validate_frozen_contract(contract, oracle, oracle, ohlcv["close"])["passed"]
|
||||||
|
numeric = {"super": oracle["super"].copy()}; numeric["super"][-1] += 1.0
|
||||||
|
assert not validate_frozen_contract(contract, numeric, oracle, ohlcv["close"])["passed"]
|
||||||
|
nan = {"super": oracle["super"].copy()}; nan["super"][-1] = np.nan
|
||||||
|
assert not validate_frozen_contract(contract, nan, oracle, ohlcv["close"])["passed"]
|
||||||
|
decision = {"super": oracle["super"].copy()}; index = np.flatnonzero(np.isfinite(decision["super"]))[-1]; decision["super"][index] = ohlcv["close"][index] + 1.0
|
||||||
|
assert not validate_frozen_contract(contract, decision, oracle, ohlcv["close"])["passed"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_frozen_contract_rejects_a_deliberate_ulp_breach():
|
||||||
|
ohlcv, oracle, contract = _evidence()
|
||||||
|
breach = {"super": oracle["super"].copy()}
|
||||||
|
index = np.flatnonzero(np.isfinite(breach["super"]))[-1]
|
||||||
|
breach["super"][index] = np.nextafter(breach["super"][index], np.inf)
|
||||||
|
result = validate_frozen_contract(contract, breach, oracle, ohlcv["close"])
|
||||||
|
assert not result["passed"]
|
||||||
|
assert result["records"][0]["max_ulp"] > contract["feature_limits"]["super"]["max_ulp"]
|
||||||
|
assert "max_ulp" in result["records"][0]["numeric_violations"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_refuses_unfrozen_or_derived_contracts():
|
||||||
|
ohlcv, oracle, contract = _evidence()
|
||||||
|
contract["status"] = "accepted"
|
||||||
|
with pytest.raises(FrozenContractError):
|
||||||
|
validate_frozen_contract(contract, oracle, oracle, ohlcv["close"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_trace_comparer_requires_every_discrete_state_key_and_value():
|
||||||
|
ohlcv = deterministic_adversarial_ohlcv(length=96)
|
||||||
|
trace = cpu_psar_trace(ohlcv["close"], ohlcv["high"], ohlcv["low"], .02)
|
||||||
|
assert compare_stateful_trace(trace, trace, trace_type="psar")["exact"]
|
||||||
|
changed = dict(trace); changed["reversal"] = trace["reversal"].copy(); changed["reversal"][-1] = ~changed["reversal"][-1]
|
||||||
|
assert compare_stateful_trace(trace, changed, trace_type="psar")["mismatched_keys"] == ["reversal"]
|
||||||
|
assert not compare_stateful_trace(trace, {"output": trace["output"]}, trace_type="psar")["exact"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_decisions_do_not_cross_nan_gaps():
|
||||||
|
close = np.array([1., 2., 3.])
|
||||||
|
oracle = {"super": np.array([0., np.nan, 4.])}
|
||||||
|
calibration = calibrate(oracle, oracle, [{"request_id": "super", "indicator_id": 19}], close, corpus_manifest({"edge": deterministic_adversarial_ohlcv(length=96)}, {}))
|
||||||
|
contract = freeze_contract(calibration)
|
||||||
|
assert validate_frozen_contract(contract, oracle, oracle, close)["passed"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_historical_cpu_oracle_accepts_arbitrary_csv(tmp_path):
|
||||||
|
ohlcv = deterministic_adversarial_ohlcv(length=96)
|
||||||
|
path = tmp_path / "input.csv"
|
||||||
|
with path.open("w", newline="", encoding="utf-8") as handle:
|
||||||
|
writer = csv.DictWriter(handle, fieldnames=["close", "high", "low", "volume"]); writer.writeheader()
|
||||||
|
writer.writerows({key: ohlcv[key][index] for key in ohlcv} for index in range(96))
|
||||||
|
result = historical_cpu_oracle(path, [{"request_id": "psar", "indicator_id": 28, "period": 1, "p1": .02}])
|
||||||
|
assert result["outputs"]["psar"].shape == (96,)
|
||||||
|
assert result["source"]["evaluator"] == "evaluate_band_channel"
|
||||||
|
|
||||||
|
|
||||||
|
def test_nan_gap_semantics_are_manifested_and_rejected_before_historical_evaluation(tmp_path):
|
||||||
|
manifest = nan_gap_semantics_manifest()
|
||||||
|
assert manifest["historical_input_policy"] == "reject_non_finite_ohlcv"
|
||||||
|
assert manifest["internal_nan_gap_semantics"] == "undefined"
|
||||||
|
path = tmp_path / "nan_gap.csv"
|
||||||
|
path.write_text("close,high,low,volume\n100,101,99,1000\nnan,101,99,1000\n", encoding="utf-8")
|
||||||
|
with pytest.raises(ValueError, match="NaN/gap semantics are undefined"):
|
||||||
|
historical_cpu_oracle(path, [{"request_id": "psar", "indicator_id": 28, "period": 1, "p1": .02}])
|
||||||
122
tests/test_gpu_feature_parity_contract_v1_2.py
Normal file
122
tests/test_gpu_feature_parity_contract_v1_2.py
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from gpu_feature_parity_contract_v1_1 import deterministic_adversarial_ohlcv
|
||||||
|
from gpu_feature_parity_contract_v1_2 import (
|
||||||
|
ARTIFACT,
|
||||||
|
CALIBRATION_ARTIFACT,
|
||||||
|
ROLE_C_UNVERIFIABLE,
|
||||||
|
UNVERIFIABLE,
|
||||||
|
calibrate_supertrend,
|
||||||
|
calibrate_output,
|
||||||
|
cpu_supertrend_trace,
|
||||||
|
freeze_contract,
|
||||||
|
role_surface,
|
||||||
|
validate_all_frozen_contract,
|
||||||
|
validate_frozen_contract,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _traces():
|
||||||
|
ohlcv = deterministic_adversarial_ohlcv(length=96, seed=19)
|
||||||
|
expected = cpu_supertrend_trace(ohlcv["close"], ohlcv["high"], ohlcv["low"], 8, 3.0)
|
||||||
|
actual = {name: value.copy() for name, value in expected.items()}
|
||||||
|
return expected, actual
|
||||||
|
|
||||||
|
|
||||||
|
def test_ordered_on_device_seed_matches_historical_left_to_right_seed():
|
||||||
|
torch = pytest.importorskip("torch")
|
||||||
|
from gpu_feature_engine_v1_2 import rma_ordered_seed
|
||||||
|
|
||||||
|
values = torch.tensor([1e16, 1.0, -1e16, 3.0], dtype=torch.float64)
|
||||||
|
result = rma_ordered_seed(values, 4)
|
||||||
|
total = 0.0
|
||||||
|
for value in values.numpy():
|
||||||
|
total += value
|
||||||
|
assert result[-1].item() == total / 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_v1_2_requires_exact_branch_state_before_atr_bounds():
|
||||||
|
expected, actual = _traces()
|
||||||
|
calibration = {"artifact": CALIBRATION_ARTIFACT, "records": [calibrate_supertrend("super", expected, actual)]}
|
||||||
|
contract = freeze_contract(calibration)
|
||||||
|
assert contract["artifact"] == ARTIFACT
|
||||||
|
assert validate_frozen_contract(contract, {"super": (expected, actual)})["passed"]
|
||||||
|
changed = dict(actual)
|
||||||
|
changed["active_long"] = actual["active_long"].copy()
|
||||||
|
changed["active_long"][-1] = ~changed["active_long"][-1]
|
||||||
|
result = validate_frozen_contract(contract, {"super": (expected, changed)})
|
||||||
|
assert not result["passed"]
|
||||||
|
assert result["records"][0]["atr"] is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mutation", ["shape", "nan_mask"])
|
||||||
|
def test_v1_2_requires_atr_structure_before_measuring_bounds(mutation):
|
||||||
|
expected, actual = _traces()
|
||||||
|
calibration = {"artifact": CALIBRATION_ARTIFACT, "records": [calibrate_supertrend("super", expected, actual)]}
|
||||||
|
contract = freeze_contract(calibration)
|
||||||
|
changed = dict(actual)
|
||||||
|
changed["atr"] = actual["atr"][:-1].copy() if mutation == "shape" else actual["atr"].copy()
|
||||||
|
if mutation == "nan_mask":
|
||||||
|
changed["atr"][-1] = np.nan
|
||||||
|
result = validate_frozen_contract(contract, {"super": (expected, changed)})
|
||||||
|
assert not result["passed"]
|
||||||
|
assert result["records"][0]["atr"] is None
|
||||||
|
assert not result["records"][0]["trace"]["atr_structure_exact"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_unrecorded_batch01_roles_are_unverifiable_and_not_parity_blockers():
|
||||||
|
surface = role_surface([
|
||||||
|
{"request_id": "known", "indicator_id": 19, "period": 10, "p1": 2.0},
|
||||||
|
{"request_id": "unknown", "indicator_id": 17, "period": 999, "p1": 2.0},
|
||||||
|
])
|
||||||
|
unknown = next(item for item in surface["records"] if item["request_id"] == "unknown")
|
||||||
|
assert unknown["classification"] == ROLE_C_UNVERIFIABLE
|
||||||
|
assert unknown["role_status"] == UNVERIFIABLE
|
||||||
|
assert not unknown["parity_blocker"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_surface_is_unverifiable_when_recovered_registry_is_absent(monkeypatch):
|
||||||
|
original_import = builtins.__import__
|
||||||
|
|
||||||
|
def missing_registry(name, *args, **kwargs):
|
||||||
|
if name == "control_plane.trading_studio.indicators.registry":
|
||||||
|
raise ModuleNotFoundError(name)
|
||||||
|
return original_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(builtins, "__import__", missing_registry)
|
||||||
|
surface = role_surface([{"request_id": "known", "indicator_id": 19, "period": 10, "p1": 2.0}])
|
||||||
|
|
||||||
|
record = surface["records"][0]
|
||||||
|
assert record["role_status"] == UNVERIFIABLE
|
||||||
|
assert record["reason"] == "control_plane.trading_studio.indicators.registry is unavailable"
|
||||||
|
assert not record["parity_blocker"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_batch_contract_keeps_exact_and_calibration_bounded_classes_separate():
|
||||||
|
expected, actual = _traces()
|
||||||
|
values = np.array([np.nan, 2.0, 3.0])
|
||||||
|
calibration = {
|
||||||
|
"artifact": CALIBRATION_ARTIFACT,
|
||||||
|
"records": [
|
||||||
|
calibrate_supertrend("super", expected, actual, atr_limits={"max_absolute_error": 0.0, "mae": 0.0}),
|
||||||
|
calibrate_output("donch", 20, values, values.copy()),
|
||||||
|
calibrate_output("psar", 28, values, values.copy()),
|
||||||
|
calibrate_output("bb", 17, values, values.copy()),
|
||||||
|
calibrate_output("kc", 23, values, values.copy()),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
contract = freeze_contract(calibration)
|
||||||
|
result = validate_all_frozen_contract(
|
||||||
|
contract,
|
||||||
|
{"super": expected["output"], "donch": values, "psar": values, "bb": values, "kc": values},
|
||||||
|
{"super": actual["output"], "donch": values.copy(), "psar": values.copy(), "bb": values.copy(), "kc": values.copy()},
|
||||||
|
{"super": (expected, actual)},
|
||||||
|
)
|
||||||
|
assert result["passed"]
|
||||||
|
assert contract["feature_limits"]["donch"]["role"] == "A_EXACT"
|
||||||
|
assert contract["feature_limits"]["bb"]["role"] == "B_BOUNDED"
|
||||||
38
tests/test_gpu_supertrend_v1_2_diagnostic.py
Normal file
38
tests/test_gpu_supertrend_v1_2_diagnostic.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
torch = pytest.importorskip("torch")
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from gpu_feature_parity_contract_v1_1 import deterministic_adversarial_ohlcv # noqa: E402
|
||||||
|
from gpu_feature_engine_v1_2 import supertrend_trace # noqa: E402
|
||||||
|
from gpu_feature_parity_contract_v1_2 import cpu_supertrend_trace # noqa: E402
|
||||||
|
from gpu_supertrend_v1_2_diagnostic import diagnose # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def test_period_10_semantic_diagnostic_has_serializable_cpu_gpu_trace_statistics():
|
||||||
|
ohlcv = deterministic_adversarial_ohlcv(length=96, seed=11)
|
||||||
|
report = diagnose(ohlcv, device=torch.device("cpu"))
|
||||||
|
assert json.dumps(report, allow_nan=False)
|
||||||
|
assert [record["multiplier"] for record in report["records"]] == [2.0, 3.0, 4.0]
|
||||||
|
for record in report["records"]:
|
||||||
|
assert set(record["numeric"]) == {"output", "true_range", "atr", "basic_upper", "basic_lower", "upper", "lower"}
|
||||||
|
assert record["numeric"]["atr"]["first_divergence"] is None
|
||||||
|
assert record["direction"]["exact"]
|
||||||
|
assert record["transitions"]["direction_transition"]["exact"]
|
||||||
|
assert all(item["exact"] for item in record["branch_predicates"].values())
|
||||||
|
|
||||||
|
|
||||||
|
def test_cpu_trace_exposes_the_complete_gpu_trace_surface():
|
||||||
|
ohlcv = deterministic_adversarial_ohlcv(length=96, seed=11)
|
||||||
|
cpu = cpu_supertrend_trace(ohlcv["close"], ohlcv["high"], ohlcv["low"], 10, 3.0)
|
||||||
|
gpu = supertrend_trace(
|
||||||
|
torch.as_tensor(ohlcv["close"], dtype=torch.float64),
|
||||||
|
torch.as_tensor(ohlcv["high"], dtype=torch.float64),
|
||||||
|
torch.as_tensor(ohlcv["low"], dtype=torch.float64),
|
||||||
|
10,
|
||||||
|
3.0,
|
||||||
|
)
|
||||||
|
assert set(cpu) == set(gpu)
|
||||||
367
tests/test_hyperscalper_feature_gap_analysis_v1.py
Normal file
367
tests/test_hyperscalper_feature_gap_analysis_v1.py
Normal file
|
|
@ -0,0 +1,367 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import hyperscalper_feature_gap_analysis_v1 as runner
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_only_runner_writes_711_map_and_honest_deq_blocker(tmp_path, monkeypatch):
|
||||||
|
rows = [
|
||||||
|
{"request_id": f"r{index}", "indicator_id": index, "period": 10, "p1": 0.0}
|
||||||
|
for index in range(711)
|
||||||
|
]
|
||||||
|
request = tmp_path / "request.json"
|
||||||
|
request.write_text(json.dumps({"requests": rows}), encoding="utf-8")
|
||||||
|
checkpoint = tmp_path / "checkpoint.npz"
|
||||||
|
values = {f"r{index}": np.arange(8, dtype=float) + index for index in range(711)}
|
||||||
|
values["r710"] = np.full(8, np.nan)
|
||||||
|
np.savez(checkpoint, **values)
|
||||||
|
manifest = tmp_path / "manifest.json"
|
||||||
|
manifest.write_text(
|
||||||
|
json.dumps({"coverage": {"feature_versions": [f"r{index}" for index in range(711)]}}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
for name in ("usage.parquet", "lineage.parquet"):
|
||||||
|
(tmp_path / name).write_bytes(b"not-read-without-pyarrow")
|
||||||
|
output = tmp_path / "out"
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sys,
|
||||||
|
"argv",
|
||||||
|
[
|
||||||
|
"runner",
|
||||||
|
"--oracle-request",
|
||||||
|
str(request),
|
||||||
|
"--oracle-checkpoint",
|
||||||
|
str(checkpoint),
|
||||||
|
"--acceptance-manifest",
|
||||||
|
str(manifest),
|
||||||
|
"--historical-usage",
|
||||||
|
str(tmp_path / "usage.parquet"),
|
||||||
|
"--lineage",
|
||||||
|
str(tmp_path / "lineage.parquet"),
|
||||||
|
"--output-dir",
|
||||||
|
str(output),
|
||||||
|
"--sample-rows",
|
||||||
|
"8",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
runner.main()
|
||||||
|
result = json.loads((output / "hyperscalper_feature_gap_analysis_v1.json").read_text())
|
||||||
|
assert result["read_only"] is True
|
||||||
|
assert len(result["information_map"]["features"]) == 711
|
||||||
|
assert result["information_map"]["features"][-1]["redundancy_status"] == (
|
||||||
|
"unusable_no_finite_observations"
|
||||||
|
)
|
||||||
|
assert result["redundancy"]["map"]["format"] == "parquet"
|
||||||
|
assert result["decision_equivalence"]["status"] == "BLOCKED"
|
||||||
|
|
||||||
|
|
||||||
|
def test_checkpoint_directory_maps_hs22_filenames_to_engineering_primitives(tmp_path):
|
||||||
|
rows = [
|
||||||
|
{"indicator_id": index, "period": index + 10, "p1": float(index % 3)}
|
||||||
|
for index in range(711)
|
||||||
|
]
|
||||||
|
checkpoint_dir = tmp_path / "checkpoints"
|
||||||
|
checkpoint_dir.mkdir()
|
||||||
|
for row in reversed(rows):
|
||||||
|
np.save(
|
||||||
|
checkpoint_dir
|
||||||
|
/ f"hs22_{row['indicator_id']}_{row['period']}_{row['p1']:g}.npy",
|
||||||
|
np.array([row["indicator_id"], row["period"]], dtype=float),
|
||||||
|
allow_pickle=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
matrix, names = runner.checkpoint_directory_columns(
|
||||||
|
checkpoint_dir,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
**row,
|
||||||
|
"feature_key": f"{row['indicator_id']}:{row['period']}:{row['p1']:g}",
|
||||||
|
"request_id": None,
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matrix.shape == (2, 711)
|
||||||
|
assert np.array_equal(matrix[:, 0], [0.0, 10.0])
|
||||||
|
assert np.array_equal(matrix[:, -1], [710.0, 720.0])
|
||||||
|
assert names[0] == "hs22_0_10_0.npy"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pairwise_redundancy_uses_staggered_finite_observations_for_clustering():
|
||||||
|
sample = np.array(
|
||||||
|
[
|
||||||
|
[0.0, 0.0, np.nan, np.nan],
|
||||||
|
[1.0, 2.0, np.nan, np.nan],
|
||||||
|
[2.0, 4.0, np.nan, np.nan],
|
||||||
|
[np.nan, np.nan, 5.0, np.nan],
|
||||||
|
[np.nan, np.nan, 6.0, np.nan],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
pearson, spearman, counts = runner.pairwise_redundancy(sample, min_pair_samples=3)
|
||||||
|
|
||||||
|
assert counts[0, 1] == 3
|
||||||
|
assert pearson[0, 1] == pytest.approx(1.0)
|
||||||
|
assert spearman[0, 1] == pytest.approx(1.0)
|
||||||
|
assert np.isnan(pearson[0, 2])
|
||||||
|
assert runner.clusters(pearson) == [[0, 1]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_pairwise_redundancy_precomputes_ranks_once_per_column_with_bounded_runtime(monkeypatch):
|
||||||
|
sample = np.arange(512 * 64, dtype=float).reshape(512, 64)
|
||||||
|
sample[::17, ::7] = np.nan
|
||||||
|
calls = 0
|
||||||
|
original = runner.rank_finite_columns
|
||||||
|
|
||||||
|
def counted_ranks(values):
|
||||||
|
nonlocal calls
|
||||||
|
calls += 1
|
||||||
|
return original(values)
|
||||||
|
|
||||||
|
monkeypatch.setattr(runner, "rank_finite_columns", counted_ranks)
|
||||||
|
started = time.perf_counter()
|
||||||
|
pearson, spearman, counts = runner.pairwise_redundancy(sample, min_pair_samples=2)
|
||||||
|
|
||||||
|
assert time.perf_counter() - started < 3.0
|
||||||
|
assert calls == 1
|
||||||
|
assert counts[0, 1] > 2
|
||||||
|
assert pearson[0, 1] == pytest.approx(1.0)
|
||||||
|
assert spearman[0, 1] == pytest.approx(1.0, abs=1e-5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_aware_redundancy_uses_agreement_and_jaccard_for_detection_outputs():
|
||||||
|
sample = np.array(
|
||||||
|
[
|
||||||
|
[0.0, 1.0, 1.0, 0.0],
|
||||||
|
[1.0, 1.0, 1.0, 1.0],
|
||||||
|
[1.0, 0.0, 0.0, 1.0],
|
||||||
|
[0.0, 0.0, 0.0, 0.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
pearson, spearman, agreement, jaccard, counts = runner.type_aware_redundancy(
|
||||||
|
sample, ["state", "state", "event", "event"], min_pair_samples=2
|
||||||
|
)
|
||||||
|
primary = runner.primary_redundancy_matrix(
|
||||||
|
pearson, agreement, jaccard, ["state", "state", "event", "event"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert counts[0, 1] == 4
|
||||||
|
assert agreement[0, 1] == pytest.approx(0.5)
|
||||||
|
assert jaccard[2, 3] == pytest.approx(1 / 3)
|
||||||
|
assert np.isnan(pearson[0, 1])
|
||||||
|
assert primary[0, 1] == pytest.approx(0.5)
|
||||||
|
assert primary[2, 3] == pytest.approx(1 / 3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_semantic_types_derives_type_and_domain_from_primitive_map(tmp_path):
|
||||||
|
semantic_map = tmp_path / "semantic-map.json"
|
||||||
|
semantic_map.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"primitives": [
|
||||||
|
{"indicator_id": 1, "period": 10, "p1": 0, "output_type": "boolean", "engineering_family": "regime"},
|
||||||
|
{"indicator_id": 2, "period": 20, "p1": 1, "output_type": "event", "domain": "trigger"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
rows = [
|
||||||
|
{"feature_key": "1:10:0", "indicator_id": 1, "period": 10, "p1": 0.0},
|
||||||
|
{"feature_key": "2:20:1", "indicator_id": 2, "period": 20, "p1": 1.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
types, blocker = runner.semantic_types(semantic_map, rows)
|
||||||
|
|
||||||
|
assert blocker is None
|
||||||
|
assert types == [
|
||||||
|
{"output_type": "state", "domain": "regime"},
|
||||||
|
{"output_type": "event", "domain": "trigger"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_lineage_combo_rejects_incomplete_data():
|
||||||
|
with pytest.raises(ValueError, match="at least five triples"):
|
||||||
|
runner.lineage_combo([1, 10, 0.0])
|
||||||
|
with pytest.raises(ValueError, match="at least five triples"):
|
||||||
|
runner.lineage_combo([[1, 10, 0.0]] * 5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_lineage_combo_reads_only_the_five_primitives_from_real_flat_encoding():
|
||||||
|
# Flat lineage rows append tp, sl, confirmation bounds, and volume threshold.
|
||||||
|
combo = [
|
||||||
|
17, 20, 2.0,
|
||||||
|
31, 14, 0.0,
|
||||||
|
47, 9, 1.5,
|
||||||
|
62, 30, 0.0,
|
||||||
|
70, 20, 0.0,
|
||||||
|
0.015, 0.01, 5, 20, 5, 20, 75,
|
||||||
|
]
|
||||||
|
|
||||||
|
assert runner.lineage_combo(json.dumps(combo)) == (
|
||||||
|
(17, 20, 2.0),
|
||||||
|
(31, 14, 0.0),
|
||||||
|
(47, 9, 1.5),
|
||||||
|
(62, 30, 0.0),
|
||||||
|
(70, 20, 0.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_lineage_usage_parses_combo_json_role_triples(tmp_path):
|
||||||
|
pa = pytest.importorskip("pyarrow")
|
||||||
|
pq = pytest.importorskip("pyarrow.parquet")
|
||||||
|
lineage = tmp_path / "lineage.parquet"
|
||||||
|
combo = [1, 10, 0.0, 2, 11, 1.5, 1, 10, 0.0, 4, 13, 2.0, 5, 14, 3.0]
|
||||||
|
pq.write_table(
|
||||||
|
pa.Table.from_pylist(
|
||||||
|
[
|
||||||
|
{"strategy_id": 100, "combo_json": json.dumps(combo)},
|
||||||
|
{"strategy_id": 101, "combo_json": json.dumps(combo + [0.01, 0.02, 3, 6, 3, 6, 75])},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
lineage,
|
||||||
|
)
|
||||||
|
|
||||||
|
counts, evidence, blocker = runner.lineage_usage(lineage)
|
||||||
|
|
||||||
|
assert blocker is None
|
||||||
|
assert counts == {"1:10:0": 4, "2:11:1.5": 2, "4:13:2": 2, "5:14:3": 2}
|
||||||
|
assert evidence["role_counts"] == {
|
||||||
|
"confirm": 2,
|
||||||
|
"signal": 2,
|
||||||
|
"trend": 2,
|
||||||
|
"trigger": 2,
|
||||||
|
"vol": 2,
|
||||||
|
}
|
||||||
|
assert evidence["strategy_ids_observed"] == 2
|
||||||
|
assert evidence["combo_counts"][0]["usage_count"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_cohort_deq_evidence_aggregates_exported_ledger_associatively(tmp_path):
|
||||||
|
deq_path = tmp_path / "cohort-deq.json"
|
||||||
|
sample = {
|
||||||
|
"deq": {
|
||||||
|
"return_1_bars_bps": 10.0,
|
||||||
|
"return_2_bars_bps": -5.0,
|
||||||
|
"return_3_bars_bps": None,
|
||||||
|
"return_5_bars_bps": 0.0,
|
||||||
|
"return_10_bars_bps": 20.0,
|
||||||
|
"mfe_bps": 25.0,
|
||||||
|
"mae_bps": -4.0,
|
||||||
|
"time_to_positive_bars": 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deq_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"contract": "Cohort001-DEQ-ledger-export-v1",
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"strategy_version_id": "strategy-a",
|
||||||
|
"feature_triples": [
|
||||||
|
{"indicator_id": 7, "period": 10, "p1": 1.0},
|
||||||
|
{"indicator_id": 7, "period": 20, "p1": 2.0},
|
||||||
|
],
|
||||||
|
"folds": [
|
||||||
|
{
|
||||||
|
"fold": "Fold 1",
|
||||||
|
"scenarios": [
|
||||||
|
{"scenario": "BASE", "deq_samples": [sample]},
|
||||||
|
{"scenario": "STRESS", "deq_samples": [sample]},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
evidence, parquet_rows, blocker = runner.cohort_deq_evidence(deq_path)
|
||||||
|
|
||||||
|
assert blocker is None
|
||||||
|
assert evidence["status"] == "AVAILABLE"
|
||||||
|
assert evidence["attribution"] == "ASSOCIATIVE_NOT_CAUSAL"
|
||||||
|
assert evidence["strategy_summaries"][0]["trade_count"] == 2
|
||||||
|
assert evidence["strategy_summaries"][0]["scenario_coverage"] == ["BASE", "STRESS"]
|
||||||
|
returns = evidence["feature_summaries"][0]["returns"]
|
||||||
|
assert returns["return_1_bars_bps"]["mean"] == 10.0
|
||||||
|
assert returns["return_2_bars_bps"]["directional_incorrect_count"] == 2
|
||||||
|
assert evidence["family_summaries"][0]["trade_count"] == 4
|
||||||
|
assert {row["entity_type"] for row in parquet_rows} == {"strategy", "feature", "family"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cohort_deq_evidence_uses_genome_combo_when_exported_triples_are_null(tmp_path):
|
||||||
|
deq_path = tmp_path / "cohort-deq.json"
|
||||||
|
deq_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"contract": "Cohort001-DEQ-ledger-export-v1",
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"strategy_version_id": "strategy-a",
|
||||||
|
"feature_triples": None,
|
||||||
|
"genome": {
|
||||||
|
"combo": [
|
||||||
|
7, 10, 1.0,
|
||||||
|
8, 11, 2.0,
|
||||||
|
9, 12, 3.0,
|
||||||
|
10, 13, 4.0,
|
||||||
|
11, 14, 5.0,
|
||||||
|
0.01, 0.02, 3, 6, 3, 6, 75,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"folds": [
|
||||||
|
{
|
||||||
|
"fold": "Fold 1",
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"scenario": "BASE",
|
||||||
|
"deq_samples": [{"deq": {"return_1_bars_bps": 10.0}}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
evidence, _, blocker = runner.cohort_deq_evidence(deq_path)
|
||||||
|
|
||||||
|
assert blocker is None
|
||||||
|
assert [item["feature_key"] for item in evidence["feature_summaries"]] == [
|
||||||
|
"10:13:4",
|
||||||
|
"11:14:5",
|
||||||
|
"7:10:1",
|
||||||
|
"8:11:2",
|
||||||
|
"9:12:3",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_deq_strategy_feature_keys_falls_back_when_exported_triples_are_invalid():
|
||||||
|
strategy = {
|
||||||
|
"feature_triples": [{"indicator_id": "not-an-id"}],
|
||||||
|
"genome": {"combo": [7, 10, 1.0, 8, 11, 2.0, 9, 12, 3.0, 10, 13, 4.0, 11, 14, 5.0]},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert runner.deq_strategy_feature_keys(strategy) == [
|
||||||
|
("7:10:1", "7"),
|
||||||
|
("8:11:2", "8"),
|
||||||
|
("9:12:3", "9"),
|
||||||
|
("10:13:4", "10"),
|
||||||
|
("11:14:5", "11"),
|
||||||
|
]
|
||||||
60
tests/test_hyperscalper_wave1_feature_synthesis_v1.py
Normal file
60
tests/test_hyperscalper_wave1_feature_synthesis_v1.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import hyperscalper_wave1_feature_synthesis_v1 as synthesis
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_synthesis_requires_explicit_ohlcv_and_reports_missing_failure_artifact(
|
||||||
|
tmp_path, monkeypatch
|
||||||
|
):
|
||||||
|
features = [
|
||||||
|
{
|
||||||
|
"indicator_id": index,
|
||||||
|
"period": 10,
|
||||||
|
"p1": 0,
|
||||||
|
"domain": "trend",
|
||||||
|
"output_type": "continuous",
|
||||||
|
"input_scope": "OHLCV_ONLY",
|
||||||
|
}
|
||||||
|
for index in range(20)
|
||||||
|
] + [{"indicator_id": 99, "period": 10, "p1": 0, "input_scope": "EXTERNAL"}]
|
||||||
|
semantic = tmp_path / "semantic.json"
|
||||||
|
semantic.write_text(json.dumps({"features": features}), encoding="utf-8")
|
||||||
|
redundancy = tmp_path / "redundancy.json"
|
||||||
|
redundancy.write_text(json.dumps({"clusters": []}), encoding="utf-8")
|
||||||
|
deq = tmp_path / "deq.json"
|
||||||
|
deq.write_text(json.dumps({"feature_summaries": []}), encoding="utf-8")
|
||||||
|
gap = tmp_path / "gap.json"
|
||||||
|
gap.write_text(json.dumps({"information_map": {"features": []}}), encoding="utf-8")
|
||||||
|
output = tmp_path / "out"
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sys,
|
||||||
|
"argv",
|
||||||
|
[
|
||||||
|
"runner",
|
||||||
|
"--semantic-summary",
|
||||||
|
str(semantic),
|
||||||
|
"--redundancy-clusters",
|
||||||
|
str(redundancy),
|
||||||
|
"--deq-summary",
|
||||||
|
str(deq),
|
||||||
|
"--lineage-gap-analysis",
|
||||||
|
str(gap),
|
||||||
|
"--output-dir",
|
||||||
|
str(output),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
synthesis.main()
|
||||||
|
|
||||||
|
result = json.loads((output / "new_feature_candidate_registry_v1.json").read_text())
|
||||||
|
assert len(result["candidates"]) == 20
|
||||||
|
assert result["status"] == "BLOCKED"
|
||||||
|
assert result["sources"]["failure_mining"]["status"] == "UNAVAILABLE"
|
||||||
|
assert all(item["input_scope"] == "OHLCV_ONLY" for item in result["candidates"])
|
||||||
66
tests/test_validate_historical_oracle_acceptance.py
Normal file
66
tests/test_validate_historical_oracle_acceptance.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from validate_historical_oracle_acceptance import array_digest, validate
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_uses_runner_semantic_array_digest(tmp_path):
|
||||||
|
output_dir = tmp_path / "run"
|
||||||
|
checkpoints = output_dir / "checkpoints"
|
||||||
|
checkpoints.mkdir(parents=True)
|
||||||
|
values = np.array([1.0, 2.0], dtype=np.float64)
|
||||||
|
status = {
|
||||||
|
"artifact": "HISTORICAL_FEATURE_ORACLE_FULL_STATUS_V1",
|
||||||
|
"status": "PASS",
|
||||||
|
"completed_features": 711,
|
||||||
|
"checkpoints": [
|
||||||
|
{
|
||||||
|
"request_id": f"feature-{index}",
|
||||||
|
"path": f"feature-{index}.npy",
|
||||||
|
"dtype": "<f8",
|
||||||
|
"shape": [2],
|
||||||
|
"sha256": array_digest(values),
|
||||||
|
}
|
||||||
|
for index in range(711)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
for checkpoint in status["checkpoints"]:
|
||||||
|
np.save(checkpoints / checkpoint["path"], values, allow_pickle=False)
|
||||||
|
(output_dir / "final_status.json").write_text(json.dumps(status), encoding="utf-8")
|
||||||
|
|
||||||
|
assert validate(output_dir)["status"] == "PASS"
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_rejects_raw_npy_file_digest(tmp_path):
|
||||||
|
output_dir = tmp_path / "run"
|
||||||
|
checkpoints = output_dir / "checkpoints"
|
||||||
|
checkpoints.mkdir(parents=True)
|
||||||
|
values = np.array([1.0], dtype=np.float64)
|
||||||
|
np.save(checkpoints / "feature.npy", values, allow_pickle=False)
|
||||||
|
status = {
|
||||||
|
"artifact": "HISTORICAL_FEATURE_ORACLE_FULL_STATUS_V1",
|
||||||
|
"status": "PASS",
|
||||||
|
"completed_features": 711,
|
||||||
|
"checkpoints": [
|
||||||
|
{
|
||||||
|
"request_id": f"feature-{index}",
|
||||||
|
"path": f"feature-{index}.npy",
|
||||||
|
"dtype": "<f8",
|
||||||
|
"shape": [1],
|
||||||
|
"sha256": "0" * 64,
|
||||||
|
}
|
||||||
|
for index in range(711)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
for checkpoint in status["checkpoints"]:
|
||||||
|
np.save(checkpoints / checkpoint["path"], values, allow_pickle=False)
|
||||||
|
(output_dir / "final_status.json").write_text(json.dumps(status), encoding="utf-8")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="semantic digest mismatch"):
|
||||||
|
validate(output_dir)
|
||||||
75
validate_historical_oracle_acceptance.py
Normal file
75
validate_historical_oracle_acceptance.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Validate a completed 711-feature historical-oracle run without modifying it."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def canonical(value: object) -> bytes:
|
||||||
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def array_digest(values: np.ndarray) -> str:
|
||||||
|
values = np.ascontiguousarray(values)
|
||||||
|
return hashlib.sha256(
|
||||||
|
canonical({"dtype": values.dtype.str, "shape": values.shape}) + b"\0" + values.tobytes()
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def validate(output_dir: Path) -> dict[str, Any]:
|
||||||
|
status_path = output_dir / "final_status.json"
|
||||||
|
status = json.loads(status_path.read_text(encoding="utf-8"))
|
||||||
|
checkpoints = status.get("checkpoints")
|
||||||
|
if status.get("artifact") != "HISTORICAL_FEATURE_ORACLE_FULL_STATUS_V1":
|
||||||
|
raise ValueError("invalid full-status artifact")
|
||||||
|
if status.get("status") != "PASS" or status.get("completed_features") != 711:
|
||||||
|
raise ValueError("full-status artifact does not attest to 711 passing features")
|
||||||
|
if not isinstance(checkpoints, list) or len(checkpoints) != 711:
|
||||||
|
raise ValueError("full-status artifact must contain exactly 711 checkpoints")
|
||||||
|
request_ids = set()
|
||||||
|
for checkpoint in checkpoints:
|
||||||
|
request_id = checkpoint.get("request_id")
|
||||||
|
if not isinstance(request_id, str) or request_id in request_ids:
|
||||||
|
raise ValueError("checkpoint request IDs must be unique strings")
|
||||||
|
request_ids.add(request_id)
|
||||||
|
path = output_dir / "checkpoints" / str(checkpoint.get("path", ""))
|
||||||
|
if path.name != f"{request_id}.npy" or not path.is_file():
|
||||||
|
raise ValueError(f"missing checkpoint: {request_id}")
|
||||||
|
values = np.load(path, allow_pickle=False)
|
||||||
|
if (
|
||||||
|
values.dtype.str != checkpoint.get("dtype")
|
||||||
|
or list(values.shape) != checkpoint.get("shape")
|
||||||
|
):
|
||||||
|
raise ValueError(f"checkpoint dtype or shape mismatch: {request_id}")
|
||||||
|
if array_digest(values) != checkpoint.get("sha256"):
|
||||||
|
raise ValueError(f"checkpoint semantic digest mismatch: {request_id}")
|
||||||
|
return {
|
||||||
|
"artifact": "HISTORICAL_FEATURE_ORACLE_ACCEPTANCE_VALIDATION_V1",
|
||||||
|
"status": "PASS",
|
||||||
|
"completed_features": len(checkpoints),
|
||||||
|
"status_sha256": hashlib.sha256(status_path.read_bytes()).hexdigest(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--output-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--manifest", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
manifest = validate(args.output_dir)
|
||||||
|
if args.manifest:
|
||||||
|
args.manifest.write_bytes(canonical(manifest) + b"\n")
|
||||||
|
os.chmod(args.manifest, 0o444)
|
||||||
|
print(canonical(manifest).decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Add table
Reference in a new issue