From 153b1bed172ff601a47a959fa4b54c66cf6daf65 Mon Sep 17 00:00:00 2001 From: YoungWooSeo Date: Tue, 24 Feb 2026 18:33:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20day=5Ftrading=20=5Fexecute=5Fentry=20MC?= =?UTF-8?q?P=20=EB=A6=AC=EC=8A=A4=ED=81=AC=20=ED=8F=89=EA=B0=80=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20+=20.env.example=20MCP=5FCORS=5FORIGINS=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/endpoints/day_trading.py | 61 ++++++++++++++++++++---- mcp-server/.env.example | 24 ++++++++++ 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/backend/app/api/endpoints/day_trading.py b/backend/app/api/endpoints/day_trading.py index fdbe2cb..7632b11 100644 --- a/backend/app/api/endpoints/day_trading.py +++ b/backend/app/api/endpoints/day_trading.py @@ -217,7 +217,7 @@ async def _record_trade( executed_price=price, executed_quantity=quantity, total_amount=total, - net_profit_loss=profit_loss, # ← net_profit_loss (profit_loss 컬럼 없음) + net_profit_loss=profit_loss, profit_loss_rate=(profit_loss / total * 100) if total > 0 else 0, slippage=slippage, status="COMPLETED", @@ -299,7 +299,7 @@ async def run_day_trading_cycle() -> Dict[str, Any]: available_cash = portfolio["available_cash"] global _last_total_assets if total_assets > 0: - _last_total_assets = total_assets # is_daily_loss_exceeded 계산용 캐시 갱신 + _last_total_assets = total_assets if not is_daily_loss_exceeded(total_assets, _daily_pnl): current_day_trade_amount = sum( @@ -415,7 +415,11 @@ async def _check_exits( ) await db.execute( sa_update(ActiveTrade) - .where(ActiveTrade.stock_code == code, ActiveTrade.strategy_name == "day_trade", ActiveTrade.status == "active") + .where( + ActiveTrade.stock_code == code, + ActiveTrade.strategy_name == "day_trade", + ActiveTrade.status == "active", + ) .values(status="closed") ) await db.commit() @@ -515,7 +519,11 @@ async def _scan_signals( async def _execute_entry(signal: DayTradeSignal, quantity: int) -> Optional[Dict]: - """진입 주문 실행 및 포지션 등록""" + """진입 주문 실행 및 포지션 등록. + + 주문 직전 MCP RISK_ASSESSMENT를 호출해 approve 시에만 매수한다 (이슈 4). + MCP 장애/타임아웃은 fail-open(approve=True)으로 처리해 매매를 차단하지 않는다. + """ global _trade_lock async with _trade_lock: if len(_positions) >= MAX_POSITIONS: @@ -526,6 +534,39 @@ async def _execute_entry(signal: DayTradeSignal, quantity: int) -> Optional[Dict logger.info(f"⛔ 당일 청산 종목 재진입 차단: {signal.stock_code}") return None + # ── MCP RISK_ASSESSMENT (공통 함수, 이슈 4 — day_trading 측) ──────────── + from app.api.endpoints.mcp import request_mcp_risk_assessment + _mcp_trade_plan = { + "action": "BUY", + "strategy": signal.strategy, + "stock_code": signal.stock_code, + "stock_name": signal.stock_name, + "entry_price": signal.entry_price, + "stop_loss": signal.stop_loss, + "target_price": signal.target_price, + "quantity": quantity, + "reason": signal.reason, + } + _mcp_signals = { + "strategy": signal.strategy, + "confidence": signal.confidence, + "reason": signal.reason, + } + _approve, _mcp_reason = await request_mcp_risk_assessment( + _mcp_trade_plan, {}, _mcp_signals + ) + if not _approve: + logger.warning( + f"🛑 MCP RISK_ASSESSMENT DENY [{signal.stock_code}] " + f"→ 주문 스킵. 사유: {_mcp_reason}" + ) + return None + if _mcp_reason: + logger.info( + f"✅ MCP RISK_ASSESSMENT APPROVE [{signal.stock_code}]. 참고: {_mcp_reason}" + ) + # ───────────────────────────────────────────────────────────────────── + order_no = await _place_buy_order( signal.stock_code, quantity, int(signal.entry_price) ) @@ -608,7 +649,11 @@ async def _execute_eod_flat() -> None: ) await db.execute( sa_update(ActiveTrade) - .where(ActiveTrade.stock_code == code, ActiveTrade.strategy_name == "day_trade", ActiveTrade.status == "active") + .where( + ActiveTrade.stock_code == code, + ActiveTrade.strategy_name == "day_trade", + ActiveTrade.status == "active", + ) .values(status="closed") ) await db.commit() @@ -637,7 +682,7 @@ async def restore_positions_from_db() -> None: if _positions: return # 이미 포지션 있으면 스킵 from datetime import timezone - today_utc = datetime.now(timezone.utc).date() # UTC 기준 오늘 날짜 (created_at과 동일 기준) + today_utc = datetime.now(timezone.utc).date() try: async with AsyncSessionLocal() as db: result = await db.execute( @@ -648,11 +693,9 @@ async def restore_positions_from_db() -> None: ) rows = result.scalars().all() for row in rows: - # created_at은 UTC 저장 → UTC 날짜로 비교 if row.created_at and row.created_at.date() != today_utc: continue entry_price = float(row.entry_price) - # entry_time 표시: UTC → KST 변환 (UTC+9) if row.created_at: created_kst = row.created_at.replace(tzinfo=pytz.utc).astimezone(KST) entry_time_str = created_kst.strftime("%H:%M:%S") @@ -839,7 +882,7 @@ async def get_positions(): "count": len(result), "daily_pnl": round(_daily_pnl, 0), "is_daily_loss_exceeded": is_daily_loss_exceeded( - _last_total_assets, # 사이클 캐시 사용 (포지션 가치가 아닌 총자산) + _last_total_assets, _daily_pnl, ), } diff --git a/mcp-server/.env.example b/mcp-server/.env.example index c81a368..39a3ed5 100644 --- a/mcp-server/.env.example +++ b/mcp-server/.env.example @@ -9,6 +9,15 @@ CLAUDE_MODEL=claude-3-5-sonnet-20241022 CLAUDE_MAX_TOKENS=4096 CLAUDE_TEMPERATURE=0.7 +# Gemini AI Configuration (AI_PROVIDER=gemini 시 사용) +GEMINI_API_KEY=your_gemini_api_key_here +GEMINI_MODEL=gemini-2.0-flash +GEMINI_MAX_TOKENS=1024 +GEMINI_TEMPERATURE=0.3 + +# AI Provider (claude 또는 gemini) +AI_PROVIDER=gemini + # Backend API Configuration BACKEND_API_URL=http://localhost:8000 BACKEND_API_TIMEOUT=30 @@ -34,6 +43,21 @@ KIS_APP_SECRET=your_virtual_app_secret_here KIS_ACCOUNT_NO=your_virtual_account_number_here KIS_BASE_URL=https://openapivts.koreainvestment.com:29443 +# ──────────────────────────────────────────────────────────────────── +# CORS 설정 (이슈 5) +# MCP 서버는 서버-서버 통신 전용 → allow_credentials=False 고정 +# 아래 환경변수로 허용 Origin을 명시적으로 제한한다. +# +# Docker Compose (backend 컨테이너에서 호출): +# MCP_CORS_ORIGINS=http://backend:8000 +# +# 로컬 개발 (localhost에서 호출): +# MCP_CORS_ORIGINS=http://localhost:8000,http://127.0.0.1:8000 +# +# 기본값 (미설정 시): http://localhost:8000,http://127.0.0.1:8000,http://backend:8000 +# ──────────────────────────────────────────────────────────────────── +MCP_CORS_ORIGINS=http://localhost:8000,http://127.0.0.1:8000,http://backend:8000 + # Tool Configuration ENABLE_MARKET_DATA_TOOL=true ENABLE_INDICATOR_TOOL=true