"""Validation for persisted ML scoring artifacts.""" import math from scoring.policy import SCORE_VERSION ML_ARTIFACT_SCHEMA_VERSION = 2 REQUIRED_WEIGHT_KEYS = frozenset({ "puell_multiple", "mvrv_zscore", "reserve_risk", "rhodl_ratio", "nupl", "fear_greed", "drawdown", "pct_above_200w_sma", "pct_above_lth_rp", }) def _weights_valid(weights): if not isinstance(weights, dict) or not REQUIRED_WEIGHT_KEYS.issubset(weights): return False values = [weights[key] for key in REQUIRED_WEIGHT_KEYS] return all( isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) and value >= 0 for value in values ) and sum(values) > 0 def _has_oos_fold_weights(artifact): folds = artifact.get("cv_results", {}).get("folds", []) if not isinstance(folds, list) or not folds: return False for fold in folds: validation_range = fold.get("date_ranges", {}).get("validation") if not validation_range or not _weights_valid(fold.get("weights")): return False return True def validate_ml_artifact(artifact): """Return machine-readable validity and provenance for an ML artifact.""" errors = [] if not isinstance(artifact, dict): artifact = {} errors.append("artifact_object") schema_version = artifact.get("artifact_schema_version") if schema_version != ML_ARTIFACT_SCHEMA_VERSION: errors.append("artifact_schema_version") score_version = artifact.get("score_version") if score_version != SCORE_VERSION: errors.append("score_version") if not _weights_valid(artifact.get("weights")): errors.append("weights") provenance = artifact.get("provenance") if not isinstance(provenance, dict): provenance = {} errors.append("provenance") else: required_provenance = { "validation_method", "label_horizon_days", "weight_scope", "training_date_range", "trained_at", } if not required_provenance.issubset(provenance): errors.append("provenance") if provenance.get("validation_method") != "purged_expanding_window": errors.append("purged_validation") if provenance.get("label_horizon_days") != 365: errors.append("label_horizon_days") if provenance.get("weight_scope") != "full_history_fit": errors.append("weight_scope") return { "valid": not errors, "schema_version": schema_version, "score_version": score_version, "weight_scope": provenance.get("weight_scope"), "has_oos_fold_weights": _has_oos_fold_weights(artifact), "errors": list(dict.fromkeys(errors)), }