From 5b802237772562fb51ee705c52786b7157730529 Mon Sep 17 00:00:00 2001 From: shuim Date: Wed, 16 Sep 2026 14:13:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8B=A4=EC=A0=81=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EB=A9=94=EC=BB=A4=EB=8B=88=EC=A6=98=20(Phase=202,=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20OFF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 실적을 매매 기준에 반영하는 필터를 추가하되 기본 비활성(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 Claude-Session: https://claude.ai/code/session_01HBmzf1HDnvFHLraJ8eaaKJ --- backend/app/core/config.py | 5 +++++ backend/app/services/trade_executor.py | 29 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index cc169de..f413747 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -252,6 +252,11 @@ class Settings(BaseSettings): TREND_MA_SHORT: int = 20 TREND_MA_LONG: int = 60 TREND_TOLERANCE_PCT: float = 0.005 + # 실적 필터 (Phase 2) — 기본 OFF. point-in-time 실적 데이터로 백테스트 검증 전엔 + # 켜지 말 것(CLAUDE.md §8.1-4: 미검증 전략 실전 투입 금지). 현재 데이터원=네이버. + FUNDAMENTAL_FILTER_ENABLED: bool = False + FUNDAMENTAL_BLOCK_NEGATIVE_EPS: bool = True # 적자(EPS<0) 종목 진입 차단 + FUNDAMENTAL_MIN_UPSIDE_PCT: float = 0.0 # 컨센서스 목표가 상승여력 최소치(%). 이하면 차단 OVERHEAT_FILTER_ENABLED: bool = True # 급등 과열 필터 OVERHEAT_3DAY_MAX: float = 15.0 OVERHEAT_5DAY_MAX: float = 20.0 diff --git a/backend/app/services/trade_executor.py b/backend/app/services/trade_executor.py index c5b5c4d..d7093f9 100644 --- a/backend/app/services/trade_executor.py +++ b/backend/app/services/trade_executor.py @@ -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"):