수정본

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

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