WBS-7.6/7.9/7.7: 슬리피지 보정 + Naver 모니터링 + E2E 통합테스트
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 <noreply@anthropic.com>
This commit is contained in:
@@ -19,8 +19,12 @@ simulation_parameters:
|
|||||||
etf: 1주
|
etf: 1주
|
||||||
slippage_model:
|
slippage_model:
|
||||||
type: fixed_spread
|
type: fixed_spread
|
||||||
bps: 5
|
bps: calibration_registry.EXECUTION_SLIPPAGE_BPS
|
||||||
note: 시장가 주문 기준 평균 슬리피지. 추후 실측 데이터로 보정 예정.
|
note: >
|
||||||
|
시장가 주문 기준 평균 슬리피지. WBS-7.6(2026-06-22)에서
|
||||||
|
spec/calibration_registry.yaml의 EXECUTION_SLIPPAGE_BPS(5bps, EXPERT_PRIOR)로
|
||||||
|
정규화. 실측 거래 데이터 20건 이상 누적 후 actual_slippage 추적해
|
||||||
|
필요시 보정 (차이 > 1bps 시).
|
||||||
cash_floor:
|
cash_floor:
|
||||||
d_plus_2_recognition: true
|
d_plus_2_recognition: true
|
||||||
minimum_reserve_krw: 10000000
|
minimum_reserve_krw: 10000000
|
||||||
|
|||||||
@@ -1861,6 +1861,28 @@ thresholds:
|
|||||||
사례에서 cluster 과다노출 시 손실 회피 효과 측정.
|
사례에서 cluster 과다노출 시 손실 회피 효과 측정.
|
||||||
sunset_date: '2026-12-31'
|
sunset_date: '2026-12-31'
|
||||||
live_sample_requirement: 5
|
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:
|
calibration_policy:
|
||||||
honest_disclosure_required: true
|
honest_disclosure_required: true
|
||||||
|
|||||||
@@ -80,8 +80,18 @@ qualitative_sell_strategy:
|
|||||||
가중치로 종합."
|
가중치로 종합."
|
||||||
|
|
||||||
data_sources:
|
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:
|
relative_return_20d:
|
||||||
tool: "tools/fetch_naver_market_data_v1.py:compute_relative_return_20d"
|
tool: "tools/fetch_naver_market_data_v1.py:compute_relative_return_20d"
|
||||||
source: "finance.naver.com/item/sise_day.naver (무인증, 동작 확인)"
|
source: "finance.naver.com/item/sise_day.naver (무인증, 동작 확인)"
|
||||||
|
|||||||
@@ -55,7 +55,24 @@ def fetch_price_history(session: requests.Session, code: str, pages: int = 3) ->
|
|||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
for page in range(1, pages + 1):
|
for page in range(1, pages + 1):
|
||||||
url = f"https://finance.naver.com/item/sise_day.naver?code={code}&page={page}"
|
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"
|
resp.encoding = "euc-kr"
|
||||||
soup = BeautifulSoup(resp.text, "html.parser")
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
table = soup.find("table", {"class": "type2"})
|
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]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
for page in range(1, pages + 1):
|
for page in range(1, pages + 1):
|
||||||
url = f"https://finance.naver.com/item/frgn.naver?code={code}&page={page}"
|
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"
|
resp.encoding = "euc-kr"
|
||||||
soup = BeautifulSoup(resp.text, "html.parser")
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
for table in soup.find_all("table", {"class": "type2"}):
|
for table in soup.find_all("table", {"class": "type2"}):
|
||||||
|
|||||||
Reference in New Issue
Block a user