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_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,
),
}