feat: day_trading _execute_entry MCP 리스크 평가 연동 + .env.example MCP_CORS_ORIGINS 문서화

This commit is contained in:
2026-02-24 18:33:15 +09:00
parent b36b4a381f
commit 153b1bed17
2 changed files with 76 additions and 9 deletions

View File

@@ -217,7 +217,7 @@ async def _record_trade(
executed_price=price, executed_price=price,
executed_quantity=quantity, executed_quantity=quantity,
total_amount=total, 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, profit_loss_rate=(profit_loss / total * 100) if total > 0 else 0,
slippage=slippage, slippage=slippage,
status="COMPLETED", status="COMPLETED",
@@ -299,7 +299,7 @@ async def run_day_trading_cycle() -> Dict[str, Any]:
available_cash = portfolio["available_cash"] available_cash = portfolio["available_cash"]
global _last_total_assets global _last_total_assets
if total_assets > 0: 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): if not is_daily_loss_exceeded(total_assets, _daily_pnl):
current_day_trade_amount = sum( current_day_trade_amount = sum(
@@ -415,7 +415,11 @@ async def _check_exits(
) )
await db.execute( await db.execute(
sa_update(ActiveTrade) 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") .values(status="closed")
) )
await db.commit() await db.commit()
@@ -515,7 +519,11 @@ async def _scan_signals(
async def _execute_entry(signal: DayTradeSignal, quantity: int) -> Optional[Dict]: async def _execute_entry(signal: DayTradeSignal, quantity: int) -> Optional[Dict]:
"""진입 주문 실행 및 포지션 등록""" """진입 주문 실행 및 포지션 등록.
주문 직전 MCP RISK_ASSESSMENT를 호출해 approve 시에만 매수한다 (이슈 4).
MCP 장애/타임아웃은 fail-open(approve=True)으로 처리해 매매를 차단하지 않는다.
"""
global _trade_lock global _trade_lock
async with _trade_lock: async with _trade_lock:
if len(_positions) >= MAX_POSITIONS: 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}") logger.info(f"⛔ 당일 청산 종목 재진입 차단: {signal.stock_code}")
return None 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( order_no = await _place_buy_order(
signal.stock_code, quantity, int(signal.entry_price) signal.stock_code, quantity, int(signal.entry_price)
) )
@@ -608,7 +649,11 @@ async def _execute_eod_flat() -> None:
) )
await db.execute( await db.execute(
sa_update(ActiveTrade) 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") .values(status="closed")
) )
await db.commit() await db.commit()
@@ -637,7 +682,7 @@ async def restore_positions_from_db() -> None:
if _positions: if _positions:
return # 이미 포지션 있으면 스킵 return # 이미 포지션 있으면 스킵
from datetime import timezone from datetime import timezone
today_utc = datetime.now(timezone.utc).date() # UTC 기준 오늘 날짜 (created_at과 동일 기준) today_utc = datetime.now(timezone.utc).date()
try: try:
async with AsyncSessionLocal() as db: async with AsyncSessionLocal() as db:
result = await db.execute( result = await db.execute(
@@ -648,11 +693,9 @@ async def restore_positions_from_db() -> None:
) )
rows = result.scalars().all() rows = result.scalars().all()
for row in rows: for row in rows:
# created_at은 UTC 저장 → UTC 날짜로 비교
if row.created_at and row.created_at.date() != today_utc: if row.created_at and row.created_at.date() != today_utc:
continue continue
entry_price = float(row.entry_price) entry_price = float(row.entry_price)
# entry_time 표시: UTC → KST 변환 (UTC+9)
if row.created_at: if row.created_at:
created_kst = row.created_at.replace(tzinfo=pytz.utc).astimezone(KST) created_kst = row.created_at.replace(tzinfo=pytz.utc).astimezone(KST)
entry_time_str = created_kst.strftime("%H:%M:%S") entry_time_str = created_kst.strftime("%H:%M:%S")
@@ -839,7 +882,7 @@ async def get_positions():
"count": len(result), "count": len(result),
"daily_pnl": round(_daily_pnl, 0), "daily_pnl": round(_daily_pnl, 0),
"is_daily_loss_exceeded": is_daily_loss_exceeded( "is_daily_loss_exceeded": is_daily_loss_exceeded(
_last_total_assets, # 사이클 캐시 사용 (포지션 가치가 아닌 총자산) _last_total_assets,
_daily_pnl, _daily_pnl,
), ),
} }

View File

@@ -9,6 +9,15 @@ CLAUDE_MODEL=claude-3-5-sonnet-20241022
CLAUDE_MAX_TOKENS=4096 CLAUDE_MAX_TOKENS=4096
CLAUDE_TEMPERATURE=0.7 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 Configuration
BACKEND_API_URL=http://localhost:8000 BACKEND_API_URL=http://localhost:8000
BACKEND_API_TIMEOUT=30 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_ACCOUNT_NO=your_virtual_account_number_here
KIS_BASE_URL=https://openapivts.koreainvestment.com:29443 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 # Tool Configuration
ENABLE_MARKET_DATA_TOOL=true ENABLE_MARKET_DATA_TOOL=true
ENABLE_INDICATOR_TOOL=true ENABLE_INDICATOR_TOOL=true