#!/usr/bin/env python3 """ build_p3_01_stop_loss_taxonomy.py ──────────────────────────────────────────────────────────────────────── P3_01: 손절 체계 재정의 — ABSOLUTE_RISK_STOP vs RELATIVE_ALERT 분리 문제 진단: 기존: "시장대비 10% 빠지면 매도" → 목적 불명확 실제: (1) 절대 리스크 스탑 + (2) 상대성과 경보 혼합 해결: 1. ABSOLUTE_RISK_STOP_V1: ATR 기반 절대 하방 캡 2. RELATIVE_UNDERPERFORMANCE_ALERT_V1: 강도 약화 신호 (로테이션용) 출력: - Temp/p3_01_stop_taxonomy_spec.json - Temp/p3_01_implementation_plan.json """ from __future__ import annotations import json import sys from pathlib import Path from datetime import datetime ROOT = Path(__file__).resolve().parent.parent OUTPUT_SPEC = ROOT / "Temp" / "p3_01_stop_taxonomy_spec.json" OUTPUT_PLAN = ROOT / "Temp" / "p3_01_implementation_plan.json" 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) def build_stop_taxonomy() -> dict: """손절 체계 정의.""" spec = { "schema_version": "stop_loss_taxonomy_v1", "generated_at": datetime.now().isoformat(), "root_cause": "절대 리스크(자본보호)와 상대강도(기회비용)를 혼합 → 손절 논리 불명확", "diagnosis": { "issue_1": "절대 스탑 없이 상대 로테이션만 사용 → 시장 폭락 시 -30% 출혈 가능", "issue_2": "지정가와 수량 규칙 없음 → HTS 입력 불가능 (HS007)", "issue_3": "비호가 가격 사용 → TICK_NORMALIZER 통과 실패 (HS008)", "issue_4": "상대성과만으로 전량 청산 → 기회비용 최악 (회복 불가)" }, "taxonomy": { "ABSOLUTE_RISK_STOP_V1": { "purpose": "자본 하방 보호 (항상 1순위)", "target": "손실을 ATR/퍼센트 캡으로 제한", "formula_core": "max(entry*0.92, entry - ATR20*1.5)", "formula_core_note": "ATR20_Pct >= 8%면 *2.0으로 확대", "formula_satellite": "entry - ATR20*2.0", "formula_satellite_fallback": "entry*0.88", "order_method": "지정가 (갭하락 시 09:00~09:15 시장가 금지)", "quantity_rule": "3단계: 50% 즉시 + 50% 나머지", "sample_prices": { "entry": 100000, "atr20": 2000, "core_stop": 98000, "core_stop_pct": "-2.0%", "satellite_stop": 96000, "satellite_stop_pct": "-4.0%" }, "implementation": "spec/exit/stop_loss.yaml:ABSOLUTE_RISK_STOP_V1" }, "RELATIVE_UNDERPERFORMANCE_ALERT_V1": { "purpose": "기회비용 관리 (로테이션) — 손절매 아님", "target": "강도 약화 신호 포착 → 신규 매수 차단 or 일부 trim", "formula": "excess_ret_20d <= min(-10, rel_threshold_pct)", "excess_ret_20d": "ret_stock_20d - beta_adj * ret_market_20d", "rel_threshold_pct": "-clip(1.5 * sigma20_pct, 6, 18)", "confirmation": "2영업일 연속 종가 확인 (단발 노이즈 차단)", "action_ladder": { "WATCH": "alert만 충족 → 신규매수 금지, 보유 유지", "TRIM_30": "alert + [수급이탈|섹터순위하락|MA20이탈] 중 1개 → 30% 지정가", "TRIM_50": "alert + 확인조건 2개↑ OR 절손 <=-20% → 50% 분할매도", "EXIT_100": "하드스탑|회계위험|거래정지 → 전량 하네스 지정방식" }, "sample_trigger": { "stock_ret_20d": "0%", "market_ret_20d": "5%", "excess_ret": "-5% (시장 수익 미달)", "beta_adj_threshold": "-10% (기준)", "trigger": "YES" }, "implementation": "spec/exit/stop_loss.yaml:RELATIVE_UNDERPERFORMANCE_ALERT_V1" }, "FUNDAMENTAL_THESIS_BREAK_V1": { "purpose": "재무 위험 (ROE 붕괴, FCF 악화 등)", "independent": "절대/상대 스탑과 독립 평가", "implementation": "spec/exit/stop_loss.yaml:FUNDAMENTAL_THESIS_BREAK_V1" } }, "mandatory_fields": { "stop_trigger": "[price, qty, order_method, reason] 4필드 강제", "price_normalization": "TICK_NORMALIZER_V1 통과 필수 (HS008)", "multi_condition": "다중조건 접속사 금지: '또는', '실패 시' (HS007)", "single_relative_exit_100": "상대성과만으로 EXIT_100 금지" } } return spec def build_implementation_plan() -> dict: """구현 계획.""" plan = { "phase": "P3_01_STOP_LOSS_TAXONOMY_FIX", "priority": "P0", "files_to_update": [ "spec/exit/stop_loss.yaml", "spec/13_formula_registry.yaml", "gas_data_feed.gs (함수 3개 신규)", "tools/validate_stop_loss_policy_v1.py (신규)" ], "tasks": [ { "task_id": "P3_01_A", "title": "stop_loss.yaml 리팩토링", "steps": [ "ABSOLUTE_RISK_STOP_V1 섹션 신규 생성", "RELATIVE_UNDERPERFORMANCE_ALERT_V1 섹션 신규 생성", "기존 '시장대비 N%' 문구 → ALERT로 강등", "모든 트리거에 [price, qty, method, reason] 4필드 명시" ], "acceptance": { "stop_policy_ambiguous_phrase_count": 0, "stop_action_has_price_qty_method_reason": True, "relative_only_full_liquidation_count": 0 } }, { "task_id": "P3_01_B", "title": "formula_registry에 3개 공식 등록", "steps": [ "ABSOLUTE_RISK_STOP_V1 formula_id 등록", "RELATIVE_UNDERPERFORMANCE_ALERT_V1 formula_id 등록", "FUNDAMENTAL_THESIS_BREAK_V1 formula_id 등록", "execution_order에 포함 (active=true)" ], "acceptance": { "formula_count": 3, "execution_order_included": True } }, { "task_id": "P3_01_C", "title": "GAS 함수 구현", "functions": [ "calcAbsoluteRiskStopV1_(): entry, atr20, pct → stop_price", "calcRelativeUnderperfAlertV1_(): ret_stock, ret_market → alert_flag", "calcStopActionLadderV1_(): alert + conditions → action (WATCH/TRIM/EXIT)" ], "file": "gas_data_feed.gs", "acceptance": { "function_count": 3, "gated_to_datasheet": True } }, { "task_id": "P3_01_D", "title": "검증 도구 구현", "file": "tools/validate_stop_loss_policy_v1.py", "checks": [ "gap_down 프로토콜: 09:00~09:15 시장가 투매 금지", "TICK_NORMALIZER_V1: 모든 지정가 통과", "multi-condition: 접속사 금지", "single-relative EXIT_100: 금지" ], "acceptance": { "validation_pass": True, "violations": 0 } } ], "risk_mitigation": [ "기존 '시장대비 N%' 로직은 ALERT로만 사용 (전량 청산 금지)", "ABSOLUTE_RISK_STOP은 매우 항상 1순위 (코어 손절)", "상대성과는 신규 매수만 차단 (보유 포지션은 허용)" ], "rollout": { "phase_1": "spec/exit/stop_loss.yaml 리팩토링 + formula_registry 등록", "phase_2": "GAS 함수 구현 + 데이터 전송", "phase_3": "analysis_prompt.md 업데이트 (LLM 지침)", "phase_4": "실전 신호 검증 (3건 이상)" } } return plan def main() -> int: print("=" * 80) print(" P3_01: 손절 체계 재정의 — ABSOLUTE vs RELATIVE 분리") print("=" * 80) # 스펙 생성 spec = build_stop_taxonomy() OUTPUT_SPEC.write_text( json.dumps(spec, ensure_ascii=False, indent=2), encoding="utf-8" ) print(f"\n✓ 손절 분류 스펙 저장: {OUTPUT_SPEC.name}") print(f" 분류: {len(spec['taxonomy'])}개 (ABSOLUTE, RELATIVE, FUNDAMENTAL)") # 구현 계획 plan = build_implementation_plan() OUTPUT_PLAN.write_text( json.dumps(plan, ensure_ascii=False, indent=2), encoding="utf-8" ) print(f"✓ 구현 계획 저장: {OUTPUT_PLAN.name}") print(f" 파일 업데이트: {len(plan['files_to_update'])}") print(f" 태스크: {len(plan['tasks'])}") # 진단 요약 print(f"\n[문제 진단]") for i, issue in enumerate(spec['diagnosis'].values(), 1): print(f" {i}. {issue}") print(f"\n[3가지 손절 메커니즘]") for name, detail in spec['taxonomy'].items(): print(f" • {name}") print(f" → {detail['purpose']}") print(f"\n[다음 액션]") for task in plan['tasks'][:2]: print(f" {task['task_id']}: {task['title']}") return 0 if __name__ == "__main__": sys.exit(main())