"""WBS-7.6(2026-06-21) — 실거래 슬리피지 실측 캡처 스캐폴딩. spec/55_execution_simulator_contract.yaml의 slippage_model(bps=5)은 이론치이며 "추후 실측 데이터로 보정 예정"이라는 메모만 있고 실제 캡처 경로가 없었다. 이 모듈은 주문은 사람이 HTS에서 직접 실행한다는 governance/rules/06 원칙을 그대로 유지한 채 (API로 체결을 가져오지 않는다), 실행 후 사람이 수동으로 기록한 실제 체결가를 누적해 가정치(5bps)와 비교할 수 있게 한다. 5건 미만이면 항상 DATA_GATED로 보고한다 — 추정 금지 원칙(spec/00_execution_contract.yaml)을 따른다. 표준 라이브러리 sqlite3만 사용한다. """ from __future__ import annotations import sqlite3 from pathlib import Path from typing import Any from src.quant_engine.storage_backend_v1 import StoreSpec, default_sqlite_store_path, normalize_store_spec SCHEMA = """ CREATE TABLE IF NOT EXISTS realized_slippage_samples ( id INTEGER PRIMARY KEY AUTOINCREMENT, ticker TEXT NOT NULL, side TEXT NOT NULL CHECK (side IN ('BUY', 'SELL')), intended_price REAL NOT NULL, actual_fill_price REAL NOT NULL, slippage_bps_actual REAL NOT NULL, recorded_at TEXT NOT NULL, note TEXT, inserted_at TEXT DEFAULT (datetime('now')) ); """ ASSUMED_SLIPPAGE_BPS = 5.0 MIN_SAMPLE_FOR_COMPARISON = 5 def default_execution_slippage_store_path(root: Path) -> Path: return default_sqlite_store_path(root, "execution_slippage/execution_slippage.db") def resolve_store_path(spec: StoreSpec, root: Path) -> Path: backend, location = normalize_store_spec( spec, root, default_sqlite_name="execution_slippage/execution_slippage.db" ) if backend != "sqlite": raise ValueError("execution_slippage_store_v1 currently executes on sqlite only.") return Path(location) def init_db(db_path: Path) -> None: db_path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(db_path) try: conn.executescript(SCHEMA) conn.commit() finally: conn.close() def _compute_slippage_bps(intended_price: float, actual_fill_price: float, side: str) -> float: """체결가가 의도가(지정가)보다 불리한 방향으로 움직인 만큼을 양수 bps로 환산한다. BUY: 실제 체결가가 의도가보다 높으면(더 비싸게 샀으면) 양수 슬리피지. SELL: 실제 체결가가 의도가보다 낮으면(더 싸게 팔았으면) 양수 슬리피지. """ if intended_price <= 0: raise ValueError("intended_price must be > 0") direction = 1 if side.upper() == "BUY" else -1 return direction * (actual_fill_price - intended_price) / intended_price * 10_000.0 def insert_realized_slippage_sample( db_path: Path, *, ticker: str, side: str, intended_price: float, actual_fill_price: float, recorded_at: str, note: str | None = None, ) -> dict[str, Any]: init_db(db_path) slippage_bps = _compute_slippage_bps(intended_price, actual_fill_price, side) conn = sqlite3.connect(db_path) try: conn.execute( "INSERT INTO realized_slippage_samples " "(ticker, side, intended_price, actual_fill_price, slippage_bps_actual, recorded_at, note) " "VALUES (?, ?, ?, ?, ?, ?, ?)", (ticker, side.upper(), intended_price, actual_fill_price, slippage_bps, recorded_at, note), ) conn.commit() finally: conn.close() return { "ticker": ticker, "side": side.upper(), "intended_price": intended_price, "actual_fill_price": actual_fill_price, "slippage_bps_actual": round(slippage_bps, 4), "recorded_at": recorded_at, } def fetch_all_samples(db_path: Path) -> list[dict[str, Any]]: if not db_path.exists(): return [] conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row try: rows = conn.execute( "SELECT ticker, side, intended_price, actual_fill_price, slippage_bps_actual, recorded_at, note " "FROM realized_slippage_samples ORDER BY recorded_at ASC" ).fetchall() return [dict(row) for row in rows] finally: conn.close() def build_slippage_comparison_report(db_path: Path) -> dict[str, Any]: """WBS-7.6 성공 하네스 — 5건 미만이면 DATA_GATED를 정직하게 반환한다(추정 금지).""" samples = fetch_all_samples(db_path) sample_n = len(samples) if sample_n < MIN_SAMPLE_FOR_COMPARISON: return { "status": "DATA_GATED", "sample_n": sample_n, "min_required": MIN_SAMPLE_FOR_COMPARISON, "assumed_slippage_bps": ASSUMED_SLIPPAGE_BPS, "actual_mean_slippage_bps": None, "note": f"실측 표본 {sample_n}/{MIN_SAMPLE_FOR_COMPARISON}건 — 비교 불가, 가정치(5bps) 유지", } actual_mean = sum(s["slippage_bps_actual"] for s in samples) / sample_n gap = abs(actual_mean - ASSUMED_SLIPPAGE_BPS) return { "status": "OK", "sample_n": sample_n, "assumed_slippage_bps": ASSUMED_SLIPPAGE_BPS, "actual_mean_slippage_bps": round(actual_mean, 4), "gap_bps": round(gap, 4), "recommendation": ( "가정치(5bps) 유지" if gap <= 3.0 else "spec/55_execution_simulator_contract.yaml의 bps 값을 실측 평균으로 갱신 검토" ), }