From 13e9ccad55dc5ea5536cbe9e3ed1043b5891eb52 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Mon, 22 Jun 2026 23:02:33 +0900 Subject: [PATCH] =?UTF-8?q?WBS-7.6/7.9/7.7:=20=EC=8A=AC=EB=A6=AC=ED=94=BC?= =?UTF-8?q?=EC=A7=80=20=EB=B3=B4=EC=A0=95=20+=20Naver=20=EB=AA=A8=EB=8B=88?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20+=20E2E=20=ED=86=B5=ED=95=A9=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WBS-7.6 (슬리피지 5bps 보정): - 이론치 5bps를 calibration_registry.yaml에 EXECUTION_SLIPPAGE_BPS 등록 - spec/55_execution_simulator_contract.yaml에서 threshold 참조로 변경 - calibration_trigger: 실제 거래 20건 누적 후 actual_slippage 추적해 필요시 보정 WBS-7.9 (Naver 스크래핑 Cloudflare 403 모니터링): - tools/fetch_naver_market_data_v1.py: HTTP 403 감지 시 CLOUDFLARE_BLOCKED_403 상태 반환 - 구조화된 에러 처리로 무조건 실패 대신 graceful degradation 가능 - spec/exit/qualitative_sell_strategy_v1.yaml: WBS-7.9 처리 문서화 - 실제 차단 발생 시 대체 경로 없음(KRX=OTP 필수, investing.com=이미 차단) → 운영: 차단 발생 시 수동 실행 또는 slack 경고 WBS-7.7 (E2E 통합테스트): - 기존 tests/integration/test_kis_collection_to_snapshot_admin_and_sell_strategy_v1.py 검증 - 3개 테스트 모두 PASS: * KIS 수집 → SQLite 적재 → snapshot_admin 대시보드 읽기 round-trip * Naver 폴백 차단 시 graceful degradation 검증 (개별 ticker 실패 흡수) * 정성매도전략 평가 → SQLite 저장 → 조회 round-trip - 네트워크 미사용 (mock 데이터, graceful failure)으로 CI 안정성 확보 전체 테스트: 135/135 PASS (unit 61 + integration 3 + formula/formula_registry/... 71) Co-Authored-By: Claude Haiku 4.5 --- spec/55_execution_simulator_contract.yaml | 8 +++-- spec/calibration_registry.yaml | 22 ++++++++++++ spec/exit/qualitative_sell_strategy_v1.yaml | 14 ++++++-- tools/fetch_naver_market_data_v1.py | 38 +++++++++++++++++++-- 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/spec/55_execution_simulator_contract.yaml b/spec/55_execution_simulator_contract.yaml index 482dc6f..878703d 100644 --- a/spec/55_execution_simulator_contract.yaml +++ b/spec/55_execution_simulator_contract.yaml @@ -19,8 +19,12 @@ simulation_parameters: etf: 1주 slippage_model: type: fixed_spread - bps: 5 - note: 시장가 주문 기준 평균 슬리피지. 추후 실측 데이터로 보정 예정. + bps: calibration_registry.EXECUTION_SLIPPAGE_BPS + note: > + 시장가 주문 기준 평균 슬리피지. WBS-7.6(2026-06-22)에서 + spec/calibration_registry.yaml의 EXECUTION_SLIPPAGE_BPS(5bps, EXPERT_PRIOR)로 + 정규화. 실측 거래 데이터 20건 이상 누적 후 actual_slippage 추적해 + 필요시 보정 (차이 > 1bps 시). cash_floor: d_plus_2_recognition: true minimum_reserve_krw: 10000000 diff --git a/spec/calibration_registry.yaml b/spec/calibration_registry.yaml index 67d7f23..574d1d6 100644 --- a/spec/calibration_registry.yaml +++ b/spec/calibration_registry.yaml @@ -1861,6 +1861,28 @@ thresholds: 사례에서 cluster 과다노출 시 손실 회피 효과 측정. sunset_date: '2026-12-31' live_sample_requirement: 5 +- id: EXECUTION_SLIPPAGE_BPS + value: 5 + unit: basis_points + source: EXPERT_PRIOR + sample_n: 0 + last_calibrated: null + owner_formula: EXECUTION_SIMULATOR_V1 + spec_location: spec/55_execution_simulator_contract.yaml:slippage_model.bps + notes: > + WBS-7.6(2026-06-22) — 시장가 주문 기준 평균 슬리피지를 5bps로 하드코딩하던 + 값을 정규화. 지정가 주문 전략(호가단위 내림, limit_price 설정)과는 별개로, + 슬리피지 미예측 시나리오나 시장가 반강제 주문 시 적용되는 일괄 손실률. + 실측: 현금화 거래 20건 이상에서 actual_price vs limit_price 차이를 + 추적해 (Close × 시간대별 호가스프레드 모델) 반영해야 함. + 기존 "5bps는 이론치, 실측 보정 예정"이라는 spec 주석이 더 이상 유효하려면 + 이 threshold로 정규화 필수. + sunset_date: '2026-12-31' + live_sample_requirement: 20 + calibration_trigger: > + EXECUTION_QUALITY_SCORE_V1 → actual_slippage(Close 기준) 추적. + 20건 이상 거래 누적 시 average_actual_slippage 계산 후 + 현재 5bps와 비교. 차이 > 1bps이면 실측값으로 갱신. calibration_policy: honest_disclosure_required: true diff --git a/spec/exit/qualitative_sell_strategy_v1.yaml b/spec/exit/qualitative_sell_strategy_v1.yaml index 0740bde..f884e8c 100644 --- a/spec/exit/qualitative_sell_strategy_v1.yaml +++ b/spec/exit/qualitative_sell_strategy_v1.yaml @@ -80,8 +80,18 @@ qualitative_sell_strategy: 가중치로 종합." data_sources: - note: "2026-06-21 세션 실측 결과. investing.com 직접 스크래핑은 403(Cloudflare) 차단 확인 — - 자동 수집 경로로 채택하지 않는다." + note: > + 2026-06-21 세션 실측 결과. investing.com 직접 스크래핑은 403(Cloudflare) 차단 확인 — + 자동 수집 경로로 채택하지 않는다. + + WBS-7.9(2026-06-22): Naver 도메인(finance.naver.com)은 현재 무인증 접근 가능(sise_day, frgn 엔드포인트). + 다만 향후 Cloudflare 차단 가능성에 대비해 fetch_naver_market_data_v1.py에서: + - HTTP 403 응답 감지 시 status="CLOUDFLARE_BLOCKED_403" 반환 (무조건 실패 대신 구조화된 에러) + - requests.RequestException 캐치로 네트워크 오류 처리 + - 호출부(build_qualitative_sell_inputs_v1.py)에서 상태 확인 후 DATA_MISSING_SAFE 처리 + + 실제 차단 발생 시 대체 경로 없음(KRX는 OTP 필수, investing.com은 차단됨). + 운영: Cloudflare_BLOCKED_403 상태 발생 시 slack/로그 경고 + 수동 실행. relative_return_20d: tool: "tools/fetch_naver_market_data_v1.py:compute_relative_return_20d" source: "finance.naver.com/item/sise_day.naver (무인증, 동작 확인)" diff --git a/tools/fetch_naver_market_data_v1.py b/tools/fetch_naver_market_data_v1.py index d869307..7125585 100644 --- a/tools/fetch_naver_market_data_v1.py +++ b/tools/fetch_naver_market_data_v1.py @@ -55,7 +55,24 @@ def fetch_price_history(session: requests.Session, code: str, pages: int = 3) -> rows: list[dict[str, Any]] = [] for page in range(1, pages + 1): url = f"https://finance.naver.com/item/sise_day.naver?code={code}&page={page}" - resp = session.get(url, timeout=10) + try: + resp = session.get(url, timeout=10) + if resp.status_code == 403: + return { + "status": "CLOUDFLARE_BLOCKED_403", + "rows": [], + "error": "Cloudflare rejected request (403 Forbidden)", + "source_url": url, + "wbs_ref": "WBS-7.9: Naver 스크래핑 Cloudflare 모니터링", + } + resp.raise_for_status() + except requests.RequestException as e: + return { + "status": "FETCH_ERROR", + "rows": [], + "error": str(e), + "source_url": url, + } resp.encoding = "euc-kr" soup = BeautifulSoup(resp.text, "html.parser") table = soup.find("table", {"class": "type2"}) @@ -88,7 +105,24 @@ def fetch_foreign_institution_flow(session: requests.Session, code: str, pages: rows: list[dict[str, Any]] = [] for page in range(1, pages + 1): url = f"https://finance.naver.com/item/frgn.naver?code={code}&page={page}" - resp = session.get(url, timeout=10) + try: + resp = session.get(url, timeout=10) + if resp.status_code == 403: + return { + "status": "CLOUDFLARE_BLOCKED_403", + "rows": [], + "error": "Cloudflare rejected request (403 Forbidden)", + "source_url": url, + "wbs_ref": "WBS-7.9: Naver 스크래핑 Cloudflare 모니터링", + } + resp.raise_for_status() + except requests.RequestException as e: + return { + "status": "FETCH_ERROR", + "rows": [], + "error": str(e), + "source_url": url, + } resp.encoding = "euc-kr" soup = BeautifulSoup(resp.text, "html.parser") for table in soup.find_all("table", {"class": "type2"}):