Files
auto-stock/mcp-server/auto_trade_claude.py
shuim ab591f7fd6 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>
2026-03-10 17:26:37 +09:00

205 lines
7.7 KiB
Python

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+)" # 종목코드
ENTRY_FORMAT = r"진입가[:\s]+([\d,]+)" # 진입가
STOP_FORMAT = r"손절가[:\s]+([\d,]+)" # 손절가
TARGET_FORMAT = r"목표가[:\s]+([\d,]+)" # 목표가
ACTIVE_TRADES_FILE = os.path.join(os.path.dirname(__file__), 'active_trades.json')
def parse_claude_response(text):
code = re.search(CLAUDE_FORMAT, text)
entry = re.search(ENTRY_FORMAT, text)
stop = re.search(STOP_FORMAT, text)
target = re.search(TARGET_FORMAT, text)
return {
'stock_code': code.group(1) if code else None,
'entry_price': int(entry.group(1).replace(',', '')) if entry else None,
'stop_loss': int(stop.group(1).replace(',', '')) if stop else None,
'take_profit': int(target.group(1).replace(',', '')) if target else None,
}
def save_active_trade(trade_info):
"""활성화된 거래를 JSON 파일에 저장합니다."""
trades = []
if os.path.exists(ACTIVE_TRADES_FILE):
with open(ACTIVE_TRADES_FILE, 'r', encoding='utf-8') as f:
try:
trades = json.load(f)
except json.JSONDecodeError:
print(f"경고: {ACTIVE_TRADES_FILE} 파일이 비어있거나 잘못된 형식입니다. 새 파일을 생성합니다.")
trades.append(trade_info)
with open(ACTIVE_TRADES_FILE, 'w', encoding='utf-8') as f:
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 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=stock_code,
order_type='BUY',
quantity=quantity,
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': stock_code,
'stock_name': stock_name,
'quantity': quantity,
'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(),
'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}")
except Exception as e:
logger.error(f"❌ Exception during order placement: {e}", exc_info=True)
print(f"❌ 주문 중 오류 발생: {e}")
if __name__ == "__main__":
# 예시: Claude 응답 텍스트를 받아 자동매매 실행
import sys
if len(sys.argv) > 1:
text = sys.argv[1]
asyncio.run(auto_trade_from_claude(text))
else:
print("사용법: python auto_trade_claude.py 'Claude 응답 텍스트'")