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,21 @@
|
||||
"""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
|
||||
@@ -36,7 +54,8 @@ class ClaudeClient:
|
||||
self,
|
||||
system_prompt: str,
|
||||
user_message: str,
|
||||
tools: List[Dict[str, Any]] = None
|
||||
tools: List[Dict[str, Any]] = None,
|
||||
max_tokens: int = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Claude를 통한 분석 실행.
|
||||
|
||||
@@ -51,7 +70,7 @@ class ClaudeClient:
|
||||
|
||||
params = {
|
||||
"model": self.model,
|
||||
"max_tokens": settings.MAX_TOKENS,
|
||||
"max_tokens": max_tokens or settings.MAX_TOKENS,
|
||||
"temperature": settings.TEMPERATURE,
|
||||
"system": [
|
||||
{
|
||||
@@ -109,6 +128,32 @@ class ClaudeClient:
|
||||
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],
|
||||
@@ -124,28 +169,12 @@ class ClaudeClient:
|
||||
|
||||
if news_data:
|
||||
user_message += f"\n뉴스:{_dumps(news_data)}"
|
||||
|
||||
|
||||
response = await self.analyze(system_prompt, user_message)
|
||||
|
||||
# 텍스트 응답 파싱
|
||||
for content in response["content"]:
|
||||
if content["type"] == "text":
|
||||
try:
|
||||
# JSON 응답 파싱 시도
|
||||
return json.loads(content["text"])
|
||||
except json.JSONDecodeError:
|
||||
# JSON이 아닌 경우 기본 구조로 반환
|
||||
return {
|
||||
"decision": "HOLD",
|
||||
"confidence": 0.5,
|
||||
"analysis": content["text"]
|
||||
}
|
||||
|
||||
return {
|
||||
"decision": "HOLD",
|
||||
"confidence": 0.0,
|
||||
"analysis": "분석 실패"
|
||||
}
|
||||
return self._parse_json_response(
|
||||
response,
|
||||
{"decision": "HOLD", "confidence": 0.0, "analysis": "분석 실패"}
|
||||
)
|
||||
|
||||
async def analyze_market(
|
||||
self,
|
||||
@@ -167,26 +196,10 @@ class ClaudeClient:
|
||||
user_message = f"종목:{stock_code}\n시장:{_dumps(market_data)}{news_text}"
|
||||
|
||||
response = await self.analyze(system_prompt, user_message)
|
||||
|
||||
for content in response["content"]:
|
||||
if content["type"] == "text":
|
||||
try:
|
||||
return json.loads(content["text"])
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"trend": "SIDEWAYS",
|
||||
"sentiment": "NEUTRAL",
|
||||
"recommendation": "HOLD",
|
||||
"confidence": 0.5,
|
||||
"analysis": content["text"]
|
||||
}
|
||||
|
||||
return {
|
||||
"trend": "UNKNOWN",
|
||||
"sentiment": "NEUTRAL",
|
||||
"recommendation": "HOLD",
|
||||
"confidence": 0.0
|
||||
}
|
||||
return self._parse_json_response(
|
||||
response,
|
||||
{"trend": "UNKNOWN", "sentiment": "NEUTRAL", "recommendation": "HOLD", "confidence": 0.0}
|
||||
)
|
||||
|
||||
async def calculate_fair_price(
|
||||
self,
|
||||
@@ -201,51 +214,28 @@ class ClaudeClient:
|
||||
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)
|
||||
|
||||
for content in response["content"]:
|
||||
if content["type"] == "text":
|
||||
try:
|
||||
return json.loads(content["text"])
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"buy_prices": {"neutral": market_data.get('current_price', 0)},
|
||||
"sell_prices": {"target1": market_data.get('current_price', 0) * 1.05},
|
||||
"stop_loss": market_data.get('current_price', 0) * 0.97,
|
||||
"strategy": content["text"],
|
||||
"confidence": 0.5
|
||||
}
|
||||
|
||||
return {
|
||||
"error": "분석 실패",
|
||||
"confidence": 0.0
|
||||
}
|
||||
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]:
|
||||
"""리스크 평가"""
|
||||
system_prompt = """투자 리스크 관리 전문가. JSON으로 응답.
|
||||
필수 필드: risk_level(LOW/MEDIUM/HIGH/CRITICAL), risk_factors, approval(APPROVED/REJECTED), recommendation."""
|
||||
"""리스크 평가 (토큰 최소화 — max_tokens=RISK_MAX_TOKENS)"""
|
||||
system_prompt = """리스크 평가. JSON만 출력.
|
||||
필수: risk_level(LOW/MEDIUM/HIGH), approval(APPROVED/REJECTED), reason(한줄)."""
|
||||
|
||||
user_message = f"포트폴리오:{_dumps(portfolio)}\n거래계획:{_dumps(trade_plan)}"
|
||||
|
||||
response = await self.analyze(system_prompt, user_message)
|
||||
|
||||
for content in response["content"]:
|
||||
if content["type"] == "text":
|
||||
try:
|
||||
return json.loads(content["text"])
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"risk_level": "MEDIUM",
|
||||
"approval": "HOLD",
|
||||
"confidence": 0.5,
|
||||
"analysis": content["text"]
|
||||
}
|
||||
|
||||
return {"error": "Failed to assess risk"}
|
||||
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,
|
||||
@@ -264,20 +254,10 @@ class ClaudeClient:
|
||||
user_message += f"\n뉴스:{_dumps(news_data)}"
|
||||
|
||||
response = await self.analyze(system_prompt, user_message)
|
||||
|
||||
# 텍스트 응답 파싱
|
||||
for content in response["content"]:
|
||||
if content["type"] == "text":
|
||||
try:
|
||||
text = content["text"]
|
||||
json_start = text.find('{')
|
||||
json_end = text.rfind('}') + 1
|
||||
if json_start != -1 and json_end != -1:
|
||||
return json.loads(text[json_start:json_end])
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse optimization result: {e}")
|
||||
|
||||
return {"error": "Failed to generate optimized rules"}
|
||||
return self._parse_json_response(
|
||||
response,
|
||||
{"error": "Failed to generate optimized rules"}
|
||||
)
|
||||
|
||||
async def generate_trade_journal(
|
||||
self,
|
||||
@@ -292,19 +272,10 @@ class ClaudeClient:
|
||||
user_message = f"거래:{_dumps(trade_info)}\n시장:{_dumps(market_data)}"
|
||||
|
||||
response = await self.analyze(system_prompt, user_message)
|
||||
|
||||
for content in response["content"]:
|
||||
if content["type"] == "text":
|
||||
try:
|
||||
text = content["text"]
|
||||
json_start = text.find('{')
|
||||
json_end = text.rfind('}') + 1
|
||||
if json_start != -1 and json_end != -1:
|
||||
return json.loads(text[json_start:json_end])
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse journal result: {e}")
|
||||
|
||||
return {"error": "Failed to generate journal"}
|
||||
return self._parse_json_response(
|
||||
response,
|
||||
{"error": "Failed to generate journal"}
|
||||
)
|
||||
|
||||
|
||||
claude_client = ClaudeClient()
|
||||
|
||||
Reference in New Issue
Block a user