Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """Hard-rule checker for Stock Predictor factor configs and survey reports. | |
| This script is intentionally separate from historical validation. It catches | |
| configuration and harness-contract violations before a candidate is treated as | |
| production-adjacent. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| from typing import Any | |
| import numpy as np | |
| import pandas as pd | |
| ROOT = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(ROOT)) | |
| from models.multi_factor_overlay import build_runtime_signed_factors # noqa: E402 | |
| DEFAULT_OVERLAY_CONFIG = ROOT / "config" / "multi_factor_overlay.json" | |
| DEFAULT_COMBINE_SPLIT_REPORT = ROOT / "docs" / "validation_runs" / "factor_combine_split_strategy_survey_20260528.html" | |
| class CheckResult: | |
| def __init__(self) -> None: | |
| self.failures: list[str] = [] | |
| self.warnings: list[str] = [] | |
| self.passes: list[str] = [] | |
| def pass_(self, message: str) -> None: | |
| self.passes.append(message) | |
| def warn(self, message: str) -> None: | |
| self.warnings.append(message) | |
| def fail(self, message: str) -> None: | |
| self.failures.append(message) | |
| def ok(self) -> bool: | |
| return not self.failures | |
| def _load_json(path: Path) -> dict[str, Any]: | |
| try: | |
| return json.loads(path.read_text(encoding="utf-8")) | |
| except FileNotFoundError as exc: | |
| raise SystemExit(f"missing required file: {path}") from exc | |
| except json.JSONDecodeError as exc: | |
| raise SystemExit(f"invalid JSON in {path}: {exc}") from exc | |
| def _weights(payload: dict[str, Any]) -> dict[str, float]: | |
| selected = payload.get("selected_config") or payload.get("config") or {} | |
| raw = selected.get("weights") or {} | |
| return {str(name): float(value) for name, value in raw.items()} | |
| def _failed_gate_checks(payload: dict[str, Any]) -> list[str]: | |
| gate = payload.get("gate") or {} | |
| explicit = gate.get("failed_checks") | |
| if isinstance(explicit, list): | |
| return [str(item) for item in explicit] | |
| checks = gate.get("checks") or {} | |
| return [str(name) for name, passed in checks.items() if not bool(passed)] | |
| def _delta(payload: dict[str, Any], name: str, default: float = 0.0) -> float: | |
| try: | |
| return float(((payload.get("validation") or {}).get("deltas") or {}).get(name, default)) | |
| except Exception: | |
| return default | |
| def _max_zero_allowed(payload: dict[str, Any], default: int) -> int: | |
| thresholds = payload.get("selection_thresholds") or {} | |
| try: | |
| return int(thresholds.get("max_zero_factor_weights", default)) | |
| except Exception: | |
| return default | |
| def _check_runtime_support(weights: dict[str, float], result: CheckResult) -> None: | |
| closes = pd.Series(np.linspace(80.0, 120.0, 180)) | |
| df = pd.DataFrame( | |
| { | |
| "open": closes - 0.4, | |
| "high": closes + 1.2, | |
| "low": closes - 1.0, | |
| "close": closes, | |
| "volume": np.linspace(10000.0, 40000.0, len(closes)), | |
| } | |
| ) | |
| features = pd.DataFrame(index=df.index) | |
| try: | |
| factors = build_runtime_signed_factors(features, df, list(weights)) | |
| except Exception as exc: | |
| result.fail(f"runtime factor support failed: {exc}") | |
| return | |
| if list(factors.columns) != list(weights): | |
| result.fail("runtime factor output columns do not match config factor order") | |
| return | |
| result.pass_(f"runtime supports all {len(weights)} configured factors") | |
| def _check_big_player_factor_contract(weights: dict[str, float], result: CheckResult) -> None: | |
| legacy = { | |
| "big_player_power_1m_4m", | |
| "big_player_power_gt_4m", | |
| "big_player_buy_ratio_1m_4m", | |
| "big_player_sell_ratio_1m_4m", | |
| "big_player_buy_ratio_gt_4m", | |
| "big_player_sell_ratio_gt_4m", | |
| } | |
| quantity = {"big_player_buy", "big_player_sell"} | |
| present_legacy = sorted(legacy & set(weights)) | |
| present_quantity = sorted(quantity & set(weights)) | |
| if present_legacy: | |
| result.fail( | |
| "current overlay config must use explicit big-player buy/sell quantity strength, " | |
| f"not legacy ratio/signed fields: {', '.join(present_legacy)}" | |
| ) | |
| if not present_quantity: | |
| result.fail("current overlay config must include big_player_buy and big_player_sell quantity factors") | |
| elif set(present_quantity) != quantity: | |
| missing = sorted(quantity - set(present_quantity)) | |
| result.fail(f"big-player quantity factors must be configured as a complete buy/sell pair set; missing: {', '.join(missing)}") | |
| else: | |
| result.pass_("big-player factors use complete explicit buy/sell quantity strength set") | |
| def _check_engulf_factor_contract(weights: dict[str, float], result: CheckResult) -> None: | |
| if "bt_double_bull_engulf_one_bear" in weights: | |
| result.fail("current overlay config must use split engulf factors, not bt_double_bull_engulf_one_bear") | |
| split = {"bt_bull_engulf", "bt_bull_engulf_bear"} | |
| present = sorted(split & set(weights)) | |
| if not present: | |
| result.fail("current overlay config must include split bull engulf factors") | |
| elif set(present) != split: | |
| missing = sorted(split - set(present)) | |
| result.fail(f"bull engulf split factors must be configured together; missing: {', '.join(missing)}") | |
| else: | |
| result.pass_("bull engulf factors are split into bullish and bearish-risk signals") | |
| def check_overlay_config(path: Path, *, max_zero_default: int) -> CheckResult: | |
| result = CheckResult() | |
| payload = _load_json(path) | |
| weights = _weights(payload) | |
| if not weights: | |
| result.fail("overlay config has no selected_config.weights") | |
| return result | |
| out_of_range = [name for name, value in weights.items() if value < 0.0 or value > 1.0] | |
| if out_of_range: | |
| result.fail(f"weights out of [0, 1] range: {', '.join(out_of_range)}") | |
| else: | |
| result.pass_("all weights are inside [0, 1]") | |
| zero_weights = [name for name, value in weights.items() if abs(value) <= 1e-12] | |
| max_zero = _max_zero_allowed(payload, max_zero_default) | |
| if len(zero_weights) > max_zero: | |
| result.fail(f"zero-weight count {len(zero_weights)} exceeds max {max_zero}: {', '.join(zero_weights)}") | |
| else: | |
| result.pass_(f"zero-weight count {len(zero_weights)} <= max {max_zero}") | |
| _check_big_player_factor_contract(weights, result) | |
| _check_engulf_factor_contract(weights, result) | |
| status = str(payload.get("integration_status") or "") | |
| strict_promoted = bool(payload.get("strict_promoted")) | |
| failed = _failed_gate_checks(payload) | |
| sell_delta = _delta(payload, "sell_precision_delta_pp") | |
| buy_delta = _delta(payload, "buy_precision_delta_pp") | |
| if status == "strict_golden" or strict_promoted: | |
| if failed: | |
| result.fail(f"strict golden config has failed checks: {', '.join(failed)}") | |
| if sell_delta < 0: | |
| result.fail(f"strict golden config has negative SELL precision delta: {sell_delta}pp") | |
| if buy_delta < 0: | |
| result.fail(f"strict golden config has negative BUY precision delta: {buy_delta}pp") | |
| if not payload.get("strict_source_has_golden_config"): | |
| result.fail("strict golden config does not cite a source golden config") | |
| if result.ok: | |
| result.pass_("strict golden promotion gates are internally consistent") | |
| else: | |
| if failed: | |
| result.pass_(f"failed gates are contained in non-strict config: {', '.join(failed)}") | |
| if sell_delta < 0 and status != "candidate_only": | |
| result.fail("negative SELL precision delta must remain candidate_only") | |
| if sell_delta < 0 and status == "candidate_only": | |
| note = str(((payload.get("runtime") or {}).get("note") or "")).lower() | |
| if "sell" not in note and "candidate" not in note: | |
| result.warn("candidate_only config has negative SELL precision but runtime note does not explain it") | |
| result.pass_("negative SELL precision delta is not promoted as strict golden") | |
| _check_runtime_support(weights, result) | |
| return result | |
| def check_combine_split_report(path: Path) -> CheckResult: | |
| result = CheckResult() | |
| if not path.exists(): | |
| result.warn(f"combine/split report not found: {path}") | |
| return result | |
| text = path.read_text(encoding="utf-8") | |
| required = [ | |
| "Combine / Split Probability", | |
| "新增 Related Factor Survey", | |
| "Validation Plan", | |
| "Source Evidence", | |
| "SELL precision", | |
| "ADX trend regime gate", | |
| "exhaustion_cluster_confirmed", | |
| ] | |
| missing = [item for item in required if item not in text] | |
| if missing: | |
| result.fail(f"combine/split report missing required sections/terms: {', '.join(missing)}") | |
| else: | |
| result.pass_("combine/split report includes required sections and P0 recommendations") | |
| return result | |
| def merge_results(results: list[CheckResult]) -> CheckResult: | |
| merged = CheckResult() | |
| for item in results: | |
| merged.failures.extend(item.failures) | |
| merged.warnings.extend(item.warnings) | |
| merged.passes.extend(item.passes) | |
| return merged | |
| def parse_args(argv: list[str] | None = None) -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="Check Stock Predictor factor harness hard rules.") | |
| parser.add_argument("--overlay-config", default=str(DEFAULT_OVERLAY_CONFIG)) | |
| parser.add_argument("--combine-split-report", default=str(DEFAULT_COMBINE_SPLIT_REPORT)) | |
| parser.add_argument("--max-zero-factor-weights", type=int, default=5) | |
| return parser.parse_args(argv) | |
| def main(argv: list[str] | None = None) -> int: | |
| args = parse_args(argv) | |
| overlay = Path(args.overlay_config) | |
| if not overlay.is_absolute(): | |
| overlay = ROOT / overlay | |
| report = Path(args.combine_split_report) | |
| if not report.is_absolute(): | |
| report = ROOT / report | |
| result = merge_results( | |
| [ | |
| check_overlay_config(overlay, max_zero_default=args.max_zero_factor_weights), | |
| check_combine_split_report(report), | |
| ] | |
| ) | |
| print("Stock Predictor harness rule check") | |
| for message in result.passes: | |
| print(f"PASS: {message}") | |
| for message in result.warnings: | |
| print(f"WARN: {message}") | |
| for message in result.failures: | |
| print(f"FAIL: {message}") | |
| print("RESULT:", "PASS" if result.ok else "FAIL") | |
| return 0 if result.ok else 1 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |