수정본

This commit is contained in:
2026-02-22 17:23:20 +09:00
parent b147498c31
commit 32e2263f0a
109 changed files with 13979 additions and 4580 deletions

View File

@@ -0,0 +1,85 @@
import re
import asyncio
import json
import os
from datetime import datetime, timezone
from app.services.korea_investment_api import korea_investment_api
# Claude 응답에서 종목/가격 추출
CLAUDE_FORMAT = r"종목[:\s]+(\d+)" # 종목코드
ENTRY_FORMAT = r"진입가[:\s]+([\d,]+)" # 진입가
STOP_FORMAT = r"손절가[:\s]+([\d,]+)" # 손절가
TARGET_FORMAT = r"목표가[:\s]+([\d,]+)" # 목표가
ACTIVE_TRADES_FILE = os.path.join(os.path.dirname(__file__), 'active_trades.json')
def parse_claude_response(text):
code = re.search(CLAUDE_FORMAT, text)
entry = re.search(ENTRY_FORMAT, text)
stop = re.search(STOP_FORMAT, text)
target = re.search(TARGET_FORMAT, text)
return {
'stock_code': code.group(1) if code else None,
'entry_price': int(entry.group(1).replace(',', '')) if entry else None,
'stop_loss': int(stop.group(1).replace(',', '')) if stop else None,
'take_profit': int(target.group(1).replace(',', '')) if target else None,
}
def save_active_trade(trade_info):
"""활성화된 거래를 JSON 파일에 저장합니다."""
trades = []
if os.path.exists(ACTIVE_TRADES_FILE):
with open(ACTIVE_TRADES_FILE, 'r', encoding='utf-8') as f:
try:
trades = json.load(f)
except json.JSONDecodeError:
print(f"경고: {ACTIVE_TRADES_FILE} 파일이 비어있거나 잘못된 형식입니다. 새 파일을 생성합니다.")
trades.append(trade_info)
with open(ACTIVE_TRADES_FILE, 'w', encoding='utf-8') as f:
json.dump(trades, f, indent=4, ensure_ascii=False)
print(f"✅ 거래 정보 저장 완료: {trade_info['stock_code']}")
async def auto_trade_from_claude(text, quantity=1):
info = parse_claude_response(text)
if info['stock_code'] and info['entry_price']:
print(f"▶️ 매수 주문 시도: {info['stock_code']}, 수량: {quantity}, 가격: {info['entry_price']}")
# 지정가 매수 주문
result = await korea_investment_api.place_order(
stock_code=info['stock_code'],
order_type='BUY',
quantity=quantity,
price=info['entry_price']
)
print('주문 결과:', result)
# API 응답 구조에 따라 rt_cd가 '0'이면 성공으로 간주
if result and result.get('rt_cd') == '0':
print("✅ 매수 주문 성공. 거래 정보를 저장합니다.")
trade_details = {
'stock_code': info['stock_code'],
'quantity': quantity,
'entry_price': info['entry_price'],
'stop_loss': info['stop_loss'],
'take_profit': info['take_profit'],
'order_id': result.get('ODNO'), # 주문번호
'order_time': result.get('ORD_TMD'), # 주문시각
'timestamp': datetime.now(timezone.utc).isoformat()
}
save_active_trade(trade_details)
else:
error_message = result.get('msg1', '알 수 없는 오류')
print(f"❌ 매수 주문 실패: {error_message}")
else:
print('Claude 응답에서 종목/가격을 찾을 수 없습니다.')
if __name__ == "__main__":
# 예시: Claude 응답 텍스트를 받아 자동매매 실행
import sys
if len(sys.argv) > 1:
text = sys.argv[1]
asyncio.run(auto_trade_from_claude(text))
else:
print("사용법: python auto_trade_claude.py 'Claude 응답 텍스트'")

View File

