Files
auto-stock/mcp-server/claude_code_client.py
shuim 028a1c352b feat: 자동매매 신뢰성 개선 — 시세 실서버 라우팅·일일 한도 전역화·수동 매수 API
- 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>
2026-09-19 23:37:08 +09:00

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()