"""validate_order_grammar_v1.py — P7-T03 주문 문법 및 매도 우선순위 waterfall 검증기 1. 매도 주문에 다중 조건 접속사(AND, OR, &, +, , 등) 기반 문장이 없는지 검증 (단일 reason_code만 허용). 2. 매도 후보가 2개 이상인 경우, waterfall 순서가 맞는지 검증: STOP > CASH_FLOOR > DISTRIBUTION > VALUE_PRESERVE_TRIM > TAKE_PROFIT > HOLD """ from __future__ import annotations import json import sys from pathlib import Path from typing import Any # Windows 로컬 인코딩 문제 해결을 위해 utf-8 강제 if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"): sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1) ROOT = Path(__file__).resolve().parents[1] DEFAULT_JSON = ROOT / "GatherTradingData.json" DEFAULT_OUT = ROOT / "Temp" / "order_grammar_validation_v1.json" # 우선순위 정의 (STOP > CASH_FLOOR > DISTRIBUTION > VALUE_PRESERVE_TRIM > TAKE_PROFIT > HOLD) PRIORITY_ORDER = [ "STOP", "CASH_FLOOR", "DISTRIBUTION", "VALUE_PRESERVE_TRIM", "TAKE_PROFIT", "HOLD" ] def load_harness(path: Path) -> dict[str, Any]: if not path.exists(): return {} try: payload = json.loads(path.read_text(encoding="utf-8")) except Exception: return {} if isinstance(payload, dict) and isinstance(payload.get("data"), dict): maybe = payload["data"].get("_harness_context") if isinstance(maybe, dict): return maybe return payload if isinstance(payload, dict) else {} def main() -> int: hctx = load_harness(DEFAULT_JSON) orders = hctx.get("order_blueprint_json") if not isinstance(orders, list): # order_blueprint_json이 문자열 형태일 수 있으므로 파싱 시도 if isinstance(orders, str) and orders.strip(): try: orders = json.loads(orders) except Exception: orders = [] else: orders = [] multi_condition_count = 0 sell_priority_missing = 0 errors: list[str] = [] # 매도 후보 필터링 sell_candidates: list[dict[str, Any]] = [] sell_actions = {"SELL", "TRIM", "EXIT", "REDUCE"} for idx, order in enumerate(orders): if not isinstance(order, dict): continue order_type = str(order.get("order_type") or "").upper() action = str(order.get("action") or "").upper() is_sell = order_type in sell_actions or action in sell_actions if is_sell: sell_candidates.append(order) # 1. 다중 조건 접속사 검사 # reason_code 또는 reason 필드를 확인 reason_code = str(order.get("reason_code") or "") # 다중 조건 접속사 감지 (AND, OR, &, +, , 등) for sep in ["AND", "OR", "&", "+", ","]: rc_upper = reason_code.upper() if sep in ["&", "+", ","]: if sep in reason_code: multi_condition_count += 1 errors.append(f"order[{idx}] ({order.get('ticker')}): reason_code contains multiple conditions separated by '{sep}'") break else: # AND, OR # 단어 경계 체크 (예: " AND ", " OR ") if f" {sep} " in f" {rc_upper} ": multi_condition_count += 1 errors.append(f"order[{idx}] ({order.get('ticker')}): reason_code contains multiple conditions separated by '{sep}'") break # 2. Sell Priority Waterfall 검증 if len(sell_candidates) >= 2: prev_priority_idx = -1 for idx, order in enumerate(sell_candidates): rc = str(order.get("reason_code") or "").upper() # 매도 사유에 매핑되는 우선순위 찾기 matched_priority_idx = -1 for p_idx, p_name in enumerate(PRIORITY_ORDER): if p_name in rc: matched_priority_idx = p_idx break if matched_priority_idx == -1: sell_priority_missing += 1 errors.append(f"order ({order.get('ticker')}): reason_code '{rc}' does not map to any priority in {PRIORITY_ORDER}") else: if matched_priority_idx < prev_priority_idx: sell_priority_missing += 1 errors.append( f"Waterfall precedence violation: '{PRIORITY_ORDER[matched_priority_idx]}' order " f"appears after '{PRIORITY_ORDER[prev_priority_idx]}'" ) prev_priority_idx = matched_priority_idx status = "PASS" if not errors else "FAIL" result = { "formula_id": "ORDER_GRAMMAR_V1", "status": status, "errors": errors, "multi_condition_order_sentence_count": multi_condition_count, "sell_priority_missing_when_candidates_ge_2": sell_priority_missing, "sell_candidates_count": len(sell_candidates) } DEFAULT_OUT.parent.mkdir(parents=True, exist_ok=True) DEFAULT_OUT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps(result, ensure_ascii=False, indent=2)) if status == "PASS": print("ORDER_GRAMMAR_V1_OK") else: print("ORDER_GRAMMAR_V1_FAIL") for e in errors: print(f" ERROR: {e}") return 0 if status == "PASS" else 1 if __name__ == "__main__": raise SystemExit(main())