@@ -1,5 +1,6 @@
from anthropic import Anthropic
from anthropic import AsyncAnthropic
from config import settings
from utils import _dumps
from typing import Dict, Any, List
import json
import logging
@@ -9,9 +10,9 @@ logger = logging.getLogger(__name__)
class ClaudeClient:
"""Claude AI 클라이언트"""
def __init__(self):
self.client = Anthropic(api_key=settings.ANTHROPIC_API_KEY)
self.client = AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
self.model = settings.CLAUDE_MODEL
async def analyze(
@@ -37,7 +38,7 @@ class ClaudeClient:
if tools:
params["tools"] = tools
response = self.client.messages.create(**params)
response = await self.client.messages.create(**params)
# 응답 파싱
result = {
@@ -72,27 +73,18 @@ class ClaudeClient:
async def validate_trading_rule(
self,
rule: Dict[str, Any],
market_data: Dict[str, Any]
market_data: Dict[str, Any],
news_data: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""트레이딩 규칙 검증"""
system_prompt = """당신은 주식 트레이딩 규칙을 분석하고 검증하는 전문가입니다.
주어진 규칙과 시장 데이터를 분석하여 다음을 제공하세요:
1. 규칙이 현재 시장 상황에 적합한지 여부
2. 실행 가능성 (0-1 사이의 점수)
3. 리스크 평가
4. 추천 사항
system_prompt = """주식 트레이딩 규칙 검증 전문가. 규칙/시장/뉴스를 분석하여 JSON으로 응답.
필수 필드: decision(BUY/SELL/HOLD), confidence(0-1), risk_level, recommendation.
악재(유상증자,감자,소송,실적악화) 시 confidence 낮추고 경고 포함."""
JSON 형식으로 응답하세요."""
user_message = f"""
트레이딩 규칙:
{json.dumps(rule, ensure_ascii=False, indent=2)}
user_message = f"규칙:{_dumps(rule)}\n시장:{_dumps(market_data)}"
시장 데이터:
{json.dumps(market_data, ensure_ascii=False, indent=2)}
이 규칙이 현재 시장 상황에서 실행되어야 하는지 분석해주세요.
"""
if news_data:
user_message += f"\n뉴스:{_dumps(news_data)}"
response = await self.analyze(system_prompt, user_message)
@@ -123,32 +115,17 @@ JSON 형식으로 응답하세요."""
news: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""시장 분석"""
system_prompt = """당신은 주식 시장 분석 전문가입니다.
주어진 시장 데이터와 뉴스를 분석하여 다음을 제공하세요:
1. 시장 트렌드 (상승/하락/횡보)
2. 주요 기술적 지표 분석
3. 뉴스 센티먼트 (긍정/부정/중립)
4. 투자 추천 (매수/매도/보유)
5. 신뢰도 (0-1)
system_prompt = """주식 시장 분석 전문가. JSON으로 응답.
필수 필드: trend(UP/DOWN/SIDEWAYS), sentiment(POSITIVE/NEGATIVE/NEUTRAL), recommendation(BUY/SELL/HOLD), confidence(0-1), analysis(간략설명)."""
JSON 형식으로 응답하세요."""
news_limit = settings.NEWS_LIMIT if settings.TOKEN_SAVE_MODE else 5
news_text = ""
if news:
news_text = "\n\n최근 뉴스:\n" + "\n".join([
f"- {item.get('title', '')}: {item.get('content', '')[:200]}"
for item in news[:5]
news_text = "\n뉴스:" + ",".join([
item.get('title', '') for item in news[:news_limit]
])
user_message = f"""
종목 코드: {stock_code}
시장 데이터:
{json.dumps(market_data, ensure_ascii=False, indent=2)}
{news_text}
이 종목에 대한 종합적인 시장 분석을 제공해주세요.
"""
user_message = f"종목:{stock_code}\n시장:{_dumps(market_data)}{news_text}"
response = await self.analyze(system_prompt, user_message)
@@ -179,43 +156,10 @@ JSON 형식으로 응답하세요."""
financial_data: Dict[str, Any] = None
) -> Dict[str, Any]:
"""AI 기반 적정 매수/매도 가격 산출"""
system_prompt = """당신은 주식 가치 평가 및 가격 전략 전문가입니다.
기술적 분석과 시장 데이터를 기반으로 다음을 제공하세요:
1. 적정 매수 가격 (공격적, 중립적, 보수적)
2. 적정 매도 가격 (목표가 1, 2, 3)
3. 손절가
4. 근거 및 전략
5. 신뢰도 (0-1)
system_prompt = """주식 가격 전략 전문가. JSON으로 응답.
필수 필드: buy_prices(aggressive,neutral,conservative), sell_prices(target1,target2,target3), stop_loss, strategy, confidence(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)}
이 종목의 적정 매수/매도 가격과 전략을 제시해주세요.
"""
user_message = f"종목:{stock_code} 현재가:{market_data.get('current_price')} 고가:{market_data.get('high_price')} 저가:{market_data.get('low_price')} 거래량:{market_data.get('volume')} 지표:{_dumps(market_data.get('indicators', {}))}"
response = await self.analyze(system_prompt, user_message)
@@ -243,25 +187,10 @@ JSON 형식으로 응답하세요. 예시:
trade_plan: Dict[str, Any]
) -> Dict[str, Any]:
"""리스크 평가"""
system_prompt = """당신은 투자 리스크 관리 전문가입니다.
현재 포트폴리오와 계획된 거래를 분석하여 다음을 제공하세요:
1. 리스크 레벨 (LOW/MEDIUM/HIGH/CRITICAL)
2. 주요 리스크 요인
3. 포트폴리오 다양성 평가
4. 추천 사항
5. 승인 여부 (APPROVED/REJECTED)
system_prompt = """투자 리스크 관리 전문가. JSON으로 응답.
필수 필드: risk_level(LOW/MEDIUM/HIGH/CRITICAL), risk_factors, approval(APPROVED/REJECTED), recommendation."""
JSON 형식으로 응답하세요."""
user_message = f"""
현재 포트폴리오:
{json.dumps(portfolio, ensure_ascii=False, indent=2)}
계획된 거래:
{json.dumps(trade_plan, ensure_ascii=False, indent=2)}
이 거래의 리스크를 평가하고 실행 여부를 결정해주세요.
"""
user_message = f"포트폴리오:{_dumps(portfolio)}\n거래계획:{_dumps(trade_plan)}"
response = await self.analyze(system_prompt, user_message)
@@ -277,11 +206,66 @@ JSON 형식으로 응답하세요."""
"analysis": content["text"]
}
return {
"risk_level": "UNKNOWN",
"approval": "REJECTED",
"confidence": 0.0
}
return {"error": "Failed to assess risk"}
async def optimize_trading_rules(
self,
stock_code: str,
market_data: Dict[str, Any],
news_data: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""시장 상황에 맞는 트레이딩 규칙 최적화"""
system_prompt = """알고리즘 트레이딩 전략가. JSON으로만 응답.
필수 필드: strategy_name, market_condition, parameters(buy_conditions,sell_conditions(take_profit_rate,stop_loss_rate,trailing_stop)), risk_assessment, confidence_score(0-100).
악재시 보수적, 호재시 적극적 설정."""
user_message = f"종목:{stock_code}\n시장:{_dumps(market_data)}"
if news_data:
user_message += f"\n뉴스:{_dumps(news_data)}"
response = await self.analyze(system_prompt, user_message)
# 텍스트 응답 파싱
for content in response["content"]:
if content["type"] == "text":
try:
text = content["text"]
json_start = text.find('{')
json_end = text.rfind('}') + 1
if json_start != -1 and json_end != -1:
return json.loads(text[json_start:json_end])
except Exception as e:
logger.error(f"Failed to parse optimization result: {e}")
return {"error": "Failed to generate optimized rules"}
async def generate_trade_journal(
self,
trade_info: Dict[str, Any],
market_data: Dict[str, Any]
) -> Dict[str, Any]:
"""매매일지 자동 작성"""
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)
for content in response["content"]:
if content["type"] == "text":
try:
text = content["text"]
json_start = text.find('{')
json_end = text.rfind('}') + 1
if json_start != -1 and json_end != -1:
return json.loads(text[json_start:json_end])
except Exception as e:
logger.error(f"Failed to parse journal result: {e}")
return {"error": "Failed to generate journal"}
claude_client = ClaudeClient()

View File

@@ -7,31 +7,43 @@ 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
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
# AI Provider Selection (claude or gemini)
AI_PROVIDER = os.getenv("AI_PROVIDER", "gemini")
# 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"))
# Gemini Settings
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.0-flash")
# Common AI Settings (AI_PROVIDER에 따라 자동 선택)
MAX_TOKENS = int(os.getenv(
"GEMINI_MAX_TOKENS" if AI_PROVIDER == "gemini" else "CLAUDE_MAX_TOKENS",
"1024"
))
TEMPERATURE = float(os.getenv(
"GEMINI_TEMPERATURE" if AI_PROVIDER == "gemini" else "CLAUDE_TEMPERATURE",
"0.3"
))
# Token Optimization Settings
TOKEN_SAVE_MODE = os.getenv("TOKEN_SAVE_MODE", "false").lower() == "true"
NEWS_LIMIT = int(os.getenv("NEWS_LIMIT", "5"))
CHART_DAYS = int(os.getenv("CHART_DAYS", "100"))
JSON_COMPACT = os.getenv("JSON_COMPACT", "false").lower() == "true"
settings = Settings()

188
mcp-server/gemini_client.py Normal file
View File

@@ -0,0 +1,188 @@
from google import genai
from google.genai import types
from config import settings
from utils import _dumps
from typing import Dict, Any, List
import json
import logging
logger = logging.getLogger(__name__)
class GeminiClient:
"""Gemini AI 클라이언트 (ClaudeClient와 동일한 인터페이스)"""
def __init__(self):
self.client = genai.Client(api_key=settings.GEMINI_API_KEY)
self.model_name = settings.GEMINI_MODEL
self.generate_config = types.GenerateContentConfig(
max_output_tokens=settings.MAX_TOKENS,
temperature=settings.TEMPERATURE,
)
async def analyze(
self,
system_prompt: str,
user_message: str,
tools: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Gemini를 통한 분석 실행"""
try:
response = await self.client.aio.models.generate_content(
model=self.model_name,
contents=f"{system_prompt}\n\n{user_message}",
config=self.generate_config,
)
text = response.text if response.text else ""
return {
"content": [{"type": "text", "text": text}],
"stop_reason": "end_turn",
"usage": {
"input_tokens": response.usage_metadata.prompt_token_count if response.usage_metadata else 0,
"output_tokens": response.usage_metadata.candidates_token_count if response.usage_metadata else 0,
}
}
except Exception as e:
logger.error(f"Gemini analysis error: {e}")
raise
def _parse_json_response(self, response: Dict[str, Any], fallback: Dict[str, Any]) -> Dict[str, Any]:
"""응답 텍스트에서 JSON 파싱 (공통 헬퍼)"""
for content in response["content"]:
if content["type"] == "text":
try:
text = content["text"]
json_start = text.find('{')
json_end = text.rfind('}') + 1
if json_start != -1 and json_end > json_start:
return json.loads(text[json_start:json_end])
except (json.JSONDecodeError, Exception):
return {**fallback, "analysis": content["text"]}
return fallback
async def validate_trading_rule(
self,
rule: Dict[str, Any],
market_data: Dict[str, Any],
news_data: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""트레이딩 규칙 검증"""
system_prompt = """주식 트레이딩 규칙 검증 전문가. 규칙/시장/뉴스를 분석하여 JSON으로만 응답.
필수 필드: decision(BUY/SELL/HOLD), confidence(0-1), risk_level, recommendation.
악재(유상증자,감자,소송,실적악화) 시 confidence 낮추고 경고 포함. JSON 외 텍스트 금지."""
user_message = f"규칙:{_dumps(rule)}\n시장:{_dumps(market_data)}"
if news_data:
user_message += f"\n뉴스:{_dumps(news_data)}"
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"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 = """주식 시장 분석 전문가. JSON으로만 응답.
필수 필드: trend(UP/DOWN/SIDEWAYS), sentiment(POSITIVE/NEGATIVE/NEUTRAL), recommendation(BUY/SELL/HOLD), confidence(0-1), analysis(간략설명). JSON 외 텍스트 금지."""
news_limit = settings.NEWS_LIMIT if settings.TOKEN_SAVE_MODE else 5
news_text = ""
if news:
news_text = "\n뉴스:" + ",".join([item.get('title', '') for item in news[:news_limit]])
user_message = f"종목:{stock_code}\n시장:{_dumps(market_data)}{news_text}"
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"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 = """주식 가격 전략 전문가. JSON으로만 응답.
필수 필드: buy_prices(aggressive,neutral,conservative), sell_prices(target1,target2,target3), stop_loss, strategy, confidence(0-1). JSON 외 텍스트 금지."""
user_message = (
f"종목:{stock_code} 현재가:{market_data.get('current_price')} "
f"고가:{market_data.get('high_price')} 저가:{market_data.get('low_price')} "
f"거래량:{market_data.get('volume')} 지표:{_dumps(market_data.get('indicators', {}))}"
)
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"error": "분석 실패", "confidence": 0.0}
)
async def assess_risk(
self,
portfolio: Dict[str, Any],
trade_plan: Dict[str, Any]
) -> Dict[str, Any]:
"""리스크 평가"""
system_prompt = """투자 리스크 관리 전문가. JSON으로만 응답.
필수 필드: risk_level(LOW/MEDIUM/HIGH/CRITICAL), risk_factors, approval(APPROVED/REJECTED), recommendation. JSON 외 텍스트 금지."""
user_message = f"포트폴리오:{_dumps(portfolio)}\n거래계획:{_dumps(trade_plan)}"
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"error": "Failed to assess risk"}
)
async def optimize_trading_rules(
self,
stock_code: str,
market_data: Dict[str, Any],
news_data: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""시장 상황에 맞는 트레이딩 규칙 최적화"""
system_prompt = """알고리즘 트레이딩 전략가. JSON으로만 응답.
필수 필드: strategy_name, market_condition, parameters(buy_conditions,sell_conditions(take_profit_rate,stop_loss_rate,trailing_stop)), risk_assessment, confidence_score(0-100).
악재시 보수적, 호재시 적극적 설정. JSON 외 텍스트 금지."""
user_message = f"종목:{stock_code}\n시장:{_dumps(market_data)}"
if news_data:
user_message += f"\n뉴스:{_dumps(news_data)}"
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"error": "Failed to generate optimized rules"}
)
async def generate_trade_journal(
self,
trade_info: Dict[str, Any],
market_data: Dict[str, Any]
) -> Dict[str, Any]:
"""매매일지 자동 작성"""
system_prompt = """매매일지 작성 전문가. JSON으로만 응답.
필수 필드: title, content(간략 Markdown), mood(평정심/흥분/공포/아쉬움/만족), tags(배열), score(0-100).
매매근거, 잘한점/아쉬운점, 개선사항 포함. JSON 외 텍스트 금지."""
user_message = f"거래:{_dumps(trade_info)}\n시장:{_dumps(market_data)}"
response = await self.analyze(system_prompt, user_message)
return self._parse_json_response(
response,
{"error": "Failed to generate journal"}
)
gemini_client = GeminiClient()

