수정본
This commit is contained in:
@@ -44,11 +44,12 @@ MDD -20%: 전 포지션 50% 강제청산 (레버리지 우선).
|
||||
|
||||
|
||||
class ClaudeClient:
|
||||
"""Claude AI 클라이언트"""
|
||||
"""Claude AI 클라이언트 (하이브리드 Haiku/Sonnet 지원)"""
|
||||
|
||||
def __init__(self):
|
||||
self.client = AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
|
||||
self.model = settings.CLAUDE_MODEL
|
||||
self.haiku_model = settings.CLAUDE_HAIKU_MODEL
|
||||
|
||||
async def analyze(
|
||||
self,
|
||||
@@ -56,20 +57,25 @@ class ClaudeClient:
|
||||
user_message: str,
|
||||
tools: List[Dict[str, Any]] = None,
|
||||
max_tokens: int = None,
|
||||
model_override: str = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Claude를 통한 분석 실행.
|
||||
|
||||
시스템 프롬프트는 두 블록으로 구성:
|
||||
1) _CACHED_CONTEXT — cache_control 적용, 반복 호출 시 캐시에서 재사용.
|
||||
2) 메서드별 role 지시문 — 캐싱 없이 매번 전송.
|
||||
|
||||
Args:
|
||||
model_override: 특정 모델 강제 지정 (하이브리드 모드에서 Haiku/Sonnet 분기용)
|
||||
"""
|
||||
try:
|
||||
use_model = model_override or self.model
|
||||
messages = [
|
||||
{"role": "user", "content": user_message}
|
||||
]
|
||||
|
||||
params = {
|
||||
"model": self.model,
|
||||
"model": use_model,
|
||||
"max_tokens": max_tokens or settings.MAX_TOKENS,
|
||||
"temperature": settings.TEMPERATURE,
|
||||
"system": [
|
||||
@@ -100,6 +106,7 @@ class ClaudeClient:
|
||||
result = {
|
||||
"content": [],
|
||||
"stop_reason": response.stop_reason,
|
||||
"model_used": use_model,
|
||||
"usage": {
|
||||
"input_tokens": usage.input_tokens,
|
||||
"output_tokens": usage.output_tokens,
|
||||
@@ -107,6 +114,10 @@ class ClaudeClient:
|
||||
"cache_write_tokens": cache_write,
|
||||
},
|
||||
}
|
||||
logger.debug(
|
||||
f"[Claude] model={use_model} in={usage.input_tokens} out={usage.output_tokens} "
|
||||
f"cache_hit={cache_read}"
|
||||
)
|
||||
|
||||
for block in response.content:
|
||||
if block.type == "text":
|
||||
@@ -224,14 +235,19 @@ class ClaudeClient:
|
||||
portfolio: Dict[str, Any],
|
||||
trade_plan: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""리스크 평가 (토큰 최소화 — max_tokens=RISK_MAX_TOKENS)"""
|
||||
"""리스크 평가 (토큰 최소화 — max_tokens=RISK_MAX_TOKENS).
|
||||
|
||||
하이브리드 모드: Haiku 사용 (단순 승인/거부 판단)
|
||||
"""
|
||||
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)
|
||||
# 하이브리드: 단순 판단은 Haiku로 비용 절감
|
||||
_model = self.haiku_model if settings.HYBRID_AI_ENABLED else None
|
||||
response = await self.analyze(system_prompt, user_message, max_tokens=settings.RISK_MAX_TOKENS, model_override=_model)
|
||||
return self._parse_json_response(
|
||||
response,
|
||||
{"error": "Failed to assess risk"}
|
||||
@@ -264,18 +280,126 @@ class ClaudeClient:
|
||||
trade_info: Dict[str, Any],
|
||||
market_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""매매일지 자동 작성"""
|
||||
"""매매일지 자동 작성.
|
||||
|
||||
하이브리드 모드: Haiku 사용 (템플릿 기반 정리 작업)
|
||||
"""
|
||||
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)
|
||||
|
||||
_model = self.haiku_model if settings.HYBRID_AI_ENABLED else None
|
||||
response = await self.analyze(system_prompt, user_message, model_override=_model)
|
||||
return self._parse_json_response(
|
||||
response,
|
||||
{"error": "Failed to generate journal"}
|
||||
)
|
||||
|
||||
async def analyze_batch_stocks(
|
||||
self,
|
||||
stocks: List[Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
"""장 전 배치 분석 — 10종목을 한 번에 받아 각각 매수적합/부적합 판정.
|
||||
|
||||
Sonnet 사용 (기본 self.model). max_tokens=2048 (10종목 응답이므로 넉넉하게).
|
||||
입력 stocks: [{stock_code, stock_name, current_price, rsi, bb_position,
|
||||
volume_ratio, score, news_titles}, ...]
|
||||
"""
|
||||
system_prompt = """주식 스윙 트레이딩 분석가. 종목 리스트를 분석하여 JSON 배열로 응답.
|
||||
각 종목: stock_code, suitable(true/false), confidence(0-1), reason(한줄), risk_factors(배열).
|
||||
JSON 외 텍스트 금지."""
|
||||
|
||||
user_message = f"종목리스트:{_dumps(stocks)}"
|
||||
|
||||
response = await self.analyze(system_prompt, user_message, max_tokens=2048)
|
||||
# 배열 응답 파싱: '{' 대신 '[' ~ ']' 범위도 처리
|
||||
return self._parse_batch_response(response)
|
||||
|
||||
def _parse_batch_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""배치 분석 응답 파싱 — JSON 배열 또는 객체 모두 처리.
|
||||
|
||||
1) 응답 텍스트에서 JSON 배열([...]) 파싱 시도
|
||||
2) JSON 객체({...}) 파싱 시도 (AI가 {stocks: [...]} 형태로 응답할 수 있음)
|
||||
3) 모두 실패하면 빈 결과 반환
|
||||
"""
|
||||
for content in response["content"]:
|
||||
if content["type"] == "text":
|
||||
text = content["text"]
|
||||
# 배열 형태 시도
|
||||
try:
|
||||
arr_start = text.find('[')
|
||||
arr_end = text.rfind(']') + 1
|
||||
if arr_start != -1 and arr_end > arr_start:
|
||||
parsed = json.loads(text[arr_start:arr_end])
|
||||
if isinstance(parsed, list):
|
||||
return {"stocks": parsed}
|
||||
except (json.JSONDecodeError, Exception):
|
||||
pass
|
||||
# 객체 형태 시도
|
||||
try:
|
||||
obj_start = text.find('{')
|
||||
obj_end = text.rfind('}') + 1
|
||||
if obj_start != -1 and obj_end > obj_start:
|
||||
return json.loads(text[obj_start:obj_end])
|
||||
except (json.JSONDecodeError, Exception):
|
||||
pass
|
||||
return {"stocks": [], "analysis": text}
|
||||
return {"stocks": []}
|
||||
|
||||
async def check_news_sentiment(
|
||||
self,
|
||||
stock_code: str,
|
||||
stock_name: str,
|
||||
news_titles: List[str],
|
||||
) -> Dict[str, Any]:
|
||||
"""장중 뉴스 감성 분석.
|
||||
|
||||
하이브리드 모드: Haiku 사용 (단순 감성 판단). max_tokens=256 (짧은 응답).
|
||||
"""
|
||||
system_prompt = """주식 뉴스 감성 분석. JSON만 출력.
|
||||
필수: sentiment(POSITIVE/NEGATIVE/NEUTRAL), severity(LOW/MEDIUM/HIGH), action(HOLD/SELL/WATCH), reason(한줄).
|
||||
JSON 외 텍스트 금지."""
|
||||
|
||||
user_message = f"종목:{stock_code}({stock_name})\n뉴스제목:{_dumps(news_titles)}"
|
||||
|
||||
_model = self.haiku_model if settings.HYBRID_AI_ENABLED else None
|
||||
response = await self.analyze(system_prompt, user_message, max_tokens=256, model_override=_model)
|
||||
return self._parse_json_response(
|
||||
response,
|
||||
{"sentiment": "NEUTRAL", "severity": "LOW", "action": "HOLD", "reason": "분석 실패"}
|
||||
)
|
||||
|
||||
async def review_daily_trades(
|
||||
self,
|
||||
trades_summary: Dict[str, Any],
|
||||
market_summary: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""장 후 당일 매매 복기.
|
||||
|
||||
Sonnet 사용 (기본 self.model). max_tokens=1024.
|
||||
trades_summary: {total_trades, wins, losses, pnl, strategies_used, notable_trades}
|
||||
market_summary: {kospi_change, regime, top_movers}
|
||||
"""
|
||||
system_prompt = """주식 매매 복기 전문가. 당일 매매를 분석하여 JSON 응답.
|
||||
필수: overall_grade(A/B/C/D/F), good_points(배열), bad_points(배열), improvements(배열), strategy_drift(bool), drift_detail(있으면), tomorrow_plan(한줄).
|
||||
JSON 외 텍스트 금지."""
|
||||
|
||||
user_message = f"매매요약:{_dumps(trades_summary)}\n시장요약:{_dumps(market_summary)}"
|
||||
|
||||
response = await self.analyze(system_prompt, user_message, max_tokens=1024)
|
||||
return self._parse_json_response(
|
||||
response,
|
||||
{
|
||||
"overall_grade": "C",
|
||||
"good_points": [],
|
||||
"bad_points": [],
|
||||
"improvements": [],
|
||||
"strategy_drift": False,
|
||||
"tomorrow_plan": "분석 실패",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
claude_client = ClaudeClient()
|
||||
|
||||
Reference in New Issue
Block a user