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

@@ -11,20 +11,29 @@
from fastapi import APIRouter, HTTPException, Query, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, func, desc
from datetime import datetime, timedelta, date
from datetime import datetime, timezone, timedelta, date
from decimal import Decimal
from typing import List, Optional
import logging
import numpy as np
from app.core.database import get_db
_KST = timezone(timedelta(hours=9))
def _today_kst() -> date:
"""KST 기준 오늘 날짜 반환 (UTC 기준 date.today() 대신 항상 이 함수 사용)"""
return datetime.now(_KST).date()
from app.core.config import settings
from app.core.security import require_api_key
from app.models.models import TradeHistory, TradingSession, Portfolio
router = APIRouter()
logger = logging.getLogger(__name__)
# 🔥 리스크 관리 설정
MAX_POSITION_SIZE_RATIO = 0.20 # 계좌의 20%까지만
# 🔥 리스크 관리 설정 — config.py 참조
MAX_POSITION_SIZE_RATIO = settings.MAX_POSITION_SIZE # config.py: 0.06 (6%)
MAX_DAILY_LOSS_RATIO = 0.03 # 일일 3% 손실 시 중단
MAX_CONSECUTIVE_LOSSES = 3 # 3연속 손실 시 중단
COMMISSION_RATE = 0.00015 # 수수료 0.015%
@@ -52,7 +61,7 @@ async def get_risk_metrics(
select(TradeHistory)
.where(TradeHistory.created_at >= since_date)
.where(TradeHistory.order_type == "SELL")
.where(TradeHistory.status.in_(["EXECUTED", "FILLED"]))
.where(TradeHistory.status.in_(["EXECUTED", "COMPLETED", "FILLED"]))
.order_by(TradeHistory.executed_at)
)
trades = result.scalars().all()
@@ -146,7 +155,7 @@ async def get_sharpe_ratio(
select(TradeHistory)
.where(TradeHistory.created_at >= since_date)
.where(TradeHistory.order_type == "SELL")
.where(TradeHistory.status.in_(["EXECUTED", "FILLED"]))
.where(TradeHistory.status.in_(["EXECUTED", "COMPLETED", "FILLED"]))
.where(TradeHistory.profit_loss_rate.isnot(None))
)
trades = result.scalars().all()
@@ -171,11 +180,19 @@ async def get_sharpe_ratio(
avg_return = np.mean(returns)
std_return = np.std(returns)
# Sharpe Ratio 계산 (연간화: 252 거래일 기준)
# Sharpe Ratio 계산 — 거래 빈도 기반 연간화
# 거래가 일 1회가 아닌 경우 sqrt(252)로 연간화하면 부풀려짐.
# 실제 거래 기간(calendar days) 대비 거래 횟수로 연간 거래 빈도를 추정한다.
# trades_per_year: 관측 기간 내 거래 빈도를 252 거래일 기준으로 환산
first_trade_date = trades[0].executed_at or trades[0].created_at
last_trade_date = trades[-1].executed_at or trades[-1].created_at
span_days = max((last_trade_date - first_trade_date).days, 1)
trades_per_year = len(returns) / span_days * 252
# 무위험 수익률은 0으로 가정
sharpe = (avg_return * np.sqrt(252)) / std_return if std_return > 0 else 0
sharpe = (avg_return * np.sqrt(trades_per_year)) / std_return if std_return > 0 else 0
# 평가
if sharpe > 2:
interpretation = "Excellent (매우 우수)"
@@ -185,12 +202,13 @@ async def get_sharpe_ratio(
interpretation = "Fair (보통)"
else:
interpretation = "Poor (개선 필요)"
return {
"sharpe_ratio": round(sharpe, 3),
"avg_return": round(avg_return * 100, 2), # %
"std_return": round(std_return * 100, 2), # %
"annualized_return": round(avg_return * 252 * 100, 2), # %
"annualized_return": round(avg_return * trades_per_year * 100, 2), # %
"trades_per_year": round(trades_per_year, 1),
"interpretation": interpretation,
"trades_count": len(returns)
}
@@ -210,7 +228,7 @@ async def get_today_session(db: AsyncSession = Depends(get_db)):
- 거래 중지 상태
"""
try:
today = date.today()
today = _today_kst()
result = await db.execute(
select(TradingSession)
@@ -250,7 +268,7 @@ async def get_today_session(db: AsyncSession = Depends(get_db)):
raise HTTPException(status_code=500, detail=str(e))
@router.post("/check-risk-limits")
@router.post("/check-risk-limits", dependencies=[Depends(require_api_key)])
async def check_risk_limits(
order_value: float = Query(..., description="주문 금액"),
account_balance: float = Query(..., description="계좌 잔고"),
@@ -265,7 +283,7 @@ async def check_risk_limits(
"""
try:
# 1. 오늘 세션 확인
today = date.today()
today = _today_kst()
result = await db.execute(
select(TradingSession)
.where(TradingSession.session_date == today)
@@ -347,7 +365,7 @@ async def check_risk_limits(
raise HTTPException(status_code=500, detail=str(e))
@router.post("/update-session")
@router.post("/update-session", dependencies=[Depends(require_api_key)])
async def update_trading_session(
trade_result: str = Query(..., description="win 또는 loss"),
profit_loss: float = Query(..., description="거래 손익"),
@@ -360,7 +378,7 @@ async def update_trading_session(
- 일일 손익 누적
"""
try:
today = date.today()
today = _today_kst()
result = await db.execute(
select(TradingSession)
.where(TradingSession.session_date == today)
@@ -416,7 +434,7 @@ async def get_win_rate(
select(TradeHistory)
.where(TradeHistory.created_at >= since_date)
.where(TradeHistory.order_type == "SELL")
.where(TradeHistory.status.in_(["EXECUTED", "FILLED"]))
.where(TradeHistory.status.in_(["EXECUTED", "COMPLETED", "FILLED"]))
)
trades = result.scalars().all()
@@ -473,7 +491,7 @@ async def get_win_rate(
raise HTTPException(status_code=500, detail=str(e))
@router.post("/mdd/reset")
@router.post("/mdd/reset", dependencies=[Depends(require_api_key)])
async def reset_mdd():
"""MDD 상태 수동 초기화 (신규 진입 차단 해제)"""
from app.services.mdd_manager import mdd_manager
@@ -482,7 +500,7 @@ async def reset_mdd():
"peak_balance": mdd_manager.peak_balance,
"reduction_applied": mdd_manager.reduction_applied,
}
mdd_manager.reset()
await mdd_manager.reset()
logger.info("✅ MDD 수동 초기화 완료")
return {
"message": "MDD 초기화 완료 — 신규 진입 가능",