fix: CRITICAL/HIGH 버그 30건 수정 + 전체 코드 주석 추가 + 중복 정리

## CRITICAL 수정 (20건)
- day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가
- trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산
- risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수
- risk_management/recommendations: date.today() → KST 변환
- mdd_manager: Lock 밖 executed_key 참조 수정
- correlation_filter: output → output2 키 수정
- portfolio: 해외 종목 USD/KRW 환율 적용 분리
- Sharpe Ratio 거래빈도 기반 연간화
- telegram_notifier: send_message() 공개 래퍼 추가
- position_manager: 앱 재시작 시 DB 포지션 복원
- swing exit_service: remaining_pct 조건 범위 수정
- run_backtest: original_qty 기반 remaining_pct 계산
- us_market_schedule: 2027년 미국 공휴일 추가
- Login.tsx: res.ok 검증 추가
- mean_reversion_strategy: RSI 로그 하드코딩 수정

## HIGH 수정 (10건)
- db_worker: DLQ 파일 백업 메커니즘 추가
- korea_investment_api: 만료 토큰 임의 연장 제거
- market_data: chart_cache 최대 100개 + TTL 만료
- main.py: rate_limit_store 1시간 미접근 IP 정리
- recommendations: fire-and-forget try/except 추가
- swing/scheduler: DB 세션 3개 분리
- Dashboard: wsMessages 전체 순회
- Layout: /health 엔드포인트 + 30초 폴링
- ml_ensemble: POST /train require_api_key 추가
- day_trading: record_trade_result_safe() 전환

