WBS-7.3: GAS→Python 마이그레이션 5개 항목 완료 (F14, F02-F06)
- F14: late_chase_risk_score 검증 * GAS가 유일한 생산처 (Python canonical 없음) * migration_action: KEEP_IN_GAS로 정정, status: DONE - F02/F03/F04/F06: priceBasis 로직 포팅 * formulas/price_basis_v1.py: select_price_basis_tier2/tier1 구현 * tests/parity/test_price_basis_parity_v1.py: 8 parity 테스트 (모두 PASS) * GAS Number.isFinite() 의미론 정확히 재현 (math.isfinite 사용) * 모든 테스트 112/112 PASS 남은 작업 (4개): - F05: decision_logic (action assignment) - F07: score_logic (threshold addition) - F10: routing decision - F15: late_chase_gate Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,48 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""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 argparse
|
||||
import json
|
||||
import re
|
||||
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]
|
||||
CONJ_RE = re.compile(r"(그리고|및|와|과|또는|/|,)")
|
||||
MULTI_CONDITION_RE = re.compile(r".*(그리고|및|와|과|또는).*(그리고|및|와|과|또는).*")
|
||||
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:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--report", default=str(ROOT / "Temp" / "operational_report.json"))
|
||||
args = ap.parse_args()
|
||||
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 = []
|
||||
|
||||
report_path = Path(args.report)
|
||||
raw = report_path.read_text(encoding="utf-8")
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
sections = payload.get("sections") if isinstance(payload, dict) else []
|
||||
text = "\n".join(str(s.get("markdown") or "") for s in sections if isinstance(s, dict))
|
||||
except Exception:
|
||||
text = raw
|
||||
multi_condition_count = 0
|
||||
sell_priority_missing = 0
|
||||
errors: list[str] = []
|
||||
|
||||
order_section = next((s for s in (payload.get("sections") if isinstance(payload, dict) else []) if isinstance(s, dict) and s.get("name") == "sell_priority_decision_table"), {}) if 'payload' in locals() else {}
|
||||
order_text = str(order_section.get("markdown") or text)
|
||||
# 매도 후보 필터링
|
||||
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
|
||||
|
||||
multi_condition_count = sum(1 for line in order_text.splitlines() if MULTI_CONDITION_RE.search(line))
|
||||
tick_normalized = "tick" in text.lower() or "호가단위" in text or "KRX" in text
|
||||
sell_candidate_count = len(re.findall(r"\bSELL\b|\bTRIM\b|매도", order_text))
|
||||
# 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,
|
||||
"tick_normalization_ok": tick_normalized,
|
||||
"sell_candidate_count": sell_candidate_count,
|
||||
"gate": "PASS" if multi_condition_count == 0 and tick_normalized else "FAIL",
|
||||
"sell_priority_missing_when_candidates_ge_2": sell_priority_missing,
|
||||
"sell_candidates_count": len(sell_candidates)
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0 if result["gate"] == "PASS" else 1
|
||||
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user