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>
This commit is contained in:
@@ -1,3 +1,19 @@
|
||||
"""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
|
||||
@@ -7,27 +23,40 @@ 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,]+)" # 진입가
|
||||
# ── 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)
|
||||
@@ -39,41 +68,53 @@ def parse_claude_response(text):
|
||||
'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)
|
||||
|
||||
|
||||
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)
|
||||
print(f"✅ 거래 정보 저장 완료: {trade_info['stock_code']}")
|
||||
|
||||
|
||||
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"""
|
||||
"""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._make_request(
|
||||
"GET",
|
||||
"/uapi/domestic-stock/v1/trading/inquire-account",
|
||||
"TTTC8434R",
|
||||
params={}
|
||||
)
|
||||
balance_info = await korea_investment_api.get_balance()
|
||||
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
|
||||
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}")
|
||||
@@ -81,14 +122,12 @@ async def get_account_balance() -> float:
|
||||
|
||||
|
||||
async def get_stock_info(stock_code: str) -> tuple:
|
||||
"""Fetch stock name and current price"""
|
||||
"""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._make_request(
|
||||
"GET",
|
||||
"/uapi/domestic-stock/v1/quotations/inquire-price",
|
||||
"FHKST01010100",
|
||||
params={"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": stock_code}
|
||||
)
|
||||
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')
|
||||
@@ -184,7 +223,7 @@ async def auto_trade_from_claude(text, quantity=1):
|
||||
'timestamp': datetime.now(timezone.utc).isoformat(),
|
||||
'validation_details': validation_details # Store validation context
|
||||
}
|
||||
save_active_trade(trade_details)
|
||||
await save_active_trade(trade_details)
|
||||
else:
|
||||
error_message = result.get('msg1', '알 수 없는 오류')
|
||||
logger.error(f"❌ Order placement failed: {error_message}")
|
||||
@@ -196,7 +235,6 @@ async def auto_trade_from_claude(text, quantity=1):
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 예시: Claude 응답 텍스트를 받아 자동매매 실행
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
text = sys.argv[1]
|
||||
asyncio.run(auto_trade_from_claude(text))
|
||||
|
||||
Reference in New Issue
Block a user