fix: 매수 차단·백테스트 왜곡 유발한 버그 5종 수정

수개월간 매수 0건 + 백테스트가 손실로 보이던 근본 원인들을 수정.

- ADX 발산(mean_reversion_strategy, run_backtest): Wilder 평활에 /period
  누락으로 ADX가 100 초과 발산 → 실서비스 SIDEWAYS 게이트(ADX<20) 영구
  무력화. 공식 복원(표준 Wilder와 대수적으로 동일 검증).
- 백테스트 워밍업(run_backtest): 국면판정 220봉을 start_date부터 세서
  14개월 미만 백테스트의 ~90%가 강제 BEAR → 가짜 손실. start_date 340일
  전부터 프리로드하고 성과는 sim_start_date부터만 집계. CAGR 분모도 워밍업
  제외한 실제 거래일로 수정. → 최근1년 -0.33%→+6.95%, 가산성 회복.
- ML HOLD 게이트(trade_executor): 신뢰도 무관 매수 차단 → 데이터 부족으로
  ML이 실패(conf=0)한 HOLD까지 모든 매수를 막던 것을 conf>=0.6 조건부로.
- 빈 Redis 추천 캐시(swing/scheduler): 빈 캐시를 '히트'로 처리해 DB 폴백을
  건너뛰던 것을 recommendations_by_rule 비면 DB 폴백하도록.
- alembic 로깅(env.py, main.py): lifespan의 alembic upgrade가 fileConfig로
  앱 로거를 꺼 startup 이후 로그 전멸 → 스킵(스키마는 create_all 담당).

검증: code-verifier 통과(실전 경로 CONFIRMED 버그 없음), walk-forward 6/9구간
양(+)·과적합 아님(조건부 견고). 서바이버십 편향은 별도 이슈로 검증 진행 중.

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 10:03:56 +09:00
parent 3965760021
commit a0973812fb
7 changed files with 112 additions and 26 deletions

View File

@@ -705,6 +705,27 @@ async def _execute_buy(
except Exception as _pm_err:
logger.debug(f"[PATTERN_MEMORY] {stock_code} 포지션 조정 실패 (무시): {_pm_err}")
# [BUG FIX] 국면별 배율(_position_multiplier)과 뉴스 배율(_news_position_scale)을
# invest_amount에 반영 — 기존에는 stock dict에 주입만 되고 실제로 사용되지 않았음.
try:
_position_multiplier = float(stock.get("_position_multiplier", 1.0) or 1.0)
except Exception:
_position_multiplier = 1.0
_position_multiplier = max(0.0, min(1.0, _position_multiplier))
try:
_news_position_scale = float(stock.get("_news_position_scale", 1.0) or 1.0)
except Exception:
_news_position_scale = 1.0
_news_position_scale = max(0.0, min(1.0, _news_position_scale))
if _position_multiplier != 1.0 or _news_position_scale != 1.0:
invest_amount = invest_amount * _position_multiplier * _news_position_scale
logger.debug(
f"[POSITION_SCALE] {stock_code}: position_multiplier={_position_multiplier:.2f} "
f"news_scale={_news_position_scale:.2f} → invest_amount={invest_amount:,.0f}"
)
# 투자금액 → 주수 환산 (Decimal 사용으로 부동소수점 오차 방지)
quantity = int(Decimal(str(invest_amount)) / Decimal(str(current_price)))
logger.info(
@@ -929,7 +950,7 @@ async def _execute_sell(
TradeHistory.created_at >= recent_cutoff,
)
)
if duplicate_check.scalar_one_or_none():
if duplicate_check.scalars().first():
return {"action": "SKIP", "reason": "duplicate_order", "stock": stock_name}
# ── 2. 주문 실행 (종목별 세마포어 + 재시도) ──────────────────────────
@@ -1321,6 +1342,24 @@ async def execute_auto_trade(stock: dict, db: AsyncSession):
)
holding = result.scalar_one_or_none() # None이면 신규매수, 있으면 추가매수/매도 판단
# [BUG FIX] ActiveTrade.remaining_pct 조회 — 미주입 시 1차 익절 후에도
# remaining_pct가 항상 1.0으로 간주되어 SELL_PARTIAL이 반복 발생하는 문제 방지.
# 읽기 전용 SELECT만 수행 (DB 쓰기 없음, swing/exit_service.py의 자체 추적과 무충돌).
if holding is not None and stock.get("remaining_pct") is None:
try:
from app.models.models import ActiveTrade
_at_result = await db.execute(
select(ActiveTrade).where(
ActiveTrade.stock_code == stock_code,
ActiveTrade.status == "active",
)
)
_active_trade = _at_result.scalars().first()
if _active_trade is not None and _active_trade.remaining_pct is not None:
stock["remaining_pct"] = float(_active_trade.remaining_pct)
except Exception as _at_err:
logger.debug(f"[TRADE] {stock_code}: ActiveTrade remaining_pct 조회 실패: {_at_err}")
# ── 신호 분석 (Enum 기반) ─────────────────────────────────────────────
text_signals = " ".join(signals)
try:
@@ -1390,10 +1429,18 @@ async def execute_auto_trade(stock: dict, db: AsyncSession):
if any(b in text_signals for b in BLOCK_KEYWORDS):
return {"action": "HOLD", "stock": stock_name, "reason": "overheat_no_rule"}
# ML HOLD이면 매수 억제 (신뢰도 무관 — HOLD는 매수하지 않음)
if ml_signal_trade == "HOLD" and settings.ML_ENSEMBLE_ENABLED:
# ML HOLD이면 매수 억제 — 단, 신뢰도 60% 이상의 '확신 있는' HOLD만 차단.
# [FIX] 기존에는 신뢰도 무관하게 차단해, 일봉 데이터 부족으로 ML 피처 추출이
# 실패한 경우(signal=HOLD, confidence=0 = 사실상 "의견 없음")까지 모든 매수를
# 막아버렸다(수개월간 거래 0의 근본 원인). 위 ml_hold_active(1417~) 로직과 동일하게
# confidence>=0.6 조건을 적용해, 저신뢰/실패 HOLD는 룰 기반 판단을 막지 않도록 한다.
if (
ml_signal_trade == "HOLD"
and settings.ML_ENSEMBLE_ENABLED
and (ml_confidence_trade or 0) >= 0.6
):
should_buy = False
logger.info(f"[TRADE] {stock_code}: ML HOLD → 매수 차단")
logger.info(f"[TRADE] {stock_code}: ML HOLD(conf≥0.6) → 매수 차단")
if should_buy and not has_rule:
# 비규칙 종목: 강한 신호 OR (신호 2개 이상 AND 점수 4점 이상) 필수