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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user