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:
@@ -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점 이상) 필수
|
||||
|
||||
Reference in New Issue
Block a user