- korea_investment_api: KIS_QUOTES_VIA_REAL — 모의투자 모드에서 시세/차트/호가 조회를 실계좌 시세 서버(20/s)로 위임, 주문·잔고는 모의서버 유지 (EGW00201 대응) - recommendations: execute_auto_trade_background asyncio.Lock 중복 실행 방지 + MAX_DAILY_TRADES를 당일 BUY 건수 차감 전역 기준으로 적용 (11종목 동시 매수 재발 방지) - manual_buy.py 신규: POST /api/v1/portfolio/buy (리스크 가드 + safe_place_order + db_worker) - mcp-server: claude_code_client(claude -p 헤드리스 모드) 추가, trade_monitor 부분매도 시 active 유지·remaining_pct 갱신, API 키 헤더·/api/v1 경로 수정, ODNO 주문ID 수정 - config: MCP_REQUEST_TIMEOUT, MCP_RISK_CACHE_TTL, BATCH_ANALYSIS_INTERVAL, SWING_INDEX_BREAKER_ENABLED(OFF), API_TRIGGERED_AUTO_TRADE(OFF) 추가 - docker-compose: 포트 충돌 해소 (postgres 15433, frontend 3001) - frontend: 추천 패널 매수 버튼, 차트/호가/저널 패널 개선 - CLAUDE.md 압축 최적화 (규칙 변경 없음), _sweep_single_axis.py 백테스트 단일축 스윕 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
125 lines
5.0 KiB
Python
125 lines
5.0 KiB
Python
"""Claude Code CLI 기반 AI 클라이언트 (테스트/개발용).
|
|
|
|
Anthropic API 크레딧 대신 Claude Code 구독(`claude -p` 헤드리스 모드)으로 분석을 실행한다.
|
|
ClaudeClient와 동일한 인터페이스 — `analyze()`만 서브프로세스 호출로 교체하고
|
|
나머지 도메인 메서드(assess_risk, analyze_market 등)는 그대로 상속.
|
|
|
|
사용:
|
|
AI_PROVIDER=claude_code
|
|
CLAUDE_CODE_OAUTH_TOKEN=<`claude setup-token` 출력> # 컨테이너에서 실행 시 필수
|
|
|
|
제약:
|
|
- API tool_use(함수 호출) 미지원 → tools 인자는 무시하고 텍스트(JSON) 응답만 사용.
|
|
- 프롬프트 캐시 usage 는 CLI가 보고하는 값을 그대로 전달.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from typing import Any, Dict, List
|
|
|
|
from config import settings
|
|
from claude_client import ClaudeClient, _CACHED_CONTEXT
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_CLI_TIMEOUT = float(os.getenv("CLAUDE_CODE_TIMEOUT", "120"))
|
|
_CLI_BIN = os.getenv("CLAUDE_CODE_BIN", "claude")
|
|
_MAX_CONCURRENT = int(os.getenv("CLAUDE_CODE_MAX_CONCURRENT", "2"))
|
|
# 프로젝트 .claude/ 설정(훅·CLAUDE.md)이 끼어들지 않도록 중립 디렉터리에서 실행
|
|
_CLI_CWD = os.getenv("CLAUDE_CODE_CWD") or os.path.join(tempfile.gettempdir(), "autostock-claude-code")
|
|
|
|
|
|
class ClaudeCodeClient(ClaudeClient):
|
|
"""`claude -p` 서브프로세스로 ClaudeClient.analyze() 를 대체."""
|
|
|
|
def __init__(self):
|
|
# 부모 __init__ 은 AsyncAnthropic(api_key) 를 만들지만 여기서는 SDK 미사용
|
|
self.client = None
|
|
self.model = settings.CLAUDE_MODEL
|
|
self.haiku_model = settings.CLAUDE_HAIKU_MODEL
|
|
self._sem = asyncio.Semaphore(_MAX_CONCURRENT)
|
|
os.makedirs(_CLI_CWD, exist_ok=True)
|
|
if shutil.which(_CLI_BIN) is None:
|
|
logger.error("[ClaudeCode] '%s' 실행 파일을 찾을 수 없음 — 분석 호출은 모두 실패합니다", _CLI_BIN)
|
|
if not os.getenv("CLAUDE_CODE_OAUTH_TOKEN") and not os.getenv("ANTHROPIC_API_KEY"):
|
|
logger.warning("[ClaudeCode] CLAUDE_CODE_OAUTH_TOKEN 미설정 — 호스트 로그인 세션이 없으면 인증 실패")
|
|
|
|
async def analyze(
|
|
self,
|
|
system_prompt: str,
|
|
user_message: str,
|
|
tools: List[Dict[str, Any]] = None,
|
|
max_tokens: int = None,
|
|
model_override: str = None,
|
|
) -> Dict[str, Any]:
|
|
use_model = model_override or self.model
|
|
if tools:
|
|
logger.debug("[ClaudeCode] tools %d개 무시 (CLI 모드는 tool_use 미지원)", len(tools))
|
|
|
|
full_system = f"{_CACHED_CONTEXT}\n\n{system_prompt}"
|
|
cmd = [
|
|
_CLI_BIN, "-p", user_message,
|
|
"--output-format", "json",
|
|
"--model", use_model,
|
|
"--system-prompt", full_system,
|
|
"--tools", "", # 파일/셸 도구 비활성 — 순수 텍스트 분석
|
|
"--max-turns", "1",
|
|
]
|
|
|
|
env = os.environ.copy()
|
|
env.pop("ANTHROPIC_API_KEY", None) # 크레딧 없는 API 키가 구독 인증을 가리지 않도록
|
|
|
|
try:
|
|
async with self._sem:
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*cmd,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
env=env,
|
|
cwd=_CLI_CWD,
|
|
)
|
|
try:
|
|
out, err = await asyncio.wait_for(proc.communicate(), timeout=_CLI_TIMEOUT)
|
|
except asyncio.TimeoutError:
|
|
proc.kill()
|
|
raise RuntimeError(f"claude CLI timeout ({_CLI_TIMEOUT}s)")
|
|
|
|
if proc.returncode != 0:
|
|
raise RuntimeError(
|
|
f"claude CLI exit={proc.returncode}: {err.decode(errors='ignore')[-500:]}"
|
|
)
|
|
|
|
data = json.loads(out.decode())
|
|
if data.get("is_error"):
|
|
raise RuntimeError(f"claude CLI error: {data.get('result')}")
|
|
|
|
usage = data.get("usage") or {}
|
|
result = {
|
|
"content": [{"type": "text", "text": data.get("result", "")}],
|
|
"stop_reason": data.get("stop_reason", "end_turn"),
|
|
"model_used": use_model,
|
|
"usage": {
|
|
"input_tokens": usage.get("input_tokens", 0),
|
|
"output_tokens": usage.get("output_tokens", 0),
|
|
"cache_read_tokens": usage.get("cache_read_input_tokens", 0),
|
|
"cache_write_tokens": usage.get("cache_creation_input_tokens", 0),
|
|
},
|
|
}
|
|
logger.debug(
|
|
"[ClaudeCode] model=%s in=%s out=%s cost=$%s",
|
|
use_model, usage.get("input_tokens"), usage.get("output_tokens"),
|
|
data.get("total_cost_usd"),
|
|
)
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Claude Code analysis error: {e}")
|
|
raise
|
|
|
|
|
|
claude_code_client = ClaudeCodeClient()
|