feat: day_trading _execute_entry MCP 리스크 평가 연동 + .env.example MCP_CORS_ORIGINS 문서화
This commit is contained in:
@@ -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,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user