수정본
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user