Files
auto-stock/mcp-server/stdio_server.py
shuim 9fab4018d4 fix: 프로그램 검증 루프 7회차 — 리스크·주문·청산·인프라 결함 일괄 수정 + 거래 확대 결정 반영
하네스 규칙 검사 전역 위반 117→0, 회차별 검증(code-verifier opus 4도메인)으로 Critical/High 해소.

주문·리스크
- MDD 강제축소 반복 매도(종목별 완료 집합·재시도 상한·날짜 롤오버·잔고 오류 보류·epoch·장외 미주문)
- KIS 거부 FAILED 기록, 결과 불명(전송 후) 재전송 금지·PENDING, 미전송 실패만 재시도(msg_cd 분류)
- 국내 전량·손실 매도 시장가, 부분 익절 직전 현재가, 지정가만 미체결 취소(cancel_order_v2: TR 0013U·조직번호) + 0체결 롤백
- 누적 4%·현금·레버리지(신규분 포함)·사이클 내 매수 누적, 수동매수 공용 가드(buy_guards), 수동매도 DT 차감·락
- 가드 우회 엔드포인트 차단(/recommendations/auto-trade 위임, US /execute 매수 410·매도 가드)
- 매수 슬롯=성공 매수, 평가 상한·회전, 업무거부/계좌 쿨다운, 프로세스 내 in-flight 주문 중복 차단
데이트레이딩
- 진입·청산 shield, 청산 진행 중 표식(소유 토큰), 2단계 청산 체크, 결과 불명 → 보유 감소 판정·비DT 차감 상한
- 전일 포지션 재시도(일시 실패 미산정), 분할매도 영속, ORB 구간 손절, /cycle 락, 사이클 내 현금 차감, 체결 수량 보정
스윙
- 손절 공백(판정 예외 시 하드 바닥), 단일 손절 소스·범위 검증, 매도 직전 KIS 잔고 상한(min(주문가능,보유)−DT)
- 결과 불명 체결 대조·장부 반영, 대체가(나이·진입 이후·ATR 캐시), 분할 단계 기록, 국면 실패 fail-closed
- 듀얼 플레이북·서킷브레이커 게이트, 평균회귀 우선·듀얼 독립, 시간손절 거래일
- 거래 확대(사용자 결정, 백테스트 미검증): RSI 46/35 정본 경로, 모멘텀 v2 ON, SIDEWAYS ADX AND, 트레일링 1차 익절 후, 20일 시간손절 ON
인프라·보안
- 계좌·손익 GET 인증, 세션 is_active·관리자 경로, 서명키 파생, realip, WS 토큰 마스킹
- KIS 토큰 즉시 실패(sleep 제거)·무효 시 캐시 삭제, 주문 5xx+JSON 본문 반환
- db_worker 종료 drain(멱등 재실행·커밋 불명 격리·예산·밴 커밋), stop_grace 40s, Redis 타임아웃·volatile-lru, /health DLQ·캐시
- KRX 휴장일, py3.11 비호환 f-string 수정 + Jenkins 이미지 내 compileall·헬스 실패 시 빌드 실패
하네스: rules_check 에 PEP 701 f-string 검사, 마커(allow-*), deploy 스모크 401 기준
CLAUDE.md: 실제 동작·플래그·백로그(§8.5-27~38) 갱신

backend 1451 passed / hooks 112 passed / tsc ok / py3.9 compile 0 errors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:01:31 +09:00

