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"):