feat: 리밸런싱 엔진 V1 + GAS 버그 수정 (2026-06-13)
주요 변경: - tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규 * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합 * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일) - src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규 * Logger.log / getSpreadsheet_() 로 run_all 연동 수정 - src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs * _mergePositionRecord_(): 소수주 중복 행 합산 신규 * parseInt → parseFloat (qty, availQty) - src/gas_adapter_parts/gdf_01_price_metrics.gs * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL - spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63) - spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,671 @@
|
||||
"""
|
||||
validate_alpha_execution_harness.py
|
||||
|
||||
APEX Alpha Preservation Execution Harness V1/V5 전용 검증기.
|
||||
|
||||
기본 validate_harness_context.py는 기존 JSON 호환성을 위해 APEX 필드를 optional로 본다.
|
||||
이 도구는 GAS/Harness V5 전환 후 APEX 필드를 의무 검증하거나, 부분 도입 상태를 감사할 때 사용한다.
|
||||
|
||||
Usage:
|
||||
python tools/validate_alpha_execution_harness.py <json> [--strict]
|
||||
python tools/validate_alpha_execution_harness.py <json> --check breakout_quality_gate
|
||||
python tools/validate_alpha_execution_harness.py <json> --check anti_whipsaw_gate
|
||||
python tools/validate_alpha_execution_harness.py <json> --check smart_cash_raise_v2
|
||||
python tools/validate_alpha_execution_harness.py <json> --check determinism
|
||||
python tools/validate_alpha_execution_harness.py <json> --check cla_harness
|
||||
|
||||
cla_harness 체크 항목 (Section 13):
|
||||
[1] market_regime_state enum 검증 (ADVANCE|PULLBACK_IN_UPTREND|DISTRIBUTION|BREAKDOWN|UNKNOWN)
|
||||
[2] CLA(CLUSTER_HOLD_ONLY) 레짐 시 코어 종목(005930/000660/229200) SELL 차단 (REGIME_CLA-1)
|
||||
[3] semiconductor_cluster_json.cluster_state enum 검증
|
||||
[4] buy_permission_json rs_verdict enum 검증 (LEADER|MARKET|LAGGARD|BROKEN|UNKNOWN)
|
||||
[5] buy_permission_json composite_verdict enum 검증 (5가지 판정값)
|
||||
[6] satellite_failure_gate_json.sfg_v1 enum 검증 (TRIGGERED|CLEAR)
|
||||
[7] sfg_v1=TRIGGERED 시 위성 ALLOW_* 차단 (SFG-2)
|
||||
[8] buy_permission_json.rag_v1 enum 검증 (PASS|FAIL|EXEMPT)
|
||||
[9] rag_v1=FAIL 시 ALLOW_* 불일치 오류 (RAG-2)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
VALID_CHECK_MODES = {"breakout_quality_gate", "anti_whipsaw_gate", "smart_cash_raise_v2", "determinism", "cla_harness", "brt_harness", "factor_cap"}
|
||||
|
||||
REQUIRED_V5_KEYS = [
|
||||
"breakout_quality_gate_json",
|
||||
"breakout_quality_gate_lock",
|
||||
"anti_whipsaw_gate_json",
|
||||
"anti_whipsaw_gate_lock",
|
||||
"smart_cash_raise_json",
|
||||
"smart_cash_raise_route",
|
||||
]
|
||||
|
||||
REQUIRED_APEX_KEYS = [
|
||||
"alpha_lead_lock",
|
||||
"alpha_lead_json",
|
||||
"follow_through_lock",
|
||||
"follow_through_json",
|
||||
"distribution_lock",
|
||||
"distribution_risk_json",
|
||||
"profit_preservation_lock",
|
||||
"profit_preservation_json",
|
||||
"smart_cash_raise_lock",
|
||||
"cash_raise_plan_json",
|
||||
"rebound_sell_trigger_json",
|
||||
"smart_sell_quantities_json",
|
||||
"execution_quality_lock",
|
||||
"execution_quality_json",
|
||||
"buy_permission_json",
|
||||
"limit_price_policy_json",
|
||||
"alpha_feedback_json",
|
||||
]
|
||||
|
||||
BUY_ACTIONS = {"BUY", "STAGED_BUY", "ADD_ON"}
|
||||
VALID_BUY_PERMISSION = {"ALLOW_PILOT", "ALLOW_ADD_ON", "WATCH", "BLOCKED"}
|
||||
|
||||
# ── [2026-05-21_CLA_HARNESS_V1] Section 13 체크 상수 ──────────────────────────
|
||||
VALID_MARKET_REGIME_STATES = {
|
||||
"ADVANCE", "PULLBACK_IN_UPTREND", "DISTRIBUTION", "BREAKDOWN", "UNKNOWN"
|
||||
}
|
||||
VALID_RS_VERDICTS = {"LEADER", "MARKET", "LAGGARD", "BROKEN", "UNKNOWN"}
|
||||
VALID_COMPOSITE_VERDICTS = {
|
||||
"PRIME_CANDIDATE", "WATCH_CANDIDATE", "REDUCE_CANDIDATE", "EXIT_REVIEW", "CLOSE_POSITION"
|
||||
}
|
||||
VALID_BRT_VERDICTS = {"LEADER", "MARKET", "LAGGARD", "BROKEN", "UNKNOWN"}
|
||||
VALID_SAQG_STATES = {"ELIGIBLE", "WATCHLIST_ONLY", "EXCLUDED", "EXEMPT"}
|
||||
VALID_SAPG_STATUSES = {"PASS", "SAPG_ALERT", "SAPG_CRITICAL", "INSUFFICIENT_DATA"}
|
||||
SEMICONDUCTOR_CORE_TICKERS = {"005930", "000660", "229200"} # 삼성전자, SK하이닉스, KODEX반도체
|
||||
|
||||
|
||||
def load_harness(path: Path) -> dict[str, Any]:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
if isinstance(payload, dict) and isinstance(payload.get("data"), dict):
|
||||
maybe = payload["data"].get("_harness_context")
|
||||
if isinstance(maybe, dict):
|
||||
return maybe
|
||||
return payload
|
||||
|
||||
|
||||
def parse_jsonish(value: Any) -> Any:
|
||||
if isinstance(value, (list, dict)):
|
||||
return value
|
||||
if isinstance(value, str) and value.strip():
|
||||
return json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
def to_number(value: Any) -> float | None:
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
if isinstance(value, str):
|
||||
text = value.strip()
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def as_rows(harness: dict[str, Any], key: str, errors: list[str]) -> list[dict[str, Any]]:
|
||||
value = parse_jsonish(harness.get(key))
|
||||
if not isinstance(value, list):
|
||||
errors.append(f"{key}: must be a list")
|
||||
return []
|
||||
rows: list[dict[str, Any]] = []
|
||||
for idx, item in enumerate(value):
|
||||
if not isinstance(item, dict):
|
||||
errors.append(f"{key}[{idx}]: must be an object")
|
||||
continue
|
||||
rows.append(item)
|
||||
return rows
|
||||
|
||||
|
||||
def validate_required_keys(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
for key in REQUIRED_APEX_KEYS:
|
||||
if key not in harness:
|
||||
errors.append(f"missing APEX key: {key}")
|
||||
|
||||
|
||||
def validate_buy_blocks(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
permissions = as_rows(harness, "buy_permission_json", errors)
|
||||
permission_by_ticker = {str(r.get("ticker")): r for r in permissions if r.get("ticker")}
|
||||
for idx, row in enumerate(permissions):
|
||||
state = row.get("buy_permission_state")
|
||||
if state not in VALID_BUY_PERMISSION:
|
||||
errors.append(f"buy_permission_json[{idx}].buy_permission_state invalid: {state!r}")
|
||||
tranche = row.get("max_tranche_pct")
|
||||
if state == "ALLOW_PILOT" and isinstance(tranche, (int, float)) and tranche > 30:
|
||||
errors.append(f"buy_permission_json[{idx}].max_tranche_pct exceeds 30 for ALLOW_PILOT")
|
||||
late_chase = to_number(row.get("late_chase_risk_score"))
|
||||
if late_chase is not None and not (0 <= late_chase <= 100):
|
||||
errors.append(f"buy_permission_json[{idx}].late_chase_risk_score must be in [0,100]")
|
||||
follow_score = to_number(row.get("follow_through_score"))
|
||||
if follow_score is not None and not (0 <= follow_score <= 100):
|
||||
errors.append(f"buy_permission_json[{idx}].follow_through_score must be in [0,100]")
|
||||
|
||||
orders = parse_jsonish(harness.get("order_blueprint_json"))
|
||||
if isinstance(orders, list):
|
||||
for idx, order in enumerate(orders):
|
||||
if not isinstance(order, dict):
|
||||
continue
|
||||
ticker = str(order.get("ticker") or "")
|
||||
order_type = str(order.get("order_type") or "")
|
||||
validation = str(order.get("validation_status") or "")
|
||||
state = (permission_by_ticker.get(ticker) or {}).get("buy_permission_state")
|
||||
if order_type in BUY_ACTIONS and validation == "PASS" and state not in {"ALLOW_PILOT", "ALLOW_ADD_ON"}:
|
||||
errors.append(
|
||||
f"order_blueprint_json[{idx}]: BUY PASS emitted while buy_permission_state={state!r}"
|
||||
)
|
||||
|
||||
|
||||
def validate_distribution_blocks(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
distribution = as_rows(harness, "distribution_risk_json", errors)
|
||||
blocked = {
|
||||
str(row.get("ticker"))
|
||||
for row in distribution
|
||||
if row.get("anti_distribution_state") == "BLOCK_BUY"
|
||||
or (isinstance(row.get("distribution_risk_score"), (int, float)) and row["distribution_risk_score"] >= 70)
|
||||
}
|
||||
orders = parse_jsonish(harness.get("order_blueprint_json"))
|
||||
if isinstance(orders, list):
|
||||
for idx, order in enumerate(orders):
|
||||
if not isinstance(order, dict):
|
||||
continue
|
||||
if str(order.get("ticker")) in blocked and str(order.get("order_type")) in BUY_ACTIONS:
|
||||
errors.append(f"order_blueprint_json[{idx}]: BUY action exists for distribution BLOCK_BUY ticker")
|
||||
|
||||
|
||||
def validate_cash_raise(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
rows = as_rows(harness, "cash_raise_plan_json", errors)
|
||||
for idx, row in enumerate(rows):
|
||||
style = row.get("execution_style")
|
||||
immediate = row.get("immediate_qty")
|
||||
rebound = row.get("rebound_wait_qty")
|
||||
cap_pct = row.get("immediate_qty_cap_pct")
|
||||
if immediate is not None and not isinstance(immediate, int):
|
||||
errors.append(f"cash_raise_plan_json[{idx}].immediate_qty must be integer or null")
|
||||
if rebound is not None and not isinstance(rebound, int):
|
||||
errors.append(f"cash_raise_plan_json[{idx}].rebound_wait_qty must be integer or null")
|
||||
if cap_pct is not None and not isinstance(cap_pct, int):
|
||||
errors.append(f"cash_raise_plan_json[{idx}].immediate_qty_cap_pct must be integer or null")
|
||||
emergency = row.get("emergency_full_sell") is True
|
||||
if style == "OVERSOLD_REBOUND_SELL" and not (isinstance(rebound, int) and rebound > 0) and not emergency:
|
||||
errors.append(
|
||||
f"cash_raise_plan_json[{idx}]: OVERSOLD_REBOUND_SELL requires rebound_wait_qty > 0 "
|
||||
f"unless emergency_full_sell=true"
|
||||
)
|
||||
|
||||
|
||||
def validate_execution_quality(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
quality_rows = as_rows(harness, "execution_quality_json", errors)
|
||||
quality_by_ticker = {str(row.get("ticker")): row for row in quality_rows if row.get("ticker")}
|
||||
for idx, row in enumerate(quality_rows):
|
||||
status = row.get("execution_quality_status")
|
||||
if status not in {"PASS", "BLOCKED", "BLOCKED_ADV_3PCT", "SPLIT_REQUIRED", "BLOCKED_SPREAD"}:
|
||||
errors.append(f"execution_quality_json[{idx}].execution_quality_status invalid: {status!r}")
|
||||
orders = parse_jsonish(harness.get("order_blueprint_json"))
|
||||
if isinstance(orders, list):
|
||||
for idx, order in enumerate(orders):
|
||||
if not isinstance(order, dict):
|
||||
continue
|
||||
if str(order.get("validation_status")) != "PASS":
|
||||
continue
|
||||
ticker = str(order.get("ticker") or "")
|
||||
quality = quality_by_ticker.get(ticker)
|
||||
if quality and quality.get("execution_quality_status") != "PASS":
|
||||
errors.append(
|
||||
f"order_blueprint_json[{idx}]: PASS order while execution_quality_status={quality.get('execution_quality_status')!r}"
|
||||
)
|
||||
for idx, row in enumerate(quality_rows):
|
||||
split_count = row.get("split_count")
|
||||
if split_count is not None and not isinstance(split_count, int):
|
||||
errors.append(f"execution_quality_json[{idx}].split_count must be integer or null")
|
||||
|
||||
|
||||
def validate_alpha_feedback_loop(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
payload = parse_jsonish(harness.get("alpha_feedback_json"))
|
||||
if not isinstance(payload, dict):
|
||||
errors.append("alpha_feedback_json: must be an object")
|
||||
return
|
||||
if payload.get("formula_id") != "ALPHA_FEEDBACK_LOOP_V1":
|
||||
errors.append(f"alpha_feedback_json.formula_id must be ALPHA_FEEDBACK_LOOP_V1, found={payload.get('formula_id')!r}")
|
||||
if not isinstance(payload.get("cases_analyzed"), int) or payload["cases_analyzed"] < 0:
|
||||
errors.append("alpha_feedback_json.cases_analyzed must be a non-negative integer")
|
||||
if not isinstance(payload.get("grade_count"), int) or payload["grade_count"] < 0:
|
||||
errors.append("alpha_feedback_json.grade_count must be a non-negative integer")
|
||||
if payload.get("status") not in {"ANALYZED", "DATA_MISSING", "DATA_INSUFFICIENT"}:
|
||||
errors.append(f"alpha_feedback_json.status invalid: {payload.get('status')!r}")
|
||||
if payload.get("recommended_filter_adjustments") is not None and not isinstance(payload.get("recommended_filter_adjustments"), list):
|
||||
errors.append("alpha_feedback_json.recommended_filter_adjustments must be a list")
|
||||
if payload.get("grade_summary") is not None and not isinstance(payload.get("grade_summary"), list):
|
||||
errors.append("alpha_feedback_json.grade_summary must be a list")
|
||||
if payload.get("status") == "ANALYZED" and payload.get("cases_analyzed", 0) < 10:
|
||||
errors.append("alpha_feedback_json: ANALYZED requires cases_analyzed >= 10")
|
||||
|
||||
|
||||
# ── [2026-05-20_HARNESS_V5] 신규 검증 함수 ──────────────────────────────────
|
||||
|
||||
def validate_breakout_quality_gate(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
"""H6: BREAKOUT_QUALITY_GATE_V2 — 뒷박 차단 게이트 검증."""
|
||||
if "breakout_quality_gate_json" not in harness:
|
||||
errors.append("missing V5 key: breakout_quality_gate_json")
|
||||
return
|
||||
rows = as_rows(harness, "breakout_quality_gate_json", errors)
|
||||
valid_gates = {"PILOT_ALLOWED", "WATCH_COOLING_OFF", "BLOCKED_LATE_CHASE"}
|
||||
blocked_tickers: set[str] = set()
|
||||
for idx, row in enumerate(rows):
|
||||
gate = row.get("breakout_quality_gate")
|
||||
score = row.get("breakout_quality_score")
|
||||
if gate not in valid_gates:
|
||||
errors.append(f"breakout_quality_gate_json[{idx}].breakout_quality_gate invalid: {gate!r}")
|
||||
if score is not None and not (0 <= float(score) <= 100):
|
||||
errors.append(f"breakout_quality_gate_json[{idx}].breakout_quality_score must be 0-100")
|
||||
if gate == "BLOCKED_LATE_CHASE":
|
||||
blocked_tickers.add(str(row.get("ticker") or ""))
|
||||
orders = parse_jsonish(harness.get("order_blueprint_json"))
|
||||
if isinstance(orders, list):
|
||||
for idx, order in enumerate(orders):
|
||||
if not isinstance(order, dict):
|
||||
continue
|
||||
if str(order.get("ticker")) in blocked_tickers and str(order.get("order_type")) in BUY_ACTIONS:
|
||||
errors.append(
|
||||
f"order_blueprint_json[{idx}]: BUY exists for BLOCKED_LATE_CHASE ticker (QEH009)"
|
||||
)
|
||||
|
||||
|
||||
def validate_anti_whipsaw_gate(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
"""H7: ANTI_WHIPSAW_HOLD_GATE_V1 — 가짜 매도 차단 게이트 검증."""
|
||||
if "anti_whipsaw_gate_json" not in harness:
|
||||
errors.append("missing V5 key: anti_whipsaw_gate_json")
|
||||
return
|
||||
rows = as_rows(harness, "anti_whipsaw_gate_json", errors)
|
||||
valid_gates = {
|
||||
"WHIPSAW_SUSPECTED",
|
||||
"INCONCLUSIVE",
|
||||
"CONFIRMED_SELL",
|
||||
"WHIPSAW_CONFIRMED",
|
||||
"WHIPSAW_WEAKENING",
|
||||
"WHIPSAW_AUTO_RELEASED",
|
||||
}
|
||||
whipsaw_tickers: set[str] = set()
|
||||
for idx, row in enumerate(rows):
|
||||
gate = row.get("anti_whipsaw_gate")
|
||||
score = row.get("anti_whipsaw_score")
|
||||
hold_days = row.get("anti_whipsaw_hold_days")
|
||||
if gate not in valid_gates:
|
||||
errors.append(f"anti_whipsaw_gate_json[{idx}].anti_whipsaw_gate invalid: {gate!r}")
|
||||
if score is not None and not (-50 <= float(score) <= 100):
|
||||
errors.append(f"anti_whipsaw_gate_json[{idx}].anti_whipsaw_score must be in [-50,100]")
|
||||
if gate == "WHIPSAW_SUSPECTED" and hold_days != 1:
|
||||
errors.append(f"anti_whipsaw_gate_json[{idx}]: WHIPSAW_SUSPECTED must have hold_days=1")
|
||||
if gate == "WHIPSAW_SUSPECTED":
|
||||
whipsaw_tickers.add(str(row.get("ticker") or ""))
|
||||
orders = parse_jsonish(harness.get("order_blueprint_json"))
|
||||
SELL_ACTIONS = {"SELL", "TRIM", "EXIT_100", "EXIT_FULL"}
|
||||
if isinstance(orders, list):
|
||||
for idx, order in enumerate(orders):
|
||||
if not isinstance(order, dict):
|
||||
continue
|
||||
ticker = str(order.get("ticker") or "")
|
||||
order_type = str(order.get("order_type") or "")
|
||||
qty = order.get("quantity")
|
||||
if (ticker in whipsaw_tickers and order_type in SELL_ACTIONS
|
||||
and str(order.get("validation_status")) == "PASS"):
|
||||
errors.append(
|
||||
f"order_blueprint_json[{idx}]: full SELL emitted for WHIPSAW_SUSPECTED ticker (QEH010)"
|
||||
)
|
||||
|
||||
reentry_rows = parse_jsonish(harness.get("anti_whipsaw_reentry_json"))
|
||||
if isinstance(reentry_rows, list):
|
||||
for idx, row in enumerate(reentry_rows):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
tier = row.get("sell_tier")
|
||||
if tier not in {1, 2}:
|
||||
errors.append(
|
||||
f"anti_whipsaw_reentry_json[{idx}]: sell_tier must be 1 or 2, found={tier!r} (QEH010-TIER)"
|
||||
)
|
||||
signal = row.get("reentry_signal")
|
||||
if signal not in {"REENTRY_CANDIDATE"}:
|
||||
errors.append(
|
||||
f"anti_whipsaw_reentry_json[{idx}].reentry_signal invalid: {signal!r}"
|
||||
)
|
||||
|
||||
|
||||
def validate_smart_cash_raise_v2(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
"""H8: SMART_CASH_RAISE_V2 — 4경로 현금확보 라우터 검증."""
|
||||
if "smart_cash_raise_json" not in harness:
|
||||
errors.append("missing V5 key: smart_cash_raise_json")
|
||||
return
|
||||
rows = as_rows(harness, "smart_cash_raise_json", errors)
|
||||
valid_routes = {"ROUTE_A", "ROUTE_B", "ROUTE_C", "ROUTE_D", "NO_ACTION"}
|
||||
portfolio_route = str(harness.get("smart_cash_raise_route") or "NO_ACTION")
|
||||
if portfolio_route not in valid_routes:
|
||||
errors.append(f"smart_cash_raise_route invalid: {portfolio_route!r}")
|
||||
for idx, row in enumerate(rows):
|
||||
route = row.get("smart_cash_raise_route")
|
||||
if route not in valid_routes:
|
||||
errors.append(f"smart_cash_raise_json[{idx}].smart_cash_raise_route invalid: {route!r}")
|
||||
rebound_wait = row.get("rebound_wait_pct")
|
||||
if route == "ROUTE_B" and rebound_wait != 50:
|
||||
errors.append(f"smart_cash_raise_json[{idx}]: ROUTE_B must have rebound_wait_pct=50")
|
||||
if route == "ROUTE_D":
|
||||
rationale = str(row.get("rationale") or "")
|
||||
emergency = row.get("emergency_full_sell") is True
|
||||
stop_gate = str(row.get("stop_breach_gate") or "")
|
||||
if not emergency and stop_gate != "BREACH" and (
|
||||
"emergency" not in rationale.lower() and "breach" not in rationale.lower()
|
||||
):
|
||||
errors.append(
|
||||
f"smart_cash_raise_json[{idx}]: ROUTE_D requires emergency or breach rationale (QEH011)"
|
||||
)
|
||||
|
||||
|
||||
def validate_cla_harness(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
"""CLA_HARNESS_V1: CLA 레짐 위성 실패 게이트·RAG·RS 판정 검증."""
|
||||
# ── [Section 13-1] market_regime_state enum 검증 ─────────────────────────
|
||||
regime_state = harness.get("market_regime_state")
|
||||
if regime_state is not None and regime_state not in VALID_MARKET_REGIME_STATES:
|
||||
errors.append(
|
||||
f"market_regime_state invalid: {regime_state!r} "
|
||||
f"(expected one of {sorted(VALID_MARKET_REGIME_STATES)})"
|
||||
)
|
||||
|
||||
# ── [Section 13-3] semiconductor_cluster_json cluster_state enum 검증 ────
|
||||
semi_json = parse_jsonish(harness.get("semiconductor_cluster_json"))
|
||||
cluster_state: str | None = None
|
||||
if isinstance(semi_json, dict):
|
||||
cluster_state = str(semi_json.get("cluster_state") or "")
|
||||
if cluster_state not in {"CLUSTER_HOLD_ONLY", "CLUSTER_OPEN", "CLUSTER_BLOCK", ""}:
|
||||
errors.append(
|
||||
f"semiconductor_cluster_json.cluster_state invalid: {cluster_state!r} "
|
||||
"(expected CLUSTER_HOLD_ONLY | CLUSTER_OPEN | CLUSTER_BLOCK)"
|
||||
)
|
||||
|
||||
# ── [Section 13-2] CLA 레짐 시 코어 종목 SELL 차단 (REGIME_CLA-1) ────────
|
||||
if cluster_state == "CLUSTER_HOLD_ONLY":
|
||||
decisions = parse_jsonish(harness.get("decisions_json"))
|
||||
if isinstance(decisions, list):
|
||||
for idx, dec in enumerate(decisions):
|
||||
if not isinstance(dec, dict):
|
||||
continue
|
||||
ticker = str(dec.get("ticker") or "")
|
||||
final_action = str(dec.get("final_action") or "").upper()
|
||||
if ticker in SEMICONDUCTOR_CORE_TICKERS and final_action == "SELL":
|
||||
errors.append(
|
||||
f"decisions_json[{idx}] ticker={ticker}: SELL emitted for core "
|
||||
"semiconductor in CLA (CLUSTER_HOLD_ONLY) regime — REGIME_CLA-1 violation"
|
||||
)
|
||||
|
||||
# ── [Section 13-4/5] buy_permission_json per-row rs_verdict/composite_verdict enum ─
|
||||
permissions = as_rows(harness, "buy_permission_json", []) # 별도 errors 수집 불필요
|
||||
for idx, bp in enumerate(permissions):
|
||||
rv = bp.get("rs_verdict")
|
||||
if rv is not None and rv not in VALID_RS_VERDICTS:
|
||||
errors.append(
|
||||
f"buy_permission_json[{idx}].rs_verdict invalid: {rv!r} "
|
||||
f"(expected one of {sorted(VALID_RS_VERDICTS)})"
|
||||
)
|
||||
cv = bp.get("composite_verdict")
|
||||
if cv is not None and cv not in VALID_COMPOSITE_VERDICTS:
|
||||
errors.append(
|
||||
f"buy_permission_json[{idx}].composite_verdict invalid: {cv!r} "
|
||||
f"(expected one of {sorted(VALID_COMPOSITE_VERDICTS)})"
|
||||
)
|
||||
|
||||
# SFG-1: satellite_failure_gate_json 존재 및 sfg_v1 유효값 확인
|
||||
if "satellite_failure_gate_json" not in harness:
|
||||
errors.append("missing CLA key: satellite_failure_gate_json")
|
||||
else:
|
||||
sfg = parse_jsonish(harness.get("satellite_failure_gate_json"))
|
||||
if not isinstance(sfg, dict):
|
||||
errors.append("satellite_failure_gate_json: must be an object")
|
||||
else:
|
||||
sfg_v1 = sfg.get("sfg_v1")
|
||||
if sfg_v1 not in {"TRIGGERED", "CLEAR"}:
|
||||
errors.append(f"satellite_failure_gate_json.sfg_v1 invalid: {sfg_v1!r} (expected TRIGGERED|CLEAR)")
|
||||
# SFG-2: TRIGGERED이면 위성 ALLOW_* buy_permission 없어야 함
|
||||
if sfg_v1 == "TRIGGERED":
|
||||
permissions = as_rows(harness, "buy_permission_json", errors)
|
||||
for idx, bp in enumerate(permissions):
|
||||
state = str(bp.get("buy_permission_state") or "")
|
||||
pos_type = str(bp.get("position_type") or bp.get("cluster_label") or "")
|
||||
is_satellite = pos_type.lower() in {"satellite", "위성"} or (
|
||||
pos_type == "" and bp.get("core_flag") is False
|
||||
)
|
||||
if is_satellite and state.startswith("ALLOW_"):
|
||||
errors.append(
|
||||
f"buy_permission_json[{idx}]: satellite ALLOW_* ({state!r}) emitted while sfg_v1=TRIGGERED (SFG-2)"
|
||||
)
|
||||
|
||||
# RAG-1: buy_permission_json RAG 필드 일관성
|
||||
permissions = as_rows(harness, "buy_permission_json", errors)
|
||||
for idx, bp in enumerate(permissions):
|
||||
rag = bp.get("rag_v1")
|
||||
if rag is not None and rag not in {"PASS", "FAIL", "EXEMPT"}:
|
||||
errors.append(f"buy_permission_json[{idx}].rag_v1 invalid: {rag!r}")
|
||||
# RAG-2: FAIL인데 ALLOW_* 상태면 오류
|
||||
if rag == "FAIL" and str(bp.get("buy_permission_state") or "").startswith("ALLOW_"):
|
||||
errors.append(
|
||||
f"buy_permission_json[{idx}]: rag_v1=FAIL but buy_permission_state=ALLOW_* (RAG-2)"
|
||||
)
|
||||
|
||||
# RS-1: decisions_json 내 rs_verdict 존재 여부 — data_feed 시트 컬럼으로 관리되므로 경고만 출력
|
||||
decisions = parse_jsonish(harness.get("decisions_json"))
|
||||
if isinstance(decisions, list):
|
||||
missing_rs = sum(
|
||||
1 for d in decisions
|
||||
if isinstance(d, dict) and "rs_verdict" not in d and "composite_verdict" not in d
|
||||
)
|
||||
if missing_rs == len(decisions) and len(decisions) > 0:
|
||||
# 전체 누락 시에만 경고 (data_feed 시트가 아직 갱신되지 않은 경우)
|
||||
print(f"[WARN] RS-1: decisions_json {missing_rs}/{len(decisions)} rows lack rs_verdict (GAS re-run needed)")
|
||||
|
||||
|
||||
def validate_determinism(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
"""결정론 검증: 하네스 락 필드와 JSON 출력 일관성 확인."""
|
||||
lock_json_pairs = [
|
||||
("breakout_quality_gate_lock", "breakout_quality_gate_json"),
|
||||
("anti_whipsaw_gate_lock", "anti_whipsaw_gate_json"),
|
||||
("alpha_lead_lock", "alpha_lead_json"),
|
||||
("distribution_lock", "distribution_risk_json"),
|
||||
("profit_preservation_lock", "profit_preservation_json"),
|
||||
("smart_cash_raise_lock", "cash_raise_plan_json"),
|
||||
("execution_quality_lock", "execution_quality_json"),
|
||||
]
|
||||
for lock_key, json_key in lock_json_pairs:
|
||||
lock_val = harness.get(lock_key)
|
||||
if str(lock_val).lower() == "true":
|
||||
json_val = parse_jsonish(harness.get(json_key))
|
||||
if json_val is None:
|
||||
errors.append(f"determinism: {lock_key}=true but {json_key} is missing")
|
||||
elif isinstance(json_val, list) and len(json_val) == 0:
|
||||
errors.append(f"determinism: {lock_key}=true but {json_key} is empty list")
|
||||
decision_lock = harness.get("decision_lock")
|
||||
if str(decision_lock).lower() == "true":
|
||||
decisions = parse_jsonish(harness.get("decisions_json"))
|
||||
trace = parse_jsonish(harness.get("decision_trace_json"))
|
||||
if isinstance(decisions, list) and isinstance(trace, list):
|
||||
final_map: dict[str, set] = {}
|
||||
for d in decisions:
|
||||
if isinstance(d, dict) and d.get("ticker"):
|
||||
final_map.setdefault(str(d["ticker"]), set()).add(d.get("final_action"))
|
||||
for idx, t in enumerate(trace):
|
||||
if not isinstance(t, dict):
|
||||
continue
|
||||
ticker = str(t.get("ticker") or "")
|
||||
selected = t.get("selected_action")
|
||||
allowed = final_map.get(ticker)
|
||||
if selected and allowed and selected not in allowed:
|
||||
errors.append(
|
||||
f"determinism: decision_trace[{idx}].selected_action={selected!r} not in decisions_json.final_action={sorted(allowed)!r}"
|
||||
)
|
||||
|
||||
|
||||
def validate_brt_harness(harness: dict[str, Any], errors: list[str]) -> None:
|
||||
brt = parse_jsonish(harness.get("benchmark_relative_timeseries_json"))
|
||||
if not isinstance(brt, list):
|
||||
errors.append("missing/invalid BRT key: benchmark_relative_timeseries_json")
|
||||
else:
|
||||
for idx, row in enumerate(brt):
|
||||
if not isinstance(row, dict):
|
||||
errors.append(f"benchmark_relative_timeseries_json[{idx}]: must be an object")
|
||||
continue
|
||||
verdict = row.get("brt_verdict")
|
||||
if verdict not in VALID_BRT_VERDICTS:
|
||||
errors.append(f"benchmark_relative_timeseries_json[{idx}].brt_verdict invalid: {verdict!r}")
|
||||
|
||||
index_rows = parse_jsonish(harness.get("index_relative_health_json"))
|
||||
if not isinstance(index_rows, list):
|
||||
errors.append("missing/invalid BRT key: index_relative_health_json")
|
||||
else:
|
||||
for idx, row in enumerate(index_rows):
|
||||
if not isinstance(row, dict):
|
||||
errors.append(f"index_relative_health_json[{idx}]: must be an object")
|
||||
continue
|
||||
state = row.get("relative_health_state")
|
||||
if state not in {"HEALTHY", "OVER_EXTENDED", "UNDERPERFORMING", "DECOUPLED", "INSUFFICIENT_DATA"}:
|
||||
errors.append(f"index_relative_health_json[{idx}].relative_health_state invalid: {state!r}")
|
||||
|
||||
saqg = parse_jsonish(harness.get("saqg_json"))
|
||||
if not isinstance(saqg, list):
|
||||
errors.append("missing/invalid BRT key: saqg_json")
|
||||
else:
|
||||
for idx, row in enumerate(saqg):
|
||||
if not isinstance(row, dict):
|
||||
errors.append(f"saqg_json[{idx}]: must be an object")
|
||||
continue
|
||||
state = row.get("saqg_v1")
|
||||
if state not in VALID_SAQG_STATES:
|
||||
errors.append(f"saqg_json[{idx}].saqg_v1 invalid: {state!r}")
|
||||
|
||||
sapg = parse_jsonish(harness.get("sapg_json"))
|
||||
if not isinstance(sapg, dict):
|
||||
errors.append("missing/invalid BRT key: sapg_json")
|
||||
elif sapg.get("sapg_status") not in VALID_SAPG_STATUSES:
|
||||
errors.append(f"sapg_json.sapg_status invalid: {sapg.get('sapg_status')!r}")
|
||||
|
||||
permissions = parse_jsonish(harness.get("buy_permission_json"))
|
||||
if isinstance(permissions, list):
|
||||
for idx, bp in enumerate(permissions):
|
||||
if not isinstance(bp, dict):
|
||||
continue
|
||||
if bp.get("saqg_v1") in {"EXCLUDED"} and str(bp.get("buy_permission_state") or "").startswith("ALLOW_"):
|
||||
errors.append(f"buy_permission_json[{idx}]: SAQG EXCLUDED but buy_permission_state=ALLOW_*")
|
||||
if bp.get("saqg_v1") == "WATCHLIST_ONLY" and str(bp.get("buy_permission_state") or "").startswith("ALLOW_"):
|
||||
errors.append(f"buy_permission_json[{idx}]: SAQG WATCHLIST_ONLY but buy_permission_state=ALLOW_*")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = sys.argv[1:]
|
||||
if not args or len(args) > 3:
|
||||
print(__doc__)
|
||||
return 1
|
||||
|
||||
json_path = Path(args[0])
|
||||
strict = "--strict" in args
|
||||
check_mode: str | None = None
|
||||
if "--check" in args:
|
||||
idx = args.index("--check")
|
||||
if idx + 1 >= len(args):
|
||||
print("--check requires a mode: breakout_quality_gate|anti_whipsaw_gate|smart_cash_raise_v2|determinism|cla_harness|brt_harness")
|
||||
return 1
|
||||
check_mode = args[idx + 1]
|
||||
if check_mode not in VALID_CHECK_MODES:
|
||||
print(f"Unknown --check mode: {check_mode!r}. Valid: {', '.join(sorted(VALID_CHECK_MODES))}")
|
||||
return 1
|
||||
|
||||
harness = load_harness(json_path)
|
||||
errors: list[str] = []
|
||||
|
||||
if check_mode == "breakout_quality_gate":
|
||||
validate_breakout_quality_gate(harness, errors)
|
||||
label = "BREAKOUT_QUALITY_GATE"
|
||||
elif check_mode == "anti_whipsaw_gate":
|
||||
validate_anti_whipsaw_gate(harness, errors)
|
||||
label = "ANTI_WHIPSAW_GATE"
|
||||
elif check_mode == "smart_cash_raise_v2":
|
||||
validate_smart_cash_raise_v2(harness, errors)
|
||||
label = "SMART_CASH_RAISE_V2"
|
||||
elif check_mode == "determinism":
|
||||
validate_determinism(harness, errors)
|
||||
label = "DETERMINISM"
|
||||
elif check_mode == "cla_harness":
|
||||
validate_cla_harness(harness, errors)
|
||||
label = "CLA_HARNESS"
|
||||
elif check_mode == "brt_harness":
|
||||
validate_brt_harness(harness, errors)
|
||||
label = "BRT_HARNESS"
|
||||
elif check_mode == "factor_cap":
|
||||
# P1-1 (v11): PA1 단일팩터 50% 캡 검증
|
||||
import json as _json
|
||||
pa_path = json_path.parent / "predictive_alpha_engine_v2.json"
|
||||
if not pa_path.exists():
|
||||
pa_path = json_path.parent.parent / "Temp" / "predictive_alpha_engine_v2.json"
|
||||
label = "FACTOR_CAP"
|
||||
if pa_path.exists():
|
||||
pa = _json.loads(pa_path.read_text(encoding="utf-8"))
|
||||
audit = pa.get("factor_cap_audit", {})
|
||||
max_share = float(audit.get("single_factor_max_share_pct") or 0)
|
||||
pac_std = float(audit.get("pac_stddev") or 0)
|
||||
if max_share > 50.0:
|
||||
errors.append(f"single_factor_max_share_pct={max_share} > 50%")
|
||||
if pac_std < 5.0:
|
||||
errors.append(f"pac_stddev={pac_std} < 5.0")
|
||||
else:
|
||||
errors.append(f"predictive_alpha_engine_v2.json not found at {pa_path}")
|
||||
else:
|
||||
# Legacy / --strict mode
|
||||
apex_present = any(key in harness for key in REQUIRED_APEX_KEYS)
|
||||
if strict:
|
||||
validate_required_keys(harness, errors)
|
||||
# Also validate V5 keys in strict mode
|
||||
for key in REQUIRED_V5_KEYS:
|
||||
if key not in harness:
|
||||
errors.append(f"missing V5 key: {key}")
|
||||
validate_alpha_feedback_loop(harness, errors)
|
||||
if errors:
|
||||
print("ALPHA EXECUTION HARNESS FAIL")
|
||||
for err in errors:
|
||||
print(f"- {err}")
|
||||
return 1
|
||||
elif not apex_present:
|
||||
print("ALPHA EXECUTION HARNESS SKIPPED: APEX fields not present (use --strict after GAS Harness V5 export)")
|
||||
return 0
|
||||
validate_buy_blocks(harness, errors)
|
||||
validate_distribution_blocks(harness, errors)
|
||||
validate_cash_raise(harness, errors)
|
||||
validate_execution_quality(harness, errors)
|
||||
validate_alpha_feedback_loop(harness, errors)
|
||||
# V5 checks when keys present
|
||||
if "breakout_quality_gate_json" in harness:
|
||||
validate_breakout_quality_gate(harness, errors)
|
||||
if "anti_whipsaw_gate_json" in harness:
|
||||
validate_anti_whipsaw_gate(harness, errors)
|
||||
if "smart_cash_raise_json" in harness:
|
||||
validate_smart_cash_raise_v2(harness, errors)
|
||||
if "satellite_failure_gate_json" in harness:
|
||||
validate_cla_harness(harness, errors)
|
||||
if "benchmark_relative_timeseries_json" in harness:
|
||||
validate_brt_harness(harness, errors)
|
||||
label = "ALPHA EXECUTION HARNESS"
|
||||
|
||||
if errors:
|
||||
print(f"{label} FAIL")
|
||||
for err in errors:
|
||||
print(f"- {err}")
|
||||
return 1
|
||||
print(f"{label} OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user