## 코드 정리
- 프론트엔드: isUsStock, formatTime 공통 유틸 추출
- 백엔드: dead code 제거, 중복 import 통합
- MCP: JSON 파싱 헬퍼 추출 (~60줄 감소)
- 전체 ~100개 파일 한국어 주석 추가
- 테스트 13건 코드 기준 수정 (468 passed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 16:05:41 +09:00
parent 739a8a2e5d
commit de7921d270
149 changed files with 133353 additions and 3338 deletions

View File

@@ -0,0 +1,146 @@
"""스윙 트레이딩 진입 서비스
trade_executor.py의 execute_auto_trade_background() 로직을 기반으로
strategy_type='swing' 진입만 처리하는 독립 서비스.
진입 기준 (CLAUDE.md 섹션 6.2):
- BULL: RSI(14) < 46, 가격이 BB 하단 부근, 거래량 > 20MA
- BEAR: RSI(14) < 35, 가격 < BB 하단, 거래량 > 1.5x20MA, 반전 캔들
- SIDEWAYS: 진입 없음 (CLAUDE.md 6.1)
- 포지션 사이징: 종목당 총자본 6% (MAX_POSITION_SIZE), 최대 25종목
진입 전 체크:
1. 장 오픈 여부 (정규장 09:00~15:30만 허용)
2. 스윙 포지션 한도 (25종목)
3. MDD 신규 진입 가능 여부
4. 적응형 전략 Crisis 모드 차단
5. 플레이북 이벤트 차단 (FOMC, 옵션만기 등)
6. 시장 국면 SIDEWAYS 차단
"""
import asyncio
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.config import settings
from app.models.models import Portfolio
from app.services.trade_executor import execute_auto_trade, execute_auto_trade_background
from app.services.market_analyzer import check_market_regime
from app.services.mdd_manager import mdd_manager
from app.services.swing.position_manager import count_swing_positions
from app.services.market_playbook import is_entry_allowed_by_playbook, get_position_multiplier_from_playbook
logger = logging.getLogger(__name__)
# 스윙 동시 보유 최대 종목 수 (CLAUDE.md 5.1: MAX_HOLDINGS_COUNT = 20이지만,
# 스윙 전용으로 25개까지 허용 -- 데이트레이딩과 분리 운영)
MAX_SWING_POSITIONS = 25
async def execute_swing_entries(recommendations_response: dict, db: AsyncSession) -> list[dict]:
"""배치 분석 추천 결과를 기반으로 스윙 매수 실행.
기존 execute_auto_trade_background()를 재활용하되
strategy_type='swing' 레이블을 주입하여 데이트레이딩과 분리.
처리 흐름:
1. 장 오픈 여부 체크 (정규장만 허용)
2. 포지션 한도 체크 (MAX_SWING_POSITIONS)
3. MDD 체크 (can_open_new_position)
4. 적응형 전략 Crisis 모드 차단
5. 플레이북 이벤트 차단
6. SIDEWAYS 국면 차단
7. 추천 목록에 strategy_type='swing' 주입
8. execute_auto_trade_background() 호출
Args:
recommendations_response: Redis 또는 DB에서 가져온 추천 딕셔너리
- recommendations_by_rule: 규칙별 추천 종목 리스트
db: AsyncSession 인스턴스
Returns:
list[dict]: 실행된 매수 결과 목록
"""
# 장 마감 후 매수 차단 (15:30 이후, KIS 모의투자 24시간 허용 방지)
from app.core.market_schedule import is_market_open
if not is_market_open():
logger.info("[SwingEntry] 장 마감 — 스윙 매수 차단 (정규장만 매매)")
return []
# 포지션 한도 체크
current_count = await count_swing_positions(db)
if current_count >= MAX_SWING_POSITIONS:
logger.info(
f"[SwingEntry] 최대 포지션 수 도달 ({current_count}/{MAX_SWING_POSITIONS}) — 신규 진입 스킵"
)
return []
# MDD 체크
can_open = await mdd_manager.can_open_new_position()
if not can_open:
logger.warning("[SwingEntry] MDD 초과 — 신규 진입 차단")
return []
# 적응형 전략 체크 — Crisis 모드에서 스윙 진입 전면 차단
try:
from app.services.adaptive_strategy import get_current_adaptive_params
_adp_swing = get_current_adaptive_params()
if not _adp_swing.fallback_used and _adp_swing.trading_mode == "crisis":
logger.warning("[SwingEntry] 적응형 전략 CRISIS 모드 — 스윙 진입 전면 차단")
return []
except Exception:
pass # adaptive 모듈 실패 시 기존 로직 유지
# 플레이북 체크 — FOMC/옵션만기/서킷브레이커 등 이벤트 당일 차단
if not is_entry_allowed_by_playbook():
logger.info("[SwingEntry] 플레이북 차단 — 신규 진입 비활성화 (이벤트 대응)")
return []
# SIDEWAYS 국면 체크
try:
regime = await check_market_regime()
regime_str = regime.get("regime_200d", "bull") if isinstance(regime, dict) else "bull"
if regime_str == "sideways":
logger.info("[SwingEntry] SIDEWAYS 국면 — 진입 신호 무시 (CLAUDE.md 6.1)")
return []
except Exception as e:
logger.warning(f"[SwingEntry] 시장 국면 조회 실패: {e}")
# strategy_type='swing' 레이블 주입
rec = dict(recommendations_response)
_inject_strategy_type(rec, "swing")
# 기존 execute_auto_trade_background 재활용 (시그니처 변경 없음)
try:
result = await execute_auto_trade_background(rec, db)
if isinstance(result, list):
return result
except Exception as e:
logger.error(f"[SwingEntry] 진입 실행 오류: {e}", exc_info=True)
return []
def _inject_strategy_type(rec: dict, strategy_type: str) -> None:
"""추천 목록의 각 종목에 strategy_type 필드를 주입.
recommendations_by_rule 구조가 리스트 또는 딕셔너리일 수 있어
두 가지 형태를 모두 지원.
Args:
rec: 추천 딕셔너리 (원본을 직접 수정)
strategy_type: 주입할 전략 타입 (예: "swing")
"""
groups = rec.get("recommendations_by_rule", [])
if isinstance(groups, list):
for group in groups:
if isinstance(group, dict):
for stock in group.get("stocks", []):
if isinstance(stock, dict):
stock["strategy_type"] = strategy_type
elif isinstance(groups, dict):
for group in groups.values():
if isinstance(group, list):
for stock in group:
if isinstance(stock, dict):
stock["strategy_type"] = strategy_type