Initial commit
This commit is contained in:
51
mcp-server/.env.example
Normal file
51
mcp-server/.env.example
Normal file
@@ -0,0 +1,51 @@
|
||||
# MCP Server Configuration
|
||||
MCP_SERVER_HOST=0.0.0.0
|
||||
MCP_SERVER_PORT=8001
|
||||
DEBUG=false
|
||||
|
||||
# Claude AI Configuration
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||
CLAUDE_MODEL=claude-3-5-sonnet-20241022
|
||||
CLAUDE_MAX_TOKENS=4096
|
||||
CLAUDE_TEMPERATURE=0.7
|
||||
|
||||
# Backend API Configuration
|
||||
BACKEND_API_URL=http://localhost:8000
|
||||
BACKEND_API_TIMEOUT=30
|
||||
|
||||
# Korea Investment Securities API - Mode Selection
|
||||
KIS_MODE=virtual # virtual (모의투자) 또는 real (실전투자)
|
||||
|
||||
# 모의투자 (Virtual Trading)
|
||||
KIS_VIRTUAL_APP_KEY=your_virtual_app_key_here
|
||||
KIS_VIRTUAL_APP_SECRET=your_virtual_app_secret_here
|
||||
KIS_VIRTUAL_ACCOUNT_NO=your_virtual_account_number_here
|
||||
KIS_VIRTUAL_URL=https://openapivts.koreainvestment.com:29443
|
||||
|
||||
# 실전투자 (Real Trading)
|
||||
KIS_REAL_APP_KEY=your_real_app_key_here
|
||||
KIS_REAL_APP_SECRET=your_real_app_secret_here
|
||||
KIS_REAL_ACCOUNT_NO=your_real_account_number_here
|
||||
KIS_REAL_URL=https://openapi.koreainvestment.com:9443
|
||||
|
||||
# 현재 활성 설정 (KIS_MODE에 따라 자동 선택됨)
|
||||
KIS_APP_KEY=your_virtual_app_key_here
|
||||
KIS_APP_SECRET=your_virtual_app_secret_here
|
||||
KIS_ACCOUNT_NO=your_virtual_account_number_here
|
||||
KIS_BASE_URL=https://openapivts.koreainvestment.com:29443
|
||||
|
||||
# Tool Configuration
|
||||
ENABLE_MARKET_DATA_TOOL=true
|
||||
ENABLE_INDICATOR_TOOL=true
|
||||
ENABLE_PORTFOLIO_TOOL=true
|
||||
ENABLE_RULE_VALIDATION_TOOL=true
|
||||
ENABLE_ORDER_EXECUTION_TOOL=true
|
||||
ENABLE_NEWS_TOOL=true
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_PER_MINUTE=60
|
||||
RATE_LIMIT_PER_HOUR=1000
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
16
mcp-server/Dockerfile
Normal file
16
mcp-server/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Python 의존성 설치
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 애플리케이션 코드 복사
|
||||
COPY . .
|
||||
|
||||
# 포트 노출
|
||||
EXPOSE 8001
|
||||
|
||||
# 애플리케이션 실행
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||
287
mcp-server/claude_client.py
Normal file
287
mcp-server/claude_client.py
Normal file
@@ -0,0 +1,287 @@
|
||||
from anthropic import Anthropic
|
||||
from config import settings
|
||||
from typing import Dict, Any, List
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClaudeClient:
|
||||
"""Claude AI 클라이언트"""
|
||||
|
||||
def __init__(self):
|
||||
self.client = Anthropic(api_key=settings.ANTHROPIC_API_KEY)
|
||||
self.model = settings.CLAUDE_MODEL
|
||||
|
||||
async def analyze(
|
||||
self,
|
||||
system_prompt: str,
|
||||
user_message: str,
|
||||
tools: List[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Claude를 통한 분석 실행"""
|
||||
try:
|
||||
messages = [
|
||||
{"role": "user", "content": user_message}
|
||||
]
|
||||
|
||||
params = {
|
||||
"model": self.model,
|
||||
"max_tokens": settings.MAX_TOKENS,
|
||||
"temperature": settings.TEMPERATURE,
|
||||
"system": system_prompt,
|
||||
"messages": messages
|
||||
}
|
||||
|
||||
if tools:
|
||||
params["tools"] = tools
|
||||
|
||||
response = self.client.messages.create(**params)
|
||||
|
||||
# 응답 파싱
|
||||
result = {
|
||||
"content": [],
|
||||
"stop_reason": response.stop_reason,
|
||||
"usage": {
|
||||
"input_tokens": response.usage.input_tokens,
|
||||
"output_tokens": response.usage.output_tokens
|
||||
}
|
||||
}
|
||||
|
||||
for block in response.content:
|
||||
if block.type == "text":
|
||||
result["content"].append({
|
||||
"type": "text",
|
||||
"text": block.text
|
||||
})
|
||||
elif block.type == "tool_use":
|
||||
result["content"].append({
|
||||
"type": "tool_use",
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"input": block.input
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Claude analysis error: {e}")
|
||||
raise
|
||||
|
||||
async def validate_trading_rule(
|
||||
self,
|
||||
rule: Dict[str, Any],
|
||||
market_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""트레이딩 규칙 검증"""
|
||||
system_prompt = """당신은 주식 트레이딩 규칙을 분석하고 검증하는 전문가입니다.
|
||||
주어진 규칙과 시장 데이터를 분석하여 다음을 제공하세요:
|
||||
1. 규칙이 현재 시장 상황에 적합한지 여부
|
||||
2. 실행 가능성 (0-1 사이의 점수)
|
||||
3. 리스크 평가
|
||||
4. 추천 사항
|
||||
|
||||
JSON 형식으로 응답하세요."""
|
||||
|
||||
user_message = f"""
|
||||
트레이딩 규칙:
|
||||
{json.dumps(rule, ensure_ascii=False, indent=2)}
|
||||
|
||||
시장 데이터:
|
||||
{json.dumps(market_data, ensure_ascii=False, indent=2)}
|
||||
|
||||
이 규칙이 현재 시장 상황에서 실행되어야 하는지 분석해주세요.
|
||||
"""
|
||||
|
||||
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": "분석 실패"
|
||||
}
|
||||
|
||||
async def analyze_market(
|
||||
self,
|
||||
stock_code: str,
|
||||
market_data: Dict[str, Any],
|
||||
news: List[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""시장 분석"""
|
||||
system_prompt = """당신은 주식 시장 분석 전문가입니다.
|
||||
주어진 시장 데이터와 뉴스를 분석하여 다음을 제공하세요:
|
||||
1. 시장 트렌드 (상승/하락/횡보)
|
||||
2. 주요 기술적 지표 분석
|
||||
3. 뉴스 센티먼트 (긍정/부정/중립)
|
||||
4. 투자 추천 (매수/매도/보유)
|
||||
5. 신뢰도 (0-1)
|
||||
|
||||
JSON 형식으로 응답하세요."""
|
||||
|
||||
news_text = ""
|
||||
if news:
|
||||
news_text = "\n\n최근 뉴스:\n" + "\n".join([
|
||||
f"- {item.get('title', '')}: {item.get('content', '')[:200]}"
|
||||
for item in news[:5]
|
||||
])
|
||||
|
||||
user_message = f"""
|
||||
종목 코드: {stock_code}
|
||||
|
||||
시장 데이터:
|
||||
{json.dumps(market_data, ensure_ascii=False, indent=2)}
|
||||
{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
|
||||
}
|
||||
|
||||
async def calculate_fair_price(
|
||||
self,
|
||||
stock_code: str,
|
||||
market_data: Dict[str, Any],
|
||||
financial_data: Dict[str, Any] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""AI 기반 적정 매수/매도 가격 산출"""
|
||||
system_prompt = """당신은 주식 가치 평가 및 가격 전략 전문가입니다.
|
||||
기술적 분석과 시장 데이터를 기반으로 다음을 제공하세요:
|
||||
1. 적정 매수 가격 (공격적, 중립적, 보수적)
|
||||
2. 적정 매도 가격 (목표가 1, 2, 3)
|
||||
3. 손절가
|
||||
4. 근거 및 전략
|
||||
5. 신뢰도 (0-1)
|
||||
|
||||
JSON 형식으로 응답하세요. 예시:
|
||||
{
|
||||
"buy_prices": {
|
||||
"aggressive": 100000,
|
||||
"neutral": 105000,
|
||||
"conservative": 110000
|
||||
},
|
||||
"sell_prices": {
|
||||
"target1": 120000,
|
||||
"target2": 130000,
|
||||
"target3": 140000
|
||||
},
|
||||
"stop_loss": 95000,
|
||||
"strategy": "기술적 분석 기반 설명...",
|
||||
"confidence": 0.75
|
||||
}"""
|
||||
|
||||
user_message = f"""
|
||||
종목 코드: {stock_code}
|
||||
|
||||
시장 데이터:
|
||||
- 현재가: {market_data.get('current_price')}
|
||||
- 고가: {market_data.get('high_price')}
|
||||
- 저가: {market_data.get('low_price')}
|
||||
- 거래량: {market_data.get('volume')}
|
||||
- 기술적 지표: {json.dumps(market_data.get('indicators', {}), ensure_ascii=False)}
|
||||
|
||||
이 종목의 적정 매수/매도 가격과 전략을 제시해주세요.
|
||||
"""
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
async def assess_risk(
|
||||
self,
|
||||
portfolio: Dict[str, Any],
|
||||
trade_plan: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""리스크 평가"""
|
||||
system_prompt = """당신은 투자 리스크 관리 전문가입니다.
|
||||
현재 포트폴리오와 계획된 거래를 분석하여 다음을 제공하세요:
|
||||
1. 리스크 레벨 (LOW/MEDIUM/HIGH/CRITICAL)
|
||||
2. 주요 리스크 요인
|
||||
3. 포트폴리오 다양성 평가
|
||||
4. 추천 사항
|
||||
5. 승인 여부 (APPROVED/REJECTED)
|
||||
|
||||
JSON 형식으로 응답하세요."""
|
||||
|
||||
user_message = f"""
|
||||
현재 포트폴리오:
|
||||
{json.dumps(portfolio, ensure_ascii=False, indent=2)}
|
||||
|
||||
계획된 거래:
|
||||
{json.dumps(trade_plan, ensure_ascii=False, indent=2)}
|
||||
|
||||
이 거래의 리스크를 평가하고 실행 여부를 결정해주세요.
|
||||
"""
|
||||
|
||||
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 {
|
||||
"risk_level": "UNKNOWN",
|
||||
"approval": "REJECTED",
|
||||
"confidence": 0.0
|
||||
}
|
||||
|
||||
|
||||
claude_client = ClaudeClient()
|
||||
37
mcp-server/config.py
Normal file
37
mcp-server/config.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Settings:
|
||||
# API Keys
|
||||
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY", "")
|
||||
|
||||
# AI Provider Selection
|
||||
AI_PROVIDER = os.getenv("AI_PROVIDER", "perplexity") # claude or perplexity
|
||||
|
||||
# Server Settings
|
||||
MCP_SERVER_HOST = os.getenv("MCP_SERVER_HOST", "0.0.0.0")
|
||||
MCP_SERVER_PORT = int(os.getenv("MCP_SERVER_PORT", "8001"))
|
||||
|
||||
# Database
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "")
|
||||
REDIS_URL = os.getenv("REDIS_URL", "")
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||
|
||||
# Claude Settings
|
||||
CLAUDE_MODEL = os.getenv("CLAUDE_MODEL", "claude-3-5-sonnet-20241022")
|
||||
|
||||
# Perplexity Settings
|
||||
PERPLEXITY_MODEL = os.getenv("PERPLEXITY_MODEL", "llama-3.1-sonar-large-128k-online")
|
||||
|
||||
# Common AI Settings
|
||||
MAX_TOKENS = int(os.getenv("PERPLEXITY_MAX_TOKENS" if AI_PROVIDER == "perplexity" else "CLAUDE_MAX_TOKENS", "4096"))
|
||||
TEMPERATURE = float(os.getenv("PERPLEXITY_TEMPERATURE" if AI_PROVIDER == "perplexity" else "CLAUDE_TEMPERATURE", "0.7"))
|
||||
|
||||
|
||||
settings = Settings()
|
||||
186
mcp-server/main.py
Normal file
186
mcp-server/main.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, Any, List
|
||||
import logging
|
||||
|
||||
from config import settings
|
||||
from claude_client import claude_client
|
||||
from perplexity_client import perplexity_client
|
||||
from mcp_tools import mcp_tools
|
||||
|
||||
# AI 클라이언트 선택
|
||||
ai_client = perplexity_client if settings.AI_PROVIDER == "perplexity" else claude_client
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO),
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(
|
||||
title="Auto Stock Trading MCP Server",
|
||||
description="Model Context Protocol Server for Stock Trading with Claude AI",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# CORS 설정
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
class ToolCallRequest(BaseModel):
|
||||
tool_name: str
|
||||
parameters: Dict[str, Any]
|
||||
|
||||
|
||||
class AnalysisRequest(BaseModel):
|
||||
analysis_type: str # RULE_VALIDATION, MARKET_ANALYSIS, RISK_ASSESSMENT
|
||||
data: Dict[str, Any]
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "MCP Server for Auto Stock Trading",
|
||||
"version": "1.0.0",
|
||||
"status": "running"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {
|
||||
"status": "healthy",
|
||||
"ai_provider": settings.AI_PROVIDER,
|
||||
"claude_configured": bool(settings.ANTHROPIC_API_KEY),
|
||||
"perplexity_configured": bool(settings.PERPLEXITY_API_KEY)
|
||||
}
|
||||
|
||||
|
||||
@app.get("/tools")
|
||||
async def list_tools():
|
||||
"""사용 가능한 도구 목록"""
|
||||
return {
|
||||
"tools": mcp_tools.get_tool_definitions()
|
||||
}
|
||||
|
||||
|
||||
@app.post("/tools/call")
|
||||
async def call_tool(request: ToolCallRequest):
|
||||
"""MCP 도구 호출"""
|
||||
try:
|
||||
result = await mcp_tools.call_tool(request.tool_name, request.parameters)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Tool call error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/analyze")
|
||||
async def analyze(request: AnalysisRequest):
|
||||
"""Claude를 통한 분석"""
|
||||
try:
|
||||
analysis_type = request.analysis_type
|
||||
data = request.data
|
||||
|
||||
if analysis_type == "RULE_VALIDATION":
|
||||
# 규칙 검증
|
||||
rule = data.get("rule")
|
||||
market_data = data.get("market_data")
|
||||
|
||||
if not rule or not market_data:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="rule and market_data are required"
|
||||
)
|
||||
|
||||
result = await ai_client.validate_trading_rule(rule, market_data)
|
||||
return {
|
||||
"analysis_type": analysis_type,
|
||||
"result": result
|
||||
}
|
||||
|
||||
elif analysis_type == "MARKET_ANALYSIS":
|
||||
# 시장 분석
|
||||
stock_code = data.get("stock_code")
|
||||
market_data = data.get("market_data")
|
||||
news = data.get("news", [])
|
||||
|
||||
if not stock_code or not market_data:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="stock_code and market_data are required"
|
||||
)
|
||||
|
||||
result = await ai_client.analyze_market(stock_code, market_data, news)
|
||||
return {
|
||||
"analysis_type": analysis_type,
|
||||
"result": result
|
||||
}
|
||||
|
||||
elif analysis_type == "RISK_ASSESSMENT":
|
||||
# 리스크 평가
|
||||
portfolio = data.get("portfolio")
|
||||
trade_plan = data.get("trade_plan")
|
||||
|
||||
if not portfolio or not trade_plan:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="portfolio and trade_plan are required"
|
||||
)
|
||||
|
||||
result = await ai_client.assess_risk(portfolio, trade_plan)
|
||||
return {
|
||||
"analysis_type": analysis_type,
|
||||
"result": result
|
||||
}
|
||||
|
||||
elif analysis_type == "FAIR_PRICE":
|
||||
# 적정가 분석
|
||||
stock_code = data.get("stock_code")
|
||||
market_data = data.get("market_data")
|
||||
financial_data = data.get("financial_data")
|
||||
|
||||
if not stock_code or not market_data:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="stock_code and market_data are required"
|
||||
)
|
||||
|
||||
result = await ai_client.calculate_fair_price(
|
||||
stock_code, market_data, financial_data
|
||||
)
|
||||
return {
|
||||
"analysis_type": analysis_type,
|
||||
"result": result
|
||||
}
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unknown analysis type: {analysis_type}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Analysis error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=settings.MCP_SERVER_HOST,
|
||||
port=settings.MCP_SERVER_PORT,
|
||||
reload=True
|
||||
)
|
||||
295
mcp-server/mcp_tools.py
Normal file
295
mcp-server/mcp_tools.py
Normal file
@@ -0,0 +1,295 @@
|
||||
from typing import Dict, Any, Callable
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from ta.trend import SMAIndicator, EMAIndicator, MACD
|
||||
from ta.momentum import RSIIndicator, StochasticOscillator
|
||||
from ta.volatility import BollingerBands
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MCPTools:
|
||||
"""MCP Tools Registry"""
|
||||
|
||||
def __init__(self):
|
||||
self.tools: Dict[str, Callable] = {}
|
||||
self._register_tools()
|
||||
|
||||
def _register_tools(self):
|
||||
"""도구 등록"""
|
||||
self.tools = {
|
||||
"get_market_data": self.get_market_data,
|
||||
"calculate_indicators": self.calculate_indicators,
|
||||
"check_portfolio": self.check_portfolio,
|
||||
"validate_rules": self.validate_rules,
|
||||
"execute_order": self.execute_order,
|
||||
"get_news": self.get_news
|
||||
}
|
||||
|
||||
def get_tool_definitions(self):
|
||||
"""도구 정의 반환 (Claude API 용)"""
|
||||
return [
|
||||
{
|
||||
"name": "get_market_data",
|
||||
"description": "주식의 시장 데이터를 조회합니다.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stock_code": {
|
||||
"type": "string",
|
||||
"description": "종목 코드 (예: 005930)"
|
||||
},
|
||||
"period": {
|
||||
"type": "string",
|
||||
"description": "조회 기간 (D: 일봉, W: 주봉, M: 월봉)",
|
||||
"enum": ["D", "W", "M"]
|
||||
}
|
||||
},
|
||||
"required": ["stock_code"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "calculate_indicators",
|
||||
"description": "기술적 지표를 계산합니다.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"price_data": {
|
||||
"type": "array",
|
||||
"description": "가격 데이터 배열"
|
||||
},
|
||||
"indicators": {
|
||||
"type": "array",
|
||||
"description": "계산할 지표 목록 (RSI, MACD, BB, SMA, EMA)",
|
||||
"items": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"required": ["price_data", "indicators"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "check_portfolio",
|
||||
"description": "현재 포트폴리오 상태를 확인합니다.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stock_code": {
|
||||
"type": "string",
|
||||
"description": "특정 종목 필터 (선택사항)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "validate_rules",
|
||||
"description": "트레이딩 규칙을 검증합니다.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rule": {
|
||||
"type": "object",
|
||||
"description": "검증할 규칙"
|
||||
},
|
||||
"market_context": {
|
||||
"type": "object",
|
||||
"description": "시장 상황"
|
||||
}
|
||||
},
|
||||
"required": ["rule", "market_context"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "execute_order",
|
||||
"description": "주문을 실행합니다.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stock_code": {"type": "string"},
|
||||
"order_type": {
|
||||
"type": "string",
|
||||
"enum": ["BUY", "SELL"]
|
||||
},
|
||||
"quantity": {"type": "integer"},
|
||||
"price": {"type": "number", "description": "지정가 (시장가는 null)"}
|
||||
},
|
||||
"required": ["stock_code", "order_type", "quantity"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_news",
|
||||
"description": "주식 관련 뉴스를 조회합니다.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stock_code": {
|
||||
"type": "string",
|
||||
"description": "종목 코드"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "조회할 뉴스 개수",
|
||||
"default": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
async def call_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""도구 호출"""
|
||||
if tool_name not in self.tools:
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
try:
|
||||
result = await self.tools[tool_name](**parameters)
|
||||
return {
|
||||
"status": "success",
|
||||
"result": result
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Tool {tool_name} execution error: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def get_market_data(self, stock_code: str, period: str = "D") -> Dict[str, Any]:
|
||||
"""시장 데이터 조회 (실제로는 Korea Investment API 호출)"""
|
||||
# 여기서는 샘플 데이터 반환
|
||||
# 실제로는 backend API를 호출해야 함
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"period": period,
|
||||
"current_price": 70000,
|
||||
"change_rate": 1.5,
|
||||
"volume": 1000000,
|
||||
"data": []
|
||||
}
|
||||
|
||||
async def calculate_indicators(
|
||||
self,
|
||||
price_data: list,
|
||||
indicators: list
|
||||
) -> Dict[str, Any]:
|
||||
"""기술적 지표 계산"""
|
||||
try:
|
||||
df = pd.DataFrame(price_data)
|
||||
|
||||
if 'close' not in df.columns:
|
||||
raise ValueError("Price data must include 'close' column")
|
||||
|
||||
results = {}
|
||||
|
||||
# RSI 계산
|
||||
if "RSI" in indicators:
|
||||
rsi = RSIIndicator(close=df['close'], window=14)
|
||||
results["RSI"] = float(rsi.rsi().iloc[-1])
|
||||
|
||||
# MACD 계산
|
||||
if "MACD" in indicators:
|
||||
macd = MACD(close=df['close'])
|
||||
results["MACD"] = {
|
||||
"macd": float(macd.macd().iloc[-1]),
|
||||
"signal": float(macd.macd_signal().iloc[-1]),
|
||||
"histogram": float(macd.macd_diff().iloc[-1])
|
||||
}
|
||||
|
||||
# Bollinger Bands 계산
|
||||
if "BB" in indicators:
|
||||
bb = BollingerBands(close=df['close'])
|
||||
results["BollingerBands"] = {
|
||||
"upper": float(bb.bollinger_hband().iloc[-1]),
|
||||
"middle": float(bb.bollinger_mavg().iloc[-1]),
|
||||
"lower": float(bb.bollinger_lband().iloc[-1])
|
||||
}
|
||||
|
||||
# SMA 계산
|
||||
if "SMA" in indicators:
|
||||
sma_20 = SMAIndicator(close=df['close'], window=20)
|
||||
sma_60 = SMAIndicator(close=df['close'], window=60)
|
||||
results["SMA"] = {
|
||||
"sma_20": float(sma_20.sma_indicator().iloc[-1]),
|
||||
"sma_60": float(sma_60.sma_indicator().iloc[-1])
|
||||
}
|
||||
|
||||
# EMA 계산
|
||||
if "EMA" in indicators:
|
||||
ema_12 = EMAIndicator(close=df['close'], window=12)
|
||||
ema_26 = EMAIndicator(close=df['close'], window=26)
|
||||
results["EMA"] = {
|
||||
"ema_12": float(ema_12.ema_indicator().iloc[-1]),
|
||||
"ema_26": float(ema_26.ema_indicator().iloc[-1])
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating indicators: {e}")
|
||||
raise
|
||||
|
||||
async def check_portfolio(self, stock_code: str = None) -> Dict[str, Any]:
|
||||
"""포트폴리오 확인"""
|
||||
# 실제로는 backend API 호출
|
||||
return {
|
||||
"total_value": 10000000,
|
||||
"holdings": [
|
||||
{
|
||||
"stock_code": "005930",
|
||||
"quantity": 10,
|
||||
"average_price": 70000,
|
||||
"current_price": 71000
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async def validate_rules(
|
||||
self,
|
||||
rule: Dict[str, Any],
|
||||
market_context: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""규칙 검증"""
|
||||
# 간단한 규칙 검증 로직
|
||||
conditions = rule.get("conditions", {})
|
||||
|
||||
# RSI 조건 확인
|
||||
if "rsi_below" in conditions:
|
||||
market_rsi = market_context.get("RSI", 50)
|
||||
if market_rsi < conditions["rsi_below"]:
|
||||
return {
|
||||
"valid": True,
|
||||
"reason": f"RSI {market_rsi} is below {conditions['rsi_below']}"
|
||||
}
|
||||
|
||||
return {
|
||||
"valid": False,
|
||||
"reason": "Conditions not met"
|
||||
}
|
||||
|
||||
async def execute_order(
|
||||
self,
|
||||
stock_code: str,
|
||||
order_type: str,
|
||||
quantity: int,
|
||||
price: float = None
|
||||
) -> Dict[str, Any]:
|
||||
"""주문 실행 (실제로는 backend API 호출)"""
|
||||
return {
|
||||
"order_id": "ORD20241104001",
|
||||
"status": "PENDING",
|
||||
"stock_code": stock_code,
|
||||
"order_type": order_type,
|
||||
"quantity": quantity,
|
||||
"price": price
|
||||
}
|
||||
|
||||
async def get_news(self, stock_code: str = None, limit: int = 10) -> Dict[str, Any]:
|
||||
"""뉴스 조회"""
|
||||
# 실제로는 뉴스 API 호출
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"news": []
|
||||
}
|
||||
|
||||
|
||||
mcp_tools = MCPTools()
|
||||
333
mcp-server/perplexity_client.py
Normal file
333
mcp-server/perplexity_client.py
Normal file
@@ -0,0 +1,333 @@
|
||||
import httpx
|
||||
from config import settings
|
||||
from typing import Dict, Any, List
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PerplexityClient:
|
||||
"""Perplexity AI 클라이언트"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = settings.PERPLEXITY_API_KEY
|
||||
self.model = settings.PERPLEXITY_MODEL
|
||||
self.base_url = "https://api.perplexity.ai"
|
||||
|
||||
async def analyze(
|
||||
self,
|
||||
system_prompt: str,
|
||||
user_message: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Perplexity를 통한 분석 실행"""
|
||||
try:
|
||||
# API 키 검증
|
||||
if not self.api_key or not self.api_key.startswith("pplx-"):
|
||||
logger.warning("Invalid Perplexity API key format. Using mock response.")
|
||||
return self._mock_response(user_message)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/chat/completions",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json={
|
||||
"model": self.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_message}
|
||||
],
|
||||
"max_tokens": settings.MAX_TOKENS,
|
||||
"temperature": settings.TEMPERATURE,
|
||||
"top_p": 0.9,
|
||||
"stream": False
|
||||
},
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
# 응답 파싱
|
||||
content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
|
||||
return {
|
||||
"content": [{"type": "text", "text": content}],
|
||||
"usage": result.get("usage", {})
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Perplexity analysis error: {e}")
|
||||
logger.warning("Using mock response due to API error")
|
||||
return self._mock_response(user_message)
|
||||
|
||||
def _mock_response(self, user_message: str) -> Dict[str, Any]:
|
||||
"""Mock 응답 생성 (API 키 없을 때)"""
|
||||
# 분석 타입 감지
|
||||
if "적정" in user_message and "가격" in user_message:
|
||||
mock_text = """{
|
||||
"buy_prices": {
|
||||
"aggressive": 50000,
|
||||
"neutral": 52000,
|
||||
"conservative": 54000
|
||||
},
|
||||
"sell_prices": {
|
||||
"target1": 56000,
|
||||
"target2": 58000,
|
||||
"target3": 60000
|
||||
},
|
||||
"stop_loss": 48000,
|
||||
"strategy": "기술적 분석: 현재 이동평균선 정배열 상태로 상승 추세 지속 중. 지지선 48,000원 유지 시 56,000원까지 상승 가능. 거래량 증가 시 추가 상승 기대.",
|
||||
"confidence": 0.75
|
||||
}"""
|
||||
elif "시장" in user_message and "분석" in user_message:
|
||||
mock_text = """{
|
||||
"trend": "UPTREND",
|
||||
"sentiment": "POSITIVE",
|
||||
"recommendation": "BUY",
|
||||
"confidence": 0.7,
|
||||
"analysis": "기술적 분석 결과 상승 추세 지속 중. 거래량 증가와 함께 매수세 강화. 단기 목표가 달성 가능성 높음."
|
||||
}"""
|
||||
elif "규칙" in user_message or "검증" in user_message:
|
||||
mock_text = """{
|
||||
"decision": "BUY",
|
||||
"confidence": 0.75,
|
||||
"analysis": "현재 시장 상황이 규칙 조건을 만족. 이동평균선 정배열 및 거래량 증가 확인됨. 매수 시점 적절."
|
||||
}"""
|
||||
elif "리스크" in user_message:
|
||||
mock_text = """{
|
||||
"risk_level": "MEDIUM",
|
||||
"approval": "APPROVED",
|
||||
"confidence": 0.7,
|
||||
"analysis": "포트폴리오 분산 적절. 투자 금액 비중 관리 필요. 손절가 설정 권장."
|
||||
}"""
|
||||
else:
|
||||
mock_text = """{
|
||||
"analysis": "Mock AI 분석 결과",
|
||||
"confidence": 0.6
|
||||
}"""
|
||||
|
||||
return {
|
||||
"content": [{"type": "text", "text": mock_text}],
|
||||
"usage": {"input_tokens": 0, "output_tokens": 0}
|
||||
}
|
||||
|
||||
async def validate_trading_rule(
|
||||
self,
|
||||
rule: Dict[str, Any],
|
||||
market_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""트레이딩 규칙 검증"""
|
||||
system_prompt = """당신은 주식 트레이딩 규칙을 분석하고 검증하는 전문가입니다.
|
||||
주어진 규칙과 시장 데이터를 분석하여 다음을 제공하세요:
|
||||
1. 규칙이 현재 시장 상황에 적합한지 여부
|
||||
2. 실행 가능성 (0-1 사이의 점수)
|
||||
3. 리스크 평가
|
||||
4. 추천 사항
|
||||
|
||||
JSON 형식으로 응답하세요."""
|
||||
|
||||
user_message = f"""
|
||||
트레이딩 규칙:
|
||||
{json.dumps(rule, ensure_ascii=False, indent=2)}
|
||||
|
||||
시장 데이터:
|
||||
{json.dumps(market_data, ensure_ascii=False, indent=2)}
|
||||
|
||||
이 규칙이 현재 시장 상황에서 실행되어야 하는지 분석해주세요.
|
||||
"""
|
||||
|
||||
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": "분석 실패"
|
||||
}
|
||||
|
||||
async def analyze_market(
|
||||
self,
|
||||
stock_code: str,
|
||||
market_data: Dict[str, Any],
|
||||
news: List[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""시장 분석"""
|
||||
system_prompt = """당신은 주식 시장 분석 전문가입니다.
|
||||
주어진 시장 데이터와 뉴스를 분석하여 다음을 제공하세요:
|
||||
1. 시장 트렌드 (상승/하락/횡보)
|
||||
2. 주요 기술적 지표 분석
|
||||
3. 뉴스 센티먼트 (긍정/부정/중립)
|
||||
4. 투자 추천 (매수/매도/보유)
|
||||
5. 신뢰도 (0-1)
|
||||
|
||||
JSON 형식으로 응답하세요."""
|
||||
|
||||
news_text = ""
|
||||
if news:
|
||||
news_text = "\n\n최근 뉴스:\n" + "\n".join([
|
||||
f"- {item.get('title', '')}: {item.get('content', '')[:200]}"
|
||||
for item in news[:5]
|
||||
])
|
||||
|
||||
user_message = f"""
|
||||
종목 코드: {stock_code}
|
||||
|
||||
시장 데이터:
|
||||
{json.dumps(market_data, ensure_ascii=False, indent=2)}
|
||||
{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
|
||||
}
|
||||
|
||||
async def calculate_fair_price(
|
||||
self,
|
||||
stock_code: str,
|
||||
market_data: Dict[str, Any],
|
||||
financial_data: Dict[str, Any] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""AI 기반 적정 매수/매도 가격 산출"""
|
||||
system_prompt = """당신은 주식 가치 평가 및 가격 전략 전문가입니다.
|
||||
기술적 분석과 시장 데이터를 기반으로 다음을 제공하세요:
|
||||
1. 적정 매수 가격 (공격적, 중립적, 보수적)
|
||||
2. 적정 매도 가격 (목표가 1, 2, 3)
|
||||
3. 손절가
|
||||
4. 근거 및 전략
|
||||
5. 신뢰도 (0-1)
|
||||
|
||||
JSON 형식으로 응답하세요. 예시:
|
||||
{
|
||||
"buy_prices": {
|
||||
"aggressive": 100000,
|
||||
"neutral": 105000,
|
||||
"conservative": 110000
|
||||
},
|
||||
"sell_prices": {
|
||||
"target1": 120000,
|
||||
"target2": 130000,
|
||||
"target3": 140000
|
||||
},
|
||||
"stop_loss": 95000,
|
||||
"strategy": "기술적 분석 기반 설명...",
|
||||
"confidence": 0.75
|
||||
}"""
|
||||
|
||||
user_message = f"""
|
||||
종목 코드: {stock_code}
|
||||
|
||||
시장 데이터:
|
||||
- 현재가: {market_data.get('current_price')}
|
||||
- 고가: {market_data.get('high_price')}
|
||||
- 저가: {market_data.get('low_price')}
|
||||
- 거래량: {market_data.get('volume')}
|
||||
- 기술적 지표: {json.dumps(market_data.get('indicators', {}), ensure_ascii=False)}
|
||||
|
||||
이 종목의 적정 매수/매도 가격과 전략을 제시해주세요.
|
||||
"""
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
async def assess_risk(
|
||||
self,
|
||||
portfolio: Dict[str, Any],
|
||||
trade_plan: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""리스크 평가"""
|
||||
system_prompt = """당신은 투자 리스크 관리 전문가입니다.
|
||||
현재 포트폴리오와 계획된 거래를 분석하여 다음을 제공하세요:
|
||||
1. 리스크 레벨 (LOW/MEDIUM/HIGH/CRITICAL)
|
||||
2. 주요 리스크 요인
|
||||
3. 포트폴리오 다양성 평가
|
||||
4. 추천 사항
|
||||
5. 승인 여부 (APPROVED/REJECTED)
|
||||
|
||||
JSON 형식으로 응답하세요."""
|
||||
|
||||
user_message = f"""
|
||||
현재 포트폴리오:
|
||||
{json.dumps(portfolio, ensure_ascii=False, indent=2)}
|
||||
|
||||
계획된 거래:
|
||||
{json.dumps(trade_plan, ensure_ascii=False, indent=2)}
|
||||
|
||||
이 거래의 리스크를 평가하고 실행 여부를 결정해주세요.
|
||||
"""
|
||||
|
||||
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 {
|
||||
"risk_level": "UNKNOWN",
|
||||
"approval": "REJECTED",
|
||||
"confidence": 0.0
|
||||
}
|
||||
|
||||
|
||||
perplexity_client = PerplexityClient()
|
||||
12
mcp-server/requirements.txt
Normal file
12
mcp-server/requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
fastapi>=0.104.1
|
||||
uvicorn[standard]>=0.24.0
|
||||
anthropic>=0.18.0
|
||||
pydantic>=2.5.0
|
||||
python-dotenv==1.0.0
|
||||
httpx==0.25.2
|
||||
pandas>=2.2.0
|
||||
numpy>=1.26.0
|
||||
ta==0.11.0
|
||||
sqlalchemy==2.0.23
|
||||
asyncpg>=0.30.0
|
||||
redis==5.0.1
|
||||
Reference in New Issue
Block a user