fix: ML 게이트 스케일 불일치 + 운영 실측 반영 (문서 정정 3건)
## ML 게이트 이중 블렌딩 (플래그 OFF 로 출고) market_analyzer.py:603-608 이 이미 rule30/ML70 으로 블렌딩한 뒤 0~15 로 축소 저장하는데, trade_executor 가 그 값을 정규화 없이 **또** 블렌딩했다. weighted_score = score_val(0~15) * 0.3 + ml_score(0~100) * 0.7 ≥ 60 룰 기여가 최대 4.5 로 임계 60 의 7.5% — 문서상 "룰 30%" 가 지켜지지 않았다. _ml_gate_score_v2() 신설 + ML_SCORE_NORMALIZE_ENABLED(기본 False). OFF 시 7,373 조합 비트 동일 검증. RULE_SCORE_MAX=15 신설(하드코딩 제거). 운영 실측으로 확인된 것: ML-BUY 분기는 어느 설정으로도 발화 불가. OFF: ml_score ≥ 85.7 필요 / 실측 ml_score 최대 50 ON : score ≥ 9 필요 / 실측 score 역대 최대 6 (801건) 즉 ML 게이트는 HOLD 차단(브레이크)으로만 기능해 왔다. 임계값 재보정이 필요하며 §8.1-5 에 따라 백테스트가 선행돼야 한다. ## 문서 정정 (잘못된 전제 제거) 1. "NAS 다운 → 백테스트 불가" 는 사실이 아니다. NAS 는 가동 중이고 autostock-postgres 도 떠 있다. DB 가 127.0.0.1:15433 바인딩이라 외부 직결만 안 되는 것이며, NAS 내부에서 접근 가능하다. **백테스트 재실행 가능.** 2. docs/BATCH_GUIDE.md 의 cron 서술 정정 — 운영 NAS 에 crontab 이 없고 앱 내부 스케줄러(main.py:269 → schedulers.py:74, 10분 간격)가 배치를 돌린다. setup-cron.sh 를 쓰면 오히려 중복 실행된다. 3. market_schedule.py 에 휴장일 판정이 없다 — 2026-09-24~25 거래·분석 0건은 추석 연휴이며 장애가 아니다. ## §8.4.1 운영 실측 신설 - daytrading 257건 vs swing 53건 (DT 가 거래 건수 주력). DT 포지션 0 은 15:20 EOD 전량 청산이 설계대로 동작한 결과 - strategy_configs 가 옛 값으로 운영되던 상태(RSI 30 / SL -6%)를 2026-09-28 배포(alembic 0011→0012)로 해소 - 룰 점수는 0~15 스케일(market_analyzer.py:608 에서 축소 저장), 실측 -1~6 - 9/23 에 active_trades 유령 행 129개(day_trade 120 + swing 9)를 수동 정리한 백업이 남아 있다 — 전략명 없이 close 해 엉뚱한 행이 닫히던 문제의 증거 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -442,6 +442,16 @@ class Settings(BaseSettings):
|
||||
ML_ENSEMBLE_ML_WEIGHT: float = 0.7 # ML 스코어 비중
|
||||
ML_ENSEMBLE_BUY_THRESHOLD: int = 60 # 매수 최소 앙상블 스코어 (0~100)
|
||||
ML_ENSEMBLE_CONFIDENCE_MIN: float = 0.5 # 최소 신뢰도
|
||||
# 룰베이스 스코어의 최대값 (market_analyzer 의 signal 가점 합 상한).
|
||||
# 0~RULE_SCORE_MAX → 0~100 정규화에 사용. 하드코딩 15.0 을 대체한다.
|
||||
RULE_SCORE_MAX: int = 15
|
||||
# ML 게이트 점수 스케일 정규화 (기본 False = 기존 계산 그대로).
|
||||
# False: trade_executor 의 ML 게이트가 0~15 룰점수와 0~100 ML점수를 그대로
|
||||
# 가중합 (룰 기여도가 문서상 30% 가 아니라 실질 7.5%, ML 이중 계산).
|
||||
# True : StockAnalysis.score 가 이미 rule+ML 블렌딩된 0~RULE_SCORE_MAX 값이므로
|
||||
# 재블렌딩 없이 0~100 으로 환산해 임계와 직접 비교.
|
||||
# 진입 판정이 바뀌는 변경이므로 백테스트 검증 전 활성화 금지.
|
||||
ML_SCORE_NORMALIZE_ENABLED: bool = False
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# API & 네트워크
|
||||
|
||||
@@ -1787,6 +1787,39 @@ async def _delayed_sync_portfolio(delay_seconds: int = 3):
|
||||
logger.error(f"[PORTFOLIO_SYNC] 딜레이 동기화 실패: {type(e).__name__}: {e}")
|
||||
|
||||
|
||||
def _ml_gate_score_v2(rule_score: float, ml_score: float) -> float:
|
||||
"""ML 매수/HOLD 게이트 비교용 점수 산출 (0~100 스케일 기준).
|
||||
|
||||
[배경 — 2026-09-28 스케일 불일치 수정]
|
||||
market_analyzer.py(:603-608) 는 룰 점수를 0~100 으로 정규화한 뒤
|
||||
`rule*RULE_WEIGHT + ml*ML_WEIGHT` 로 블렌딩하고, 그 결과를 다시 0~15 로
|
||||
축소해 StockAnalysis.score 에 저장한다. 즉 trade_executor 가 받는
|
||||
stock["score"] 는 **이미 ML 이 반영된 0~RULE_SCORE_MAX 스케일** 값이다.
|
||||
(ml_score 키가 실린 모든 추천 경로 = recommendations.py:45 _stock_analysis_to_dict /
|
||||
recommendations.py:589 rec.model_dump() 가 동일하게 이 값을 전달한다.)
|
||||
|
||||
그런데 기존 게이트는 그 0~15 값을 정규화 없이 0~100 ML 점수와 다시 가중합해
|
||||
① 룰 기여 상한이 15×0.3=4.5 (임계 60 의 7.5%) 로 사실상 소멸
|
||||
② ml_score 를 이중 계산
|
||||
하는 두 결함을 안고 있었다.
|
||||
|
||||
ML_SCORE_NORMALIZE_ENABLED=False(기본) → 기존 계산식을 비트 단위로 그대로 유지.
|
||||
True → 재블렌딩을 제거하고 0~RULE_SCORE_MAX 를 0~100 으로 환산해 임계와 직접 비교.
|
||||
(ML 은 이미 score 안에 ML_WEIGHT 비중으로 반영되어 있다.)
|
||||
임계값·가중치(ML_ENSEMBLE_BUY_THRESHOLD/RULE_WEIGHT/ML_WEIGHT)는 무변경.
|
||||
"""
|
||||
if not settings.ML_SCORE_NORMALIZE_ENABLED:
|
||||
# 기존 동작 (0~15 룰점수 + 0~100 ML점수 가중합) — 비트 동일
|
||||
return (
|
||||
rule_score * settings.ML_ENSEMBLE_RULE_WEIGHT
|
||||
+ ml_score * settings.ML_ENSEMBLE_ML_WEIGHT
|
||||
)
|
||||
rule_max = float(settings.RULE_SCORE_MAX or 15)
|
||||
if rule_max <= 0:
|
||||
rule_max = 15.0
|
||||
return (rule_score / rule_max) * 100.0
|
||||
|
||||
|
||||
async def execute_auto_trade(stock: dict, db: AsyncSession):
|
||||
"""종목 단건 매매 판단 및 실행.
|
||||
|
||||
@@ -1993,10 +2026,10 @@ async def execute_auto_trade(stock: dict, db: AsyncSession):
|
||||
|
||||
if settings.ML_ENSEMBLE_ENABLED and ml_score_trade is not None:
|
||||
try:
|
||||
weighted_score = (
|
||||
score_val * settings.ML_ENSEMBLE_RULE_WEIGHT
|
||||
+ float(ml_score_trade) * settings.ML_ENSEMBLE_ML_WEIGHT
|
||||
)
|
||||
# [2026-09-28] 스케일 불일치(0~15 룰점수 vs 0~100 임계) 및 ML 이중
|
||||
# 계산 수정. ML_SCORE_NORMALIZE_ENABLED=False 면 기존 계산과 동일.
|
||||
# HOLD 게이트(*0.7)도 같은 weighted_score 를 쓰므로 스케일 일관.
|
||||
weighted_score = _ml_gate_score_v2(score_val, float(ml_score_trade))
|
||||
if (
|
||||
ml_signal_trade == "BUY"
|
||||
and weighted_score >= settings.ML_ENSEMBLE_BUY_THRESHOLD
|
||||
|
||||
Reference in New Issue
Block a user