198 lines
6.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""
MCP Server with stdio transport for Claude CLI
This server provides stock trading tools via Model Context Protocol
using stdio transport for direct integration with Claude CLI.
"""
import asyncio
import sys
from typing import Any
import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# Import configuration
from config import settings
from utils import _dumps
# Initialize MCP Server
app = Server("auto-stock-mcp")
# Backend API base URL
BACKEND_URL = "http://localhost:8000/api/v1"
# [2026-10-02 H-1] 백엔드 조회 API(portfolio·trades·recommendations·market-data)는 X-API-Key 필수.
# 백엔드 security._get_api_key() 와 같은 소스(API_KEY 우선, 없으면 SECRET_KEY 폴백).
# .env 는 위의 `from config import settings` 가 import 시점에 load_dotenv(override=False) 로
# 이미 읽는다(python-dotenv, mcp-server/requirements.txt) — 그래서 이 블록은 반드시 그 import 뒤에 둔다.
# 단, mcp-server/.env 에는 API_KEY/SECRET_KEY 가 없을 수 있다(2026-10-02 로컬 확인) → 백엔드가 쓰는
# 저장소 루트 .env 를 추가로 읽는다(override=False: 이미 설정된 환경변수가 우선).
import os as _os
from pathlib import Path as _Path
if not (_os.getenv("API_KEY") or _os.getenv("SECRET_KEY")):
try:
from dotenv import load_dotenv as _load_dotenv
_load_dotenv(_Path(__file__).resolve().parents[1] / ".env", override=False)
except ImportError:
print("[stdio_server] python-dotenv 없음 — API_KEY 환경변수를 직접 설정하세요", file=sys.stderr)
_BACKEND_API_KEY = _os.getenv("API_KEY") or _os.getenv("SECRET_KEY", "")
if not _BACKEND_API_KEY:
print("[stdio_server] API_KEY/SECRET_KEY 미설정 — 백엔드 조회가 401 로 실패합니다", file=sys.stderr)
_BACKEND_HEADERS = {"X-API-Key": _BACKEND_API_KEY} if _BACKEND_API_KEY else {}
@app.list_tools()
async def list_tools() -> list[Tool]:
"""List available tools"""
return [
Tool(
name="get_portfolio",
description="Get current portfolio information including holdings and balance",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="get_market_data",
description="Get real-time market data for a specific stock",
inputSchema={
"type": "object",
"properties": {
"stock_code": {
"type": "string",
"description": "Stock code (e.g., '005930' for Samsung)"
}
},
"required": ["stock_code"]
}
),
Tool(
name="get_recommendations",
description="Get AI-generated stock recommendations based on technical analysis",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="get_trade_history",
description="Get recent trade history",
inputSchema={
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "Number of trades to return (default: 20)"
}
},
"required": []
}
),
Tool(
name="analyze_stock",
description="Perform detailed technical analysis on a stock",
inputSchema={
"type": "object",
"properties": {
"stock_code": {
"type": "string",
"description": "Stock code to analyze"
}
},
"required": ["stock_code"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""Handle tool calls"""
try:
async with httpx.AsyncClient(timeout=30.0, headers=_BACKEND_HEADERS) as client:
if name == "get_portfolio":
response = await client.get(f"{BACKEND_URL}/portfolio")
data = response.json()
return [TextContent(
type="text",
text=_dumps(data)
)]
elif name == "get_market_data":
stock_code = arguments.get("stock_code")
response = await client.get(
f"{BACKEND_URL}/market-data/prices",
params={"codes": stock_code}
)
data = response.json()
return [TextContent(
type="text",
text=_dumps(data)
)]
elif name == "get_recommendations":
response = await client.get(f"{BACKEND_URL}/recommendations/rule-based")
data = response.json()
return [TextContent(
type="text",
text=_dumps(data)
)]
elif name == "get_trade_history":
limit = arguments.get("limit", 10 if settings.TOKEN_SAVE_MODE else 20)
response = await client.get(
f"{BACKEND_URL}/trades",
params={"limit": limit}
)
data = response.json()
return [TextContent(
type="text",
text=_dumps(data)
)]
elif name == "analyze_stock":
stock_code = arguments.get("stock_code")
# Get chart data
response = await client.get(
f"{BACKEND_URL}/market-data/chart/{stock_code}",
params={"period": "D", "realtime": "true"}
)
data = response.json()
return [TextContent(
type="text",
text=_dumps(data)
)]
else:
return [TextContent(
type="text",
text=f"Unknown tool: {name}"
)]
except Exception as e:
return [TextContent(
type="text",
text=f"Error calling tool {name}: {str(e)}"
)]
async def main():
"""Run the MCP server with stdio transport"""
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())