WBS-7.3 F12/F13: distribution_risk 두 공식 역할 분리 확정(KEEP_BOTH)
GAS calcDistributionRiskRow_의 "THIN_ADAPTER: delegated to Python" 주석이 틀린 주석이었음을 발견 — GAS(DISTRIBUTION_RISK_SCORE_V1, 점수식 BUY 차단 게이트)와 Python calc_distribution_detector_per_ticker(DISTRIBUTION_SELL_DETECTOR_V1, 6신호 카운트, PRE_DISTRIBUTION_EARLY_WARNING 정밀도 보완)는 이미 spec에 서로 다른 고유 formula_id로 등록된 독립 공식이었다. "GAS가 Python의 중복" 이라는 ledger 전제가 거짓이었을 뿐, 코드는 원래부터 올바르게 분리돼 있었다. 사용자 결정(둘 다 유지, 역할 분리)에 따라: - GAS 소스의 잘못된 주석 정정(gdf_03_portfolio_gates.gs) + 번들 재생성 - 양쪽 formula_registry에 상호 related_formula 참조 추가(향후 혼동 방지) - governance/gas_logic_migration_ledger_v1.yaml: migration_action을 DELETE_DISTRIBUTION_RISK_GAS → KEEP_BOTH_SEPARATE_ROLES로 변경, DONE
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""data_feed 원자료 컬럼(MA/Ret/ATR/수급 5D·20D) 파생 함수 단위 테스트.
|
||||
|
||||
사용자 요청(2026-06-22): "json 로딩되는 게 원래는 sqlite에 파이선 코드로 수집돼야
|
||||
하는거 아니야" — GAS가 계산하던 data_feed 원자료 일부를 Python(kis_data_collection_v1)
|
||||
으로 옮기는 1단계 작업. 네트워크를 사용하지 않고 순수 계산 로직만 검증한다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.quant_engine.kis_data_collection_v1 import (
|
||||
_aggregate_flow,
|
||||
_compute_atr20,
|
||||
_compute_ma,
|
||||
_compute_ret_pct,
|
||||
)
|
||||
|
||||
|
||||
def _price_rows(closes: list[float], highs: list[float] | None = None, lows: list[float] | None = None) -> list[dict]:
|
||||
"""closes[0]이 최신 거래일. high/low를 안 주면 close와 동일하게 채운다(ATR=0 케이스 테스트용)."""
|
||||
highs = highs or closes
|
||||
lows = lows or closes
|
||||
return [{"close": c, "high": h, "low": l, "volume": 1000} for c, h, l in zip(closes, highs, lows)]
|
||||
|
||||
|
||||
def test_compute_ma_returns_none_when_insufficient_rows():
|
||||
rows = _price_rows([100.0, 101.0, 102.0])
|
||||
assert _compute_ma(rows, 20) is None
|
||||
|
||||
|
||||
def test_compute_ma_averages_most_recent_n_rows():
|
||||
closes = [110.0] * 5 + [100.0] * 15
|
||||
rows = _price_rows(closes)
|
||||
# 최근 5거래일 평균 = 110, 20거래일 평균 = (5*110 + 15*100)/20 = 102.5
|
||||
assert _compute_ma(rows, 5) == 110.0
|
||||
assert _compute_ma(rows, 20) == 102.5
|
||||
|
||||
|
||||
def test_compute_ret_pct_against_n_days_ago_close():
|
||||
closes = [110.0, 109, 108, 107, 106, 100.0]
|
||||
rows = _price_rows(closes)
|
||||
# 최신(110) vs 5거래일전(100) → (110/100 - 1) * 100 = 10%
|
||||
assert _compute_ret_pct(rows, 5) == 10.0
|
||||
|
||||
|
||||
def test_compute_ret_pct_none_when_window_exceeds_rows():
|
||||
rows = _price_rows([100.0, 99.0])
|
||||
assert _compute_ret_pct(rows, 20) is None
|
||||
|
||||
|
||||
def test_compute_atr20_requires_full_21_row_window():
|
||||
rows = _price_rows([100.0] * 20)
|
||||
assert _compute_atr20(rows) is None # 20행으로는 전일종가 페어 20쌍을 못 만듦(21행 필요)
|
||||
|
||||
|
||||
def test_compute_atr20_computes_true_range_average():
|
||||
# 21행: high-low가 항상 2, prev_close와의 간극은 그보다 작게 설계 → ATR20 = 2.0
|
||||
closes = [100.0 + i * 0.1 for i in range(21)]
|
||||
highs = [c + 1.0 for c in closes]
|
||||
lows = [c - 1.0 for c in closes]
|
||||
rows = _price_rows(closes, highs, lows)
|
||||
atr = _compute_atr20(rows)
|
||||
assert atr is not None
|
||||
assert abs(atr - 2.0) < 0.5
|
||||
|
||||
|
||||
def test_aggregate_flow_sums_recent_window():
|
||||
rows = [{"frgn_net": 100, "inst_net": -50}] * 5 + [{"frgn_net": 1000, "inst_net": 1000}] * 15
|
||||
frg5, inst5 = _aggregate_flow(rows, 5)
|
||||
assert frg5 == 500
|
||||
assert inst5 == -250
|
||||
|
||||
|
||||
def test_aggregate_flow_none_when_window_exceeds_rows():
|
||||
rows = [{"frgn_net": 10, "inst_net": 10}] * 3
|
||||
frg, inst = _aggregate_flow(rows, 20)
|
||||
assert frg is None
|
||||
assert inst is None
|
||||
Reference in New Issue
Block a user