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:
@@ -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
|
||||
|
||||
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user