feat: MCP 서버 리스크 검증 파이프라인 및 청산 상태머신 개선
- risk_validator.py: 가격/포지션/MDD/레버리지 5단계 검증 - auto_trade_claude.py: 주문 전 리스크 검증 파이프라인 통합 - trade_monitor.py: ATR 기반 익절/트레일링 스탑, API 기반 상태 전환 (active → pending_sell → closed), 2R 수익 시 50% 분할 매도 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,22 @@ import re
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
# Backend imports for risk validation
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../backend')))
|
||||
from app.services.korea_investment_api import korea_investment_api
|
||||
from app.core.database import AsyncSessionLocal
|
||||
|
||||
# Risk validator
|
||||
from risk_validator import validate_and_log_order
|
||||
|
||||
# Logging setup
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Claude 응답에서 종목/가격 추출
|
||||
CLAUDE_FORMAT = r"종목[:\s]+(\d+)" # 종목코드
|
||||
@@ -41,39 +55,144 @@ def save_active_trade(trade_info):
|
||||
json.dump(trades, f, indent=4, ensure_ascii=False)
|
||||
print(f"✅ 거래 정보 저장 완료: {trade_info['stock_code']}")
|
||||
|
||||
async def get_account_balance() -> float:
|
||||
"""Fetch current account balance from KIS API"""
|
||||
try:
|
||||
balance_info, _ = await korea_investment_api._make_request(
|
||||
"GET",
|
||||
"/uapi/domestic-stock/v1/trading/inquire-account",
|
||||
"TTTC8434R",
|
||||
params={}
|
||||
)
|
||||
if balance_info and balance_info.get('rt_cd') == '0':
|
||||
output = balance_info.get('output', {})
|
||||
# Try multiple possible field names for balance
|
||||
balance = float(
|
||||
output.get('tot_evalu_amt') or
|
||||
output.get('tot_assets') or
|
||||
output.get('dnca_tot_amt') or
|
||||
0
|
||||
)
|
||||
return balance
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch account balance: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
async def get_stock_info(stock_code: str) -> tuple:
|
||||
"""Fetch stock name and current price"""
|
||||
try:
|
||||
price_info, _ = await korea_investment_api._make_request(
|
||||
"GET",
|
||||
"/uapi/domestic-stock/v1/quotations/inquire-price",
|
||||
"FHKST01010100",
|
||||
params={"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": stock_code}
|
||||
)
|
||||
if price_info and price_info.get('rt_cd') == '0':
|
||||
output = price_info.get('output', {})
|
||||
stock_name = output.get('hts_kor_isnm', 'Unknown')
|
||||
current_price = int(output.get('stck_prpr', 0))
|
||||
return stock_name, current_price
|
||||
return 'Unknown', 0
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch stock info for {stock_code}: {e}")
|
||||
return 'Unknown', 0
|
||||
|
||||
|
||||
async def auto_trade_from_claude(text, quantity=1):
|
||||
"""
|
||||
Execute trade from Claude response with comprehensive risk validation.
|
||||
|
||||
Flow:
|
||||
1. Parse Claude response
|
||||
2. Fetch account balance and stock info
|
||||
3. Run comprehensive risk validation (CLAUDE.md Section 5)
|
||||
4. Place order only if all checks pass
|
||||
5. Log rejection if any check fails
|
||||
"""
|
||||
info = parse_claude_response(text)
|
||||
if info['stock_code'] and info['entry_price']:
|
||||
print(f"▶️ 매수 주문 시도: {info['stock_code']}, 수량: {quantity}, 가격: {info['entry_price']}")
|
||||
|
||||
if not info['stock_code'] or not info['entry_price']:
|
||||
logger.error('Claude 응답에서 종목/가격을 찾을 수 없습니다.')
|
||||
return
|
||||
|
||||
stock_code = info['stock_code']
|
||||
entry_price = info['entry_price']
|
||||
|
||||
logger.info(f"▶️ Processing trade: {stock_code}, Qty: {quantity}, Price: {entry_price}")
|
||||
|
||||
# Step 1: Fetch account balance
|
||||
account_balance = await get_account_balance()
|
||||
if account_balance <= 0:
|
||||
logger.error(f"❌ Cannot determine account balance. Trading aborted.")
|
||||
return
|
||||
|
||||
logger.info(f"Account balance: {account_balance:,.0f} KRW")
|
||||
|
||||
# Step 2: Fetch stock info
|
||||
stock_name, current_price = await get_stock_info(stock_code)
|
||||
logger.info(f"Stock info: {stock_code} ({stock_name}), Current price: {current_price} KRW")
|
||||
|
||||
# Step 3: COMPREHENSIVE RISK VALIDATION (CLAUDE.md Section 5)
|
||||
allowed, validation_details = await validate_and_log_order(
|
||||
stock_code=stock_code,
|
||||
stock_name=stock_name,
|
||||
order_quantity=quantity,
|
||||
entry_price=entry_price,
|
||||
account_balance=account_balance,
|
||||
market="KRX" # Default to domestic market
|
||||
)
|
||||
|
||||
# Step 4: Log validation result
|
||||
logger.info(f"\nRisk Validation Result:\n{json.dumps(validation_details, indent=2, ensure_ascii=False)}\n")
|
||||
|
||||
# If validation failed, abort and log rejection
|
||||
if not allowed:
|
||||
logger.error(f"🚫🚫🚫 [{stock_code}] Trade REJECTED due to risk validation failure")
|
||||
return
|
||||
|
||||
# Step 5: Place order (only if all checks pass)
|
||||
logger.info(f"✅ Risk validation passed. Proceeding with order placement...")
|
||||
print(f"▶️ 매수 주문 시도: {stock_code}, 수량: {quantity}, 가격: {entry_price}")
|
||||
|
||||
try:
|
||||
# 지정가 매수 주문
|
||||
result = await korea_investment_api.place_order(
|
||||
stock_code=info['stock_code'],
|
||||
stock_code=stock_code,
|
||||
order_type='BUY',
|
||||
quantity=quantity,
|
||||
price=info['entry_price']
|
||||
price=entry_price
|
||||
)
|
||||
logger.info(f"Order result: {result}")
|
||||
print('주문 결과:', result)
|
||||
|
||||
|
||||
# API 응답 구조에 따라 rt_cd가 '0'이면 성공으로 간주
|
||||
if result and result.get('rt_cd') == '0':
|
||||
logger.info(f"✅✅✅ [{stock_code}] Order placed successfully")
|
||||
print("✅ 매수 주문 성공. 거래 정보를 저장합니다.")
|
||||
|
||||
trade_details = {
|
||||
'stock_code': info['stock_code'],
|
||||
'stock_code': stock_code,
|
||||
'stock_name': stock_name,
|
||||
'quantity': quantity,
|
||||
'entry_price': info['entry_price'],
|
||||
'entry_price': entry_price,
|
||||
'stop_loss': info['stop_loss'],
|
||||
'take_profit': info['take_profit'],
|
||||
'order_id': result.get('ODNO'), # 주문번호
|
||||
'order_time': result.get('ORD_TMD'), # 주문시각
|
||||
'timestamp': datetime.now(timezone.utc).isoformat()
|
||||
'timestamp': datetime.now(timezone.utc).isoformat(),
|
||||
'validation_details': validation_details # Store validation context
|
||||
}
|
||||
save_active_trade(trade_details)
|
||||
else:
|
||||
error_message = result.get('msg1', '알 수 없는 오류')
|
||||
logger.error(f"❌ Order placement failed: {error_message}")
|
||||
print(f"❌ 매수 주문 실패: {error_message}")
|
||||
|
||||
else:
|
||||
print('Claude 응답에서 종목/가격을 찾을 수 없습니다.')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Exception during order placement: {e}", exc_info=True)
|
||||
print(f"❌ 주문 중 오류 발생: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 예시: Claude 응답 텍스트를 받아 자동매매 실행
|
||||
|
||||
Reference in New Issue
Block a user