View File

@@ -6,11 +6,14 @@ 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
# AI_PROVIDER에 따라 클라이언트 동적 선택
if settings.AI_PROVIDER == "gemini":
from gemini_client import gemini_client
ai_client = gemini_client
else:
ai_client = claude_client
# 로깅 설정
logging.basicConfig(
@@ -61,7 +64,7 @@ async def health_check():
"status": "healthy",
"ai_provider": settings.AI_PROVIDER,
"claude_configured": bool(settings.ANTHROPIC_API_KEY),
"perplexity_configured": bool(settings.PERPLEXITY_API_KEY)
"gemini_configured": bool(settings.GEMINI_API_KEY),
}
@@ -102,7 +105,18 @@ async def analyze(request: AnalysisRequest):
detail="rule and market_data are required"
)
result = await ai_client.validate_trading_rule(rule, market_data)
# 뉴스 정보 추가 수집
stock_code = market_data.get("stock_code")
news_data = []
if stock_code:
try:
news_result = await mcp_tools.get_news(stock_code, limit=settings.NEWS_LIMIT)
if "news" in news_result:
news_data = news_result["news"]
except Exception as e:
logger.warning(f"Failed to fetch news for validation: {e}")
result = await ai_client.validate_trading_rule(rule, market_data, news_data)
return {
"analysis_type": analysis_type,
"result": result
@@ -162,6 +176,49 @@ async def analyze(request: AnalysisRequest):
"analysis_type": analysis_type,
"result": result
}
elif analysis_type == "RULE_OPTIMIZATION":
# 트레이딩 규칙 최적화/생성
stock_code = data.get("stock_code")
market_data = data.get("market_data")
if not stock_code or not market_data:
raise HTTPException(
status_code=400,
detail="stock_code and market_data are required"
)
# 뉴스 정보 추가 수집
news_data = []
try:
news_result = await mcp_tools.get_news(stock_code, limit=settings.NEWS_LIMIT)
if "news" in news_result:
news_data = news_result["news"]
except Exception as e:
logger.warning(f"Failed to fetch news for optimization: {e}")
result = await ai_client.optimize_trading_rules(stock_code, market_data, news_data)
return {
"analysis_type": analysis_type,
"result": result
}
elif analysis_type == "GENERATE_JOURNAL":
# 매매일지 생성
trade_info = data.get("trade_info")
market_data = data.get("market_data")
if not trade_info or not market_data:
raise HTTPException(
status_code=400,
detail="trade_info and market_data are required"
)
result = await ai_client.generate_trade_journal(trade_info, market_data)
return {
"analysis_type": analysis_type,
"result": result
}
else:
raise HTTPException(

View File

@@ -1,20 +1,29 @@
from typing import Dict, Any, Callable
import logging
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
import httpx
from bs4 import BeautifulSoup
from config import settings
# Optional: KIS API dependency (not currently available in MCP server directly)
korea_investment_api = None
logger = logging.getLogger(__name__)
class MCPTools:
"""MCP Tools Registry"""
def __init__(self):
self.tools: Dict[str, Callable] = {}
self._register_tools()
# Initialize KIS API if available
self.korea_investment_api = None
def _register_tools(self):
"""도구 등록"""
@@ -155,17 +164,76 @@ class MCPTools:
}
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": []
}
"""시장 데이터 조회 (국내/해외 통합)"""
if not korea_investment_api:
return {"error": "Korea Investment API not available"}
try:
# 국내 주식 여부 판단 (6자리 숫자)
is_domestic = stock_code.isdigit() and len(stock_code) == 6
if is_domestic:
# 현재가 조회
current_price_data = await korea_investment_api.get_current_price(stock_code)
# 차트 데이터 조회
chart_data = await korea_investment_api.get_daily_chart(stock_code, period, limit=settings.CHART_DAYS)
output2 = chart_data.get('output2', [])
formatted_data = []
for item in output2:
formatted_data.append({
"date": item.get('stck_bsop_date'),
"open": int(item.get('stck_oprc')) if item.get('stck_oprc') else 0,
"high": int(item.get('stck_hgpr')) if item.get('stck_hgpr') else 0,
"low": int(item.get('stck_lwpr')) if item.get('stck_lwpr') else 0,
"close": int(item.get('stck_clpr')) if item.get('stck_clpr') else 0,
"volume": int(item.get('acml_vol')) if item.get('acml_vol') else 0
})
current_output = current_price_data.get('output', {})
return {
"stock_code": stock_code,
"period": period,
"current_price": int(current_output.get('stck_prpr', 0)) if current_output else 0,
"change_rate": float(current_output.get('prdy_ctrt', 0)) if current_output else 0,
"volume": int(current_output.get('acml_vol', 0)) if current_output else 0,
"data": formatted_data
}
else:
# 해외 주식 logic (기본 NASD 가정)
exchange = "NASD"
# TODO: 거래소 판별 로직 고도화 필요 (현재는 NASD 고정)
current_price_data = await korea_investment_api.get_overseas_price(stock_code, exchange)
chart_data = await korea_investment_api.get_overseas_daily_chart(stock_code, exchange, period)
output2 = chart_data.get('output2', [])
formatted_data = []
for item in output2:
# 해외주식 데이터 키 매핑 (Key names depend on actual API response, adjusted to common KIS overseas response)
formatted_data.append({
"date": item.get('xymd'), # 일자
"open": float(item.get('open')) if item.get('open') else 0.0,
"high": float(item.get('high')) if item.get('high') else 0.0,
"low": float(item.get('low')) if item.get('low') else 0.0,
"close": float(item.get('clos')) if item.get('clos') else 0.0,
"volume": float(item.get('tvol')) if item.get('tvol') else 0.0
})
curr = current_price_data.get('output', {})
return {
"stock_code": stock_code,
"period": period,
"current_price": float(curr.get('last', 0)) if curr else 0.0,
"change_rate": float(curr.get('rate', 0)) if curr else 0.0,
"volume": float(curr.get('tvol', 0)) if curr else 0.0,
"data": formatted_data
}
except Exception as e:
logger.error(f"Failed to get market data for {stock_code}: {e}")
raise
async def calculate_indicators(
self,
@@ -230,18 +298,41 @@ class MCPTools:
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
}
]
}
if not korea_investment_api:
return {"error": "Korea Investment API not available"}
try:
balance = await korea_investment_api.get_balance()
output1 = balance.get('output1', [])
output2 = balance.get('output2', []) # Total asset summary
holdings = []
for item in output1:
# Filter if stock_code provided
if stock_code and item.get('pdno') != stock_code:
continue
holdings.append({
"stock_code": item.get('pdno'),
"stock_name": item.get('prdt_name'),
"quantity": int(item.get('hldg_qty')),
"average_price": float(item.get('pchs_avg_pric')),
"current_price": float(item.get('prpr')),
"profit_loss": float(item.get('evlu_pfls_amt')),
"profit_rate": float(item.get('evlu_pfls_rt'))
})
total_eval = 0
if output2:
total_eval = float(output2[0].get('tot_evlu_mony', 0))
return {
"total_value": total_eval,
"holdings": holdings
}
except Exception as e:
logger.error(f"Failed to get portfolio: {e}")
raise
async def validate_rules(
self,
@@ -274,22 +365,88 @@ class MCPTools:
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
}
if not korea_investment_api:
return {"error": "Korea Investment API not available"}
try:
# 국내/해외 구분
is_domestic = stock_code.isdigit() and len(stock_code) == 6
if is_domestic:
result = await korea_investment_api.place_order(
stock_code=stock_code,
order_type=order_type,
quantity=quantity,
price=int(price) if price else None
)
else:
# 해외 주문 (지정가 필수)
if price is None:
return {"error": "Overseas orders require a limit price."}
result = await korea_investment_api.place_overseas_order(
symbol=stock_code,
order_type=order_type,
quantity=quantity,
price=price,
exchange_code="NASD" # 기본값
)
return result
except Exception as e:
logger.error(f"Failed to place order: {e}")
raise
async def get_news(self, stock_code: str = None, limit: int = 10) -> Dict[str, Any]:
"""뉴스 조회"""
# 실제로는 뉴스 API 호출
return {
"stock_code": stock_code,
"news": []
}
async def get_news(self, stock_code: str = None, limit: int = 5) -> Dict[str, Any]:
"""네이버 금융 뉴스 크롤링"""
if not stock_code:
return {"error": "stock_code is required"}
try:
url = f"https://finance.naver.com/item/news_news.naver?code={stock_code}&page=1"
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=headers)
if response.status_code != 200:
return {"error": f"Failed to fetch news: {response.status_code}"}
soup = BeautifulSoup(response.text, 'html.parser')
news_items = []
# 네이버 금융 뉴스 구조에 맞춰 파싱
for row in soup.select('table.type5 tbody tr'):
title_tag = row.select_one('.title a')
if not title_tag:
continue
title = title_tag.get_text(strip=True)
link = "https://finance.naver.com" + title_tag['href']
date_tag = row.select_one('.date')
date = date_tag.get_text(strip=True) if date_tag else ""
source_tag = row.select_one('.info')
source = source_tag.get_text(strip=True) if source_tag else ""
# 관련뉴스/공시 제외하고 싶으면 필터링 가능
item = {"title": title, "date": date, "source": source}
if not settings.TOKEN_SAVE_MODE:
item["link"] = link
news_items.append(item)
if len(news_items) >= limit:
break
return {
"stock_code": stock_code,
"news": news_items
}
except Exception as e:
logger.error(f"Error fetching news for {stock_code}: {e}")
return {"error": str(e), "news": []}
mcp_tools = MCPTools()

