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:
@@ -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 초기화 완료 — 신규 진입 가능",
|
||||
|
||||
Reference in New Issue
Block a user