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

@@ -44,10 +44,11 @@ from app.core.market_schedule import (
from app.core.config import settings
from app.core.json_helpers import _dumps
from app.services.websocket_manager import websocket_manager
from app.services.loss_tracker import loss_tracker
from app.core.database import get_db, AsyncSessionLocal
from app.models.models import TradeHistory, ActiveTrade
from app.models.models import ActiveTrade
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update as sa_update
from sqlalchemy import select
# MCP risk assessment — 모듈 최상단에서 import (순환 참조 시 별도 util 모듈 분리 고려)
from app.api.endpoints.mcp import request_mcp_risk_assessment
@@ -240,29 +241,31 @@ async def _record_trade(
profit_loss: float = 0.0,
order_price: Optional[float] = None, # 주문가 (슬리피지 분석용)
) -> None:
"""TradeHistory DB 기록"""
"""TradeHistory DB 기록 (db_worker 큐 경유 -- CLAUDE.md 3.2 준수).
db 파라미터는 시그니처 호환을 위해 유지하되 사용하지 않음 (읽기 전용).
"""
try:
from app.services.db_worker import enqueue_trade_history
total = price * quantity
slippage = (price - order_price) if order_price is not None else None
trade = TradeHistory(
stock_code=stock_code,
stock_name=stock_name,
order_type=order_type,
order_price=order_price,
order_quantity=quantity,
executed_price=price,
executed_quantity=quantity,
total_amount=total,
net_profit_loss=profit_loss,
profit_loss_rate=(profit_loss / total * 100) if total > 0 else 0,
slippage=slippage,
status="COMPLETED",
entry_reason=reason if order_type == "BUY" else None,
exit_reason=reason if order_type == "SELL" else None,
executed_at=_kst_now(),
)
db.add(trade)
await db.commit()
await enqueue_trade_history({
"stock_code": stock_code,
"stock_name": stock_name,
"order_type": order_type,
"order_price": order_price,
"order_quantity": quantity,
"executed_price": price,
"executed_quantity": quantity,
"total_amount": total,
"net_profit_loss": profit_loss,
"profit_loss_rate": (profit_loss / total * 100) if total > 0 else 0,
"slippage": slippage,
"status": "COMPLETED",
"entry_reason": reason if order_type == "BUY" else None,
"exit_reason": reason if order_type == "SELL" else None,
"executed_at": _kst_now(),
})
except Exception as e:
logger.error(f"TradeHistory 기록 실패: {e}")
@@ -413,7 +416,7 @@ async def _check_exits(
Args:
candle_cache: 사이클 단위 분봉 캐시 {stock_code: candles}. None이면 직접 fetch.
"""
global _daily_pnl
global _daily_pnl, _portfolio_cache_dirty
exits = []
now_time = _kst_time()
@@ -456,21 +459,18 @@ async def _check_exits(
_positions.pop(code, None)
_closed_today.add(code) # 당일 재진입 차단
async with AsyncSessionLocal() as db:
await _record_trade(
db, code, pos["stock_name"], "SELL",
current_price, quantity, exit_signal.reason, pnl,
)
await db.execute(
sa_update(ActiveTrade)
.where(
ActiveTrade.stock_code == code,
ActiveTrade.strategy_name == "day_trade",
ActiveTrade.status == "active",
)
.values(status="closed")
)
await db.commit()
# db_worker 큐 경유 — CLAUDE.md 3.2 준수
from app.services.db_worker import enqueue_close_active_trade
await _record_trade(
None, code, pos["stock_name"], "SELL",
current_price, quantity, exit_signal.reason, pnl,
)
await enqueue_close_active_trade(code, strategy_name="day_trade")
# loss_tracker 기록 — CLAUDE.md 7.3 규칙 준수
_profit_rate = (current_price - pos["entry_price"]) / pos["entry_price"] if pos["entry_price"] > 0 else 0
await loss_tracker.record_trade_result_safe(code, _profit_rate < 0)
await loss_tracker.save_to_db(db=None, stock_code=code)
exits.append({
"stock_code": code,
@@ -634,27 +634,26 @@ async def _execute_entry(signal: DayTradeSignal, quantity: int) -> Optional[Dict
"reason": signal.reason,
}
async with AsyncSessionLocal() as db:
await _record_trade(
db, signal.stock_code, signal.stock_name, "BUY",
signal.entry_price, quantity, signal.reason,
order_price=float(int(signal.entry_price)), # 주문가 = 지정가
)
# ActiveTrade 저장 — 재시작 시 포지션 복구를 위해
active = ActiveTrade(
stock_code=signal.stock_code,
stock_name=signal.stock_name,
quantity=quantity,
entry_price=signal.entry_price,
stop_loss=signal.stop_loss,
take_profit=signal.target_price,
highest_since_entry=signal.entry_price,
strategy_name="day_trade",
entry_reason=signal.strategy,
status="active",
)
db.add(active)
await db.commit()
# db_worker 큐 경유 — CLAUDE.md 3.2 준수
from app.services.db_worker import enqueue_active_trade_upsert
await _record_trade(
None, signal.stock_code, signal.stock_name, "BUY",
signal.entry_price, quantity, signal.reason,
order_price=float(int(signal.entry_price)), # 주문가 = 지정가
)
# ActiveTrade 저장 — 재시작 시 포지션 복구를 위해
await enqueue_active_trade_upsert({
"stock_code": signal.stock_code,
"stock_name": signal.stock_name,
"quantity": quantity,
"entry_price": signal.entry_price,
"stop_loss": signal.stop_loss,
"take_profit": signal.target_price,
"highest_since_entry": signal.entry_price,
"strategy_name": "day_trade",
"entry_reason": signal.strategy,
"status": "active",
})
logger.info(
f"🛒 데이트레이딩 진입 [{signal.stock_code}] "
@@ -678,7 +677,7 @@ async def _execute_entry(signal: DayTradeSignal, quantity: int) -> Optional[Dict
async def _execute_eod_flat() -> None:
"""EOD 전체 포지션 강제 청산"""
global _daily_pnl
global _daily_pnl, _portfolio_cache_dirty
for code, pos in list(_positions.items()):
try:
current_price = await _get_current_price(code)
@@ -692,21 +691,18 @@ async def _execute_eod_flat() -> None:
_closed_today.add(code) # 당일 재진입 차단
logger.info(f"🔔 EOD 강제 청산 [{code}]: PNL={pnl:+,.0f}원")
async with AsyncSessionLocal() as db:
await _record_trade(
db, code, pos["stock_name"], "SELL",
current_price, pos["quantity"], "EOD_flat", pnl,
)
await db.execute(
sa_update(ActiveTrade)
.where(
ActiveTrade.stock_code == code,
ActiveTrade.strategy_name == "day_trade",
ActiveTrade.status == "active",
)
.values(status="closed")
)
await db.commit()
# db_worker 큐 경유 — CLAUDE.md 3.2 준수
from app.services.db_worker import enqueue_close_active_trade
await _record_trade(
None, code, pos["stock_name"], "SELL",
current_price, pos["quantity"], "EOD_flat", pnl,
)
await enqueue_close_active_trade(code, strategy_name="day_trade")
# loss_tracker 기록 — CLAUDE.md 7.3 규칙 준수
_profit_rate = (current_price - pos["entry_price"]) / pos["entry_price"] if pos["entry_price"] > 0 else 0
await loss_tracker.record_trade_result_safe(code, _profit_rate < 0)
await loss_tracker.save_to_db(db=None, stock_code=code)
await websocket_manager.broadcast(_dumps({
"type": "day_trade_exit",