Files
auto-stock/mcp-server/claude_client.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

282 lines
12 KiB
Python

"""Claude AI 클라이언트 모듈 (claude_client.py)
Anthropic Claude API를 사용하여 주식 분석, 리스크 평가, 매매일지 생성 등을 수행.
주요 기능:
- analyze(): 범용 Claude 분석 (캐싱 컨텍스트 지원)
- validate_trading_rule(): 트레이딩 규칙 검증
- analyze_market(): 시장 분석 (추세/심리/추천)
- calculate_fair_price(): AI 기반 적정가 산출
- assess_risk(): 리스크 평가 (토큰 최소화)
- optimize_trading_rules(): 전략 파라미터 최적화
- generate_trade_journal(): 매매일지 자동 생성
캐싱 전략:
- _CACHED_CONTEXT를 system 프롬프트의 첫 블록으로 전송 (cache_control: ephemeral)
- 반복 호출 시 Anthropic 서버 측에서 캐시 히트로 입력 토큰 절약
"""
from anthropic import AsyncAnthropic
from config import settings
from utils import _dumps
from typing import Dict, Any, List
import json
import logging
logger = logging.getLogger(__name__)
# 캐싱 공통 컨텍스트 — 전략 규칙을 한 번만 전송하고 이후 호출에서 캐시로 재사용.
# Claude Sonnet 기준 캐싱 최소 임계값: 1024 토큰.
_CACHED_CONTEXT = """[자동 주식 매매 시스템 — 리스크 규칙]
포지션 사이징: 종목당 총자본 3~4%, 최대 보유 20~25개, 레버리지 ETF 최대 20%.
손절(SL): 안정종목 -6%, 변동성종목 -10%, ATR 우선(진입가 - 2.5*ATR).
익절(TP): +2R 도달 시 50% 분할매도, 나머지는 고점-(1.5*ATR) 트레일링스탑.
MDD -10%: 신규진입 중단, 포지션 30% 축소, 단기전략 중단.
MDD -20%: 전 포지션 50% 강제청산 (레버리지 우선).
가격필터: 국내 5,000원 초과, 미국 $4 초과 (ETF·리츠 예외).
상관관계: 섹터당 최대 3종목, 상관계수 0.7 초과 진입 거부.
시장국면: SIDEWAYS(ADX<20) 진입 금지.
전략A(평균회귀): 상승장 RSI<40·BB하단, 하락장 RSI<30·BB하단·반전캔들, 거래량>20MA.
전략B(코스닥스캐너): 총자본 25%, 점수=0.5*RSI+0.3*BB+0.2*Vol 상위 20개.
단기전략(15분): RSI<30·BB하단·5MA돌파·거래량>1.3배, MDD -10% 미만 비활성.
"""
class ClaudeClient:
"""Claude AI 클라이언트"""
def __init__(self):
self.client = AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
self.model = settings.CLAUDE_MODEL
async def analyze(
self,
system_prompt: str,
user_message: str,
tools: List[Dict[str, Any]] = None,
max_tokens: int = None,
) -> Dict[str, Any]:
"""Claude를 통한 분석 실행.
시스템 프롬프트는 두 블록으로 구성:
1) _CACHED_CONTEXT — cache_control 적용, 반복 호출 시 캐시에서 재사용.
2) 메서드별 role 지시문 — 캐싱 없이 매번 전송.
"""
try:
messages = [
{"role": "user", "content": user_message}
]
params = {
"model": self.model,
"max_tokens": max_tokens or settings.MAX_TOKENS,
"temperature": settings.TEMPERATURE,
"system": [
{
"type": "text",
"text": _CACHED_CONTEXT,
"cache_control": {"type": "ephemeral"},
},
{
"type": "text",
"text": system_prompt,
},
],
"messages": messages,
}
if tools:
params["tools"] = tools
response = await self.client.messages.create(**params)
# 응답 파싱
usage = response.usage
cache_read = getattr(usage, "cache_read_input_tokens", 0) or 0
cache_write = getattr(usage, "cache_creation_input_tokens", 0) or 0
if cache_read:
logger.debug(f"[cache] hit={cache_read} write={cache_write} input={usage.input_tokens} output={usage.output_tokens}")
result = {
"content": [],
"stop_reason": response.stop_reason,
"usage": {
"input_tokens": usage.input_tokens,
"output_tokens": usage.output_tokens,
"cache_read_tokens": cache_read,
"cache_write_tokens": cache_write,
},
}
for block in response.content:
if block.type == "text":
result["content"].append({
"type": "text",
"text": block.text
})
elif block.type == "tool_use":
result["content"].append({
"type": "tool_use",
"id": block.id,
"name": block.name,
"input": block.input
})
return result
except Exception as e:
logger.error(f"Claude analysis error: {e}")
raise
def _parse_json_response(self, response: Dict[str, Any], fallback: Dict[str, Any]) -> Dict[str, Any]:
"""응답 텍스트에서 JSON 파싱 (공통 헬퍼).
GeminiClient._parse_json_response()와 동일한 로직.
1) 응답 content에서 text 블록을 찾아 JSON 파싱 시도
2) 순수 json.loads 실패 시 텍스트 내 첫 '{' ~ 마지막 '}' 범위를 추출하여 재시도
3) 모두 실패하면 fallback에 analysis 필드를 추가하여 반환
"""
for content in response["content"]:
if content["type"] == "text":
text = content["text"]
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# JSON이 마크다운 코드블록 등으로 감싸진 경우 추출 시도
try:
json_start = text.find('{')
json_end = text.rfind('}') + 1
if json_start != -1 and json_end > json_start:
return json.loads(text[json_start:json_end])
except (json.JSONDecodeError, Exception):
pass
return {**fallback, "analysis": content["text"]}
return fallback
async def validate_trading_rule(
self,
rule: Dict[str, Any],
market_data: Dict[str, Any],
news_data: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""트레이딩 규칙 검증"""
system_prompt = """주식 트레이딩 규칙 검증 전문가. 규칙/시장/뉴스를 분석하여 JSON으로 응답.
필수 필드: decision(BUY/SELL/HOLD), confidence(0-1), risk_level, recommendation.
악재(유상증자,감자,소송,실적악화) 시 confidence 낮추고 경고 포함."""
user_message = f"규칙:{_dumps(rule)}\n시장:{_dumps(market_data)}"
if news_data:
user_message += f"\n뉴스:{_dumps(news_data)}"
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"decision": "HOLD", "confidence": 0.0, "analysis": "분석 실패"}
)
async def analyze_market(
self,
stock_code: str,
market_data: Dict[str, Any],
news: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""시장 분석"""
system_prompt = """주식 시장 분석 전문가. JSON으로 응답.
필수 필드: trend(UP/DOWN/SIDEWAYS), sentiment(POSITIVE/NEGATIVE/NEUTRAL), recommendation(BUY/SELL/HOLD), confidence(0-1), analysis(간략설명)."""
news_limit = settings.NEWS_LIMIT if settings.TOKEN_SAVE_MODE else 5
news_text = ""
if news:
news_text = "\n뉴스:" + ",".join([
item.get('title', '') for item in news[:news_limit]
])
user_message = f"종목:{stock_code}\n시장:{_dumps(market_data)}{news_text}"
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"trend": "UNKNOWN", "sentiment": "NEUTRAL", "recommendation": "HOLD", "confidence": 0.0}
)
async def calculate_fair_price(
self,
stock_code: str,
market_data: Dict[str, Any],
financial_data: Dict[str, Any] = None
) -> Dict[str, Any]:
"""AI 기반 적정 매수/매도 가격 산출"""
system_prompt = """주식 가격 전략 전문가. JSON으로 응답.
필수 필드: buy_prices(aggressive,neutral,conservative), sell_prices(target1,target2,target3), stop_loss, strategy, confidence(0-1)."""
user_message = f"종목:{stock_code} 현재가:{market_data.get('current_price')} 고가:{market_data.get('high_price')} 저가:{market_data.get('low_price')} 거래량:{market_data.get('volume')} 지표:{_dumps(market_data.get('indicators', {}))}"
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"error": "분석 실패", "confidence": 0.0}
)
async def assess_risk(
self,
portfolio: Dict[str, Any],
trade_plan: Dict[str, Any]
) -> Dict[str, Any]:
"""리스크 평가 (토큰 최소화 — max_tokens=RISK_MAX_TOKENS)"""
system_prompt = """리스크 평가. JSON만 출력.
필수: risk_level(LOW/MEDIUM/HIGH), approval(APPROVED/REJECTED), reason(한줄)."""
slim_portfolio = {k: portfolio[k] for k in ("strategy", "position_count", "mdd") if k in portfolio}
user_message = f"포트:{_dumps(slim_portfolio)} 계획:{_dumps(trade_plan)}"
response = await self.analyze(system_prompt, user_message, max_tokens=settings.RISK_MAX_TOKENS)
return self._parse_json_response(
response,
{"error": "Failed to assess risk"}
)
async def optimize_trading_rules(
self,
stock_code: str,
market_data: Dict[str, Any],
news_data: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""시장 상황에 맞는 트레이딩 규칙 최적화"""
system_prompt = """알고리즘 트레이딩 전략가. JSON으로만 응답.
필수 필드: strategy_name, market_condition, parameters(buy_conditions,sell_conditions(take_profit_rate,stop_loss_rate,trailing_stop)), risk_assessment, confidence_score(0-100).
악재시 보수적, 호재시 적극적 설정."""
user_message = f"종목:{stock_code}\n시장:{_dumps(market_data)}"
if news_data:
user_message += f"\n뉴스:{_dumps(news_data)}"
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"error": "Failed to generate optimized rules"}
)
async def generate_trade_journal(
self,
trade_info: Dict[str, Any],
market_data: Dict[str, Any]
) -> Dict[str, Any]:
"""매매일지 자동 작성"""
system_prompt = """매매일지 작성 전문가. JSON으로 응답.
필수 필드: title, content(간략 Markdown), mood(평정심/흥분/공포/아쉬움/만족), tags(배열), score(0-100).
매매근거, 잘한점/아쉬운점, 개선사항 포함."""
user_message = f"거래:{_dumps(trade_info)}\n시장:{_dumps(market_data)}"
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"error": "Failed to generate journal"}
)
claude_client = ClaudeClient()