feat: 실적 필터 메커니즘 (Phase 2, 기본 OFF)

실적을 매매 기준에 반영하는 필터를 추가하되 기본 비활성(OFF). 적자(EPS<0)·
하락전망(컨센서스 목표가 대비 상승여력 부족) 종목의 진입을 차단한다. 데이터원은
네이버(get_stock_fundamentals, Phase 1 재사용, 10분 캐시).

- config: FUNDAMENTAL_FILTER_ENABLED=False(기본), FUNDAMENTAL_BLOCK_NEGATIVE_EPS,
  FUNDAMENTAL_MIN_UPSIDE_PCT.
- _execute_buy: 필터 ON일 때만 주문 전 실적 스크리닝, 실패 시 차단 안 함(안전).
  차단 사유는 관측성 로그로 노출(fundamental_negative_eps / fundamental_low_upside).

⚠️ 켜기 전 필수: point-in-time 실적 데이터(OpenDART)로 백테스트 검증
(CLAUDE.md §8.1-4). 네이버 현재값은 룩어헤드라 백테스트 부적합 → 검증 후 ON.
현재는 OFF라 라이브 동작 변화 없음.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBmzf1HDnvFHLraJ8eaaKJ
This commit is contained in:
2026-09-16 14:13:04 +09:00
parent d41e048821
commit 5b80223777
2 changed files with 34 additions and 0 deletions

View File

@@ -489,6 +489,35 @@ async def _execute_buy(
logger.debug(f"[BUY_SKIP] {stock_code} — 장 시작 30분 변동성 회피 (09:00~09:30)")
return {"action": "SKIP", "stock": stock_name, "reason": "opening_range_volatility"}
# ── 0.5. 실적 필터 (Phase 2 — 기본 OFF) ──────────────────────────────────
# FUNDAMENTAL_FILTER_ENABLED=True일 때만 동작. 적자(EPS<0)·하락전망(컨센서스
# 목표가 여력 부족) 종목 진입 차단. 데이터원=네이버. **point-in-time 백테스트
# 검증(OpenDART) 전엔 켜지 말 것.** 실패 시 차단 안 함(안전).
if settings.FUNDAMENTAL_FILTER_ENABLED and is_domestic:
try:
from app.services.naver_finance import get_stock_fundamentals
_ff = await get_stock_fundamentals(stock_code)
if _ff:
if settings.FUNDAMENTAL_BLOCK_NEGATIVE_EPS and _ff.get("eps"):
try:
_eps_v = float(str(_ff["eps"]).replace(",", "").replace("원", ""))
if _eps_v < 0:
return {"action": "HOLD", "stock": stock_name,
"reason": "fundamental_negative_eps(적자)"}
except Exception:
pass
_tp = _ff.get("target_price")
if _tp and current_price:
try:
_upside = (float(str(_tp).replace(",", "")) / current_price - 1) * 100
if _upside < settings.FUNDAMENTAL_MIN_UPSIDE_PCT:
return {"action": "HOLD", "stock": stock_name,
"reason": f"fundamental_low_upside(목표가 여력 {_upside:+.0f}%)"}
except Exception:
pass
except Exception as _ff_err:
logger.debug(f"[FUND_FILTER] {stock_code} 실적필터 스킵: {_ff_err}")
# ── 1. 3일 급등 체크 (ATR 기반 동적 임계값) ───────────────────────────────
surge_check = await check_surge_prevention(stock_code, is_domestic, exchange_code, current_price)
if surge_check.get("skip"):