Files
auto-stock/mcp-server/auto_trade_claude.py
shuim de7921d270 fix: CRITICAL/HIGH 버그 30건 수정 + 전체 코드 주석 추가 + 중복 정리
## CRITICAL 수정 (20건)
- day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가
- trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산
- risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수
- risk_management/recommendations: date.today() → KST 변환
- mdd_manager: Lock 밖 executed_key 참조 수정
- correlation_filter: output → output2 키 수정
- portfolio: 해외 종목 USD/KRW 환율 적용 분리
- Sharpe Ratio 거래빈도 기반 연간화
- telegram_notifier: send_message() 공개 래퍼 추가
- position_manager: 앱 재시작 시 DB 포지션 복원
- swing exit_service: remaining_pct 조건 범위 수정
- run_backtest: original_qty 기반 remaining_pct 계산
- us_market_schedule: 2027년 미국 공휴일 추가
- Login.tsx: res.ok 검증 추가
- mean_reversion_strategy: RSI 로그 하드코딩 수정

## HIGH 수정 (10건)
- db_worker: DLQ 파일 백업 메커니즘 추가
- korea_investment_api: 만료 토큰 임의 연장 제거
- market_data: chart_cache 최대 100개 + TTL 만료
- main.py: rate_limit_store 1시간 미접근 IP 정리
- recommendations: fire-and-forget try/except 추가
- swing/scheduler: DB 세션 3개 분리
- Dashboard: wsMessages 전체 순회
- Layout: /health 엔드포인트 + 30초 폴링
- ml_ensemble: POST /train require_api_key 추가
- day_trading: record_trade_result_safe() 전환

## 코드 정리
- 프론트엔드: isUsStock, formatTime 공통 유틸 추출
- 백엔드: dead code 제거, 중복 import 통합
- MCP: JSON 파싱 헬퍼 추출 (~60줄 감소)
- 전체 ~100개 파일 한국어 주석 추가
- 테스트 13건 코드 기준 수정 (468 passed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:05:41 +09:00

243 lines
9.6 KiB
Python

"""Claude AI 응답 기반 자동매매 실행 모듈 (auto_trade_claude.py)
Claude 응답 텍스트에서 종목코드/진입가/손절가/목표가를 정규식으로 추출하고,
리스크 검증(risk_validator) 통과 후 KIS API를 통해 매수 주문을 실행한다.
주요 흐름:
1. parse_claude_response() — 정규식 파싱
2. get_account_balance() — 계좌 잔고 조회
3. get_stock_info() — 현재가 및 종목명 조회
4. validate_and_log_order() — 종합 리스크 검증 (CLAUDE.md 섹션 5)
5. place_order() — KIS API 지정가 매수 주문
6. save_active_trade() — 체결 정보 JSON 파일 저장
활성 거래 저장: active_trades.json (asyncio.Lock + to_thread로 동시 접근 방지)
"""
import re
import asyncio
import json
import os
import sys
import logging
from datetime import datetime, timezone
from decimal import Decimal
# 백엔드 모듈 임포트를 위한 경로 추가
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 risk_validator import validate_and_log_order
# 로깅 설정
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# ── Claude 응답 파싱용 정규식 패턴 ──────────────────────────────────────────────
CLAUDE_FORMAT = r"종목[:\s]+(\d+)" # 종목코드 (6자리 숫자)
ENTRY_FORMAT = r"진입가[:\s]+([\d,]+)" # 진입가 (콤마 포함 숫자)
STOP_FORMAT = r"손절가[:\s]+([\d,]+)" # 손절가
TARGET_FORMAT = r"목표가[:\s]+([\d,]+)" # 목표가
# 활성 거래 정보를 저장하는 JSON 파일 경로
ACTIVE_TRADES_FILE = os.path.join(os.path.dirname(__file__), 'active_trades.json')
# active_trades.json 동시 접근 방지용 asyncio Lock
_trades_file_lock = asyncio.Lock()
def parse_claude_response(text):
"""Claude 응답 텍스트에서 종목코드, 진입가, 손절가, 목표가를 정규식으로 추출한다.
Args:
text: Claude AI 응답 텍스트
Returns:
dict: {"stock_code", "entry_price", "stop_loss", "take_profit"}
각 값이 없으면 None
"""
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 _read_trades_sync() -> list:
"""동기 방식으로 active_trades.json 읽기 (asyncio.to_thread 내부 전용)."""
if not os.path.exists(ACTIVE_TRADES_FILE):
return []
with open(ACTIVE_TRADES_FILE, 'r', encoding='utf-8') as f:
try:
return json.load(f)
except json.JSONDecodeError:
logger.warning(f"경고: {ACTIVE_TRADES_FILE} 파일이 비어있거나 잘못된 형식입니다. 새 파일을 생성합니다.")
return []
def _write_trades_sync(trades: list) -> None:
"""동기 방식으로 active_trades.json 쓰기 (asyncio.to_thread 내부 전용)."""
with open(ACTIVE_TRADES_FILE, 'w', encoding='utf-8') as f:
json.dump(trades, f, indent=4, ensure_ascii=False)
async def save_active_trade(trade_info: dict) -> None:
"""활성화된 거래를 JSON 파일에 저장합니다. (Lock + to_thread로 race condition 방지)"""
async with _trades_file_lock:
trades = await asyncio.to_thread(_read_trades_sync)
trades.append(trade_info)
await asyncio.to_thread(_write_trades_sync, trades)
logger.info(f"거래 정보 저장 완료: {trade_info['stock_code']}")
async def get_account_balance() -> float:
"""Fetch current account balance from KIS API via public get_balance() method.
[수정 1] _make_request() 직접 호출 제거 → get_balance() 공개 메서드 사용.
get_balance() 응답: output2[0].tot_evalu_mony (총평가금액)
output2[0].dnca_tot_amt (예수금총금액) 폴백
"""
try:
balance_info = await korea_investment_api.get_balance()
if balance_info and balance_info.get('rt_cd') == '0':
output2 = balance_info.get('output2', [])
if output2:
summary = output2[0]
balance = float(
summary.get('tot_evalu_mony') or
summary.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 via public get_current_price() method.
[수정 1] _make_request() 직접 호출 제거 → get_current_price() 공개 메서드 사용.
"""
try:
price_info = await korea_investment_api.get_current_price(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
}
await 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 응답 텍스트를 받아 자동매매 실행
if len(sys.argv) > 1:
text = sys.argv[1]
asyncio.run(auto_trade_from_claude(text))
else:
print("사용법: python auto_trade_claude.py 'Claude 응답 텍스트'")