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:
146
backend/app/services/swing/entry_service.py
Normal file
146
backend/app/services/swing/entry_service.py
Normal 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
|
||||
Reference in New Issue
Block a user