View File

@@ -1,333 +0,0 @@
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()

View File

@@ -1,6 +1,7 @@
fastapi>=0.104.1
uvicorn[standard]>=0.24.0
anthropic>=0.18.0
google-genai>=1.0.0
pydantic>=2.5.0
python-dotenv==1.0.0
httpx==0.25.2
@@ -10,3 +11,4 @@ ta==0.11.0
sqlalchemy==2.0.23
asyncpg>=0.30.0
redis==5.0.1
beautifulsoup4>=4.12.0

178
mcp-server/stdio_server.py Executable file
View File

@@ -0,0 +1,178 @@
#!/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"
@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) 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())

169
mcp-server/trade_monitor.py Normal file
View File

@@ -0,0 +1,169 @@
import asyncio
import json
import os
from datetime import datetime
import logging
from decimal import Decimal
# backend 경로를 sys.path에 추가하여 서비스 모듈 임포트
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../backend')))
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
from app.models.models import ActiveTrade
from app.services.korea_investment_api import korea_investment_api
# 로깅 설정
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# --- DB 설정 ---
engine = create_async_engine(settings.DATABASE_URL, echo=False)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
# ----------------
ACTIVE_TRADES_FILE = os.path.join(os.path.dirname(__file__), 'active_trades.json')
MONITORING_INTERVAL_SECONDS = 60 # 60초마다 확인
async def sync_json_from_db(db: AsyncSession):
"""DB의 active_trades 목록을 JSON 파일에 동기화(덮어쓰기)합니다."""
try:
result = await db.execute(select(ActiveTrade).where(ActiveTrade.status == 'active'))
active_trades_from_db = result.scalars().all()
json_trades = [
{
'stock_code': trade.stock_code,
'stock_name': trade.stock_name,
'quantity': trade.quantity,
'entry_price': float(trade.entry_price),
'stop_loss': float(trade.stop_loss),
'take_profit': float(trade.take_profit),
'order_id': trade.order_id,
'reason': trade.entry_reason,
'timestamp': trade.created_at.isoformat()
}
for trade in active_trades_from_db
]
with open(ACTIVE_TRADES_FILE, 'w', encoding='utf-8') as f:
json.dump(json_trades, f, indent=4, ensure_ascii=False)
logging.info(f"💾 [JSON] DB와 동기화 완료. 활성 거래 {len(json_trades)}건.")
except Exception as e:
logging.error(f"❌ [JSON] DB 동기화 실패: {e}")
async def check_and_execute_trades(db: AsyncSession):
"""
DB에서 활성화된 거래를 확인하고 손절/익절 조건에 따라 매도 주문을 실행합니다.
"""
logging.info(" 거래 모니터링 시작 (DB 기반)...")
result = await db.execute(select(ActiveTrade).where(ActiveTrade.status == 'active'))
active_trades = result.scalars().all()
if not active_trades:
logging.info("현재 활성화된 거래가 없습니다.")
return
for trade in active_trades:
stock_code = trade.stock_code
stop_loss = float(trade.stop_loss)
take_profit = float(trade.take_profit)
quantity = trade.quantity
entry_price = float(trade.entry_price)
should_sell = False
sell_reason = ""
try:
# 현재가 조회
price_info, _ = await korea_investment_api._make_request(
"GET",
"/uapi/domestic-stock/v1/quotations/inquire-price",
"FHKST01010100",
params={"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": stock_code}
)
if price_info and price_info.get('rt_cd') == '0':
current_price_str = price_info.get('output', {}).get('stck_prpr')
if not current_price_str:
logging.error(f"[{stock_code}] 현재가 정보를 응답에서 찾을 수 없습니다: {price_info}")
continue
current_price = int(current_price_str)
logging.info(f"[{stock_code}] 현재가: {current_price} (매수가: {entry_price}, 손절가: {stop_loss}, 목표가: {take_profit})")
# 손절 조건 확인
if stop_loss and current_price <= stop_loss:
should_sell = True
sell_reason = f"손절가 도달 (현재가: {current_price} <= 손절가: {stop_loss})"
# 익절 조건 확인
elif take_profit and current_price >= take_profit:
should_sell = True
sell_reason = f"목표가 도달 (현재가: {current_price} >= 목표가: {take_profit})"
else:
logging.error(f"[{stock_code}] 현재가 조회 실패: {price_info.get('msg1', '알 수 없는 오류')}")
continue
except Exception as e:
logging.error(f"[{stock_code}] 처리 중 예외 발생: {e}")
continue
if should_sell:
logging.warning(f"🚨 [{stock_code}] 매도 조건 충족: {sell_reason}")
try:
# 시장가 매도 주문 (price=None)
sell_result = await korea_investment_api.place_order(
stock_code=stock_code,
order_type='SELL',
quantity=quantity,
price=None
)
logging.info(f"[{stock_code}] 매도 주문 결과: {sell_result}")
if sell_result and sell_result.get('rt_cd') == '0':
logging.info(f"✅ [{stock_code}] 매도 주문 성공. DB 상태를 'closed'로 변경합니다.")
trade.status = 'closed'
trade.updated_at = datetime.utcnow()
db.add(trade)
await db.commit()
else:
logging.error(f"❌ [{stock_code}] 매도 주문 실패: {sell_result.get('msg1', '알 수 없는 오류')}")
except Exception as e:
logging.error(f"[{stock_code}] 매도 주문 중 예외 발생: {e}")
await db.rollback()
async def main():
"""메인 실행 루프"""
logging.info("🚀 자동 매도 모니터링 시스템을 시작합니다. (DB 모드)")
logging.info(f"점검 주기: {MONITORING_INTERVAL_SECONDS}초")
# 시작 시 DB와 JSON 동기화
async with AsyncSessionLocal() as db:
await sync_json_from_db(db)
while True:
try:
async with AsyncSessionLocal() as db:
await check_and_execute_trades(db)
# 매 사이클 종료 후 DB와 JSON 동기화
await sync_json_from_db(db)
except Exception as e:
logging.critical(f"모니터링 루프에서 치명적 오류 발생: {e}", exc_info=True)
logging.info(f"{MONITORING_INTERVAL_SECONDS}초 후 다음 점검을 시작합니다.\n")
await asyncio.sleep(MONITORING_INTERVAL_SECONDS)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logging.info("사용자에 의해 프로그램이 종료되었습니다.")

10
mcp-server/utils.py Normal file
View File

@@ -0,0 +1,10 @@
"""공용 유틸리티 (MCP 서버)"""
import json
from config import settings
def _dumps(obj) -> str:
"""토큰 절약 모드에 따라 compact/pretty JSON 직렬화"""
if settings.JSON_COMPACT or settings.TOKEN_SAVE_MODE:
return json.dumps(obj, ensure_ascii=False, separators=(',', ':'))
return json.dumps(obj, ensure_ascii=False, indent=2)