Skip to main content
Glama
generate_chart_image.py12.3 kB
from fastmcp import Context import httpx import numpy as np import matplotlib.pyplot as plt import matplotlib.dates as mdates from matplotlib.patches import Rectangle from datetime import datetime, timedelta import os from typing import Literal, Optional, Dict, Any from config import API_BASE import asyncio # 한글 폰트 설정 (시스템에 따라 조정 필요) plt.rcParams['font.family'] = 'DejaVu Sans' plt.rcParams['axes.unicode_minus'] = False async def generate_chart_image( market: str, interval: Literal["minute1", "minute3", "minute5", "minute10", "minute15", "minute30", "minute60", "minute240", "day", "week", "month"] = "day", chart_type: Literal["line", "candlestick", "ohlc"] = "candlestick", count: int = 100, include_volume: bool = True, include_ma: bool = True, start_date: Optional[str] = None, end_date: Optional[str] = None, ctx: Optional[Context] = None ) -> Dict[str, Any]: """ 지정된 마켓의 캔들 데이터를 기반으로 차트 이미지를 생성합니다. Args: market (str): 차트를 생성할 마켓 코드 (예: "KRW-BTC") interval (Literal): 캔들 간격 chart_type (Literal): 차트 유형 ("line", "candlestick", "ohlc") count (int): 표시할 캔들 개수 (기본: 100, 최대: 200) include_volume (bool): 거래량 포함 여부 (기본: True) include_ma (bool): 이동평균선 포함 여부 (기본: True) start_date (str, optional): 시작 날짜 (YYYY-MM-DD 형식, 예: "2024-06-01") end_date (str, optional): 종료 날짜 (YYYY-MM-DD 형식, 예: "2024-12-31") ctx (Context, optional): FastMCP 컨텍스트 객체 Returns: Dict[str, Any]: 차트 생성 결과 - success 시: {"success": True, "file_path": "경로", "image_url": "URL", "message": "메시지"} - error 시: {"success": False, "error": "오류 메시지"} """ if ctx: ctx.info(f"차트 생성 시작: {market} {interval} {chart_type}") # 입력 파라미터 검증 if count > 200: count = 200 elif count < 10: count = 10 try: # 캔들 데이터 조회 candles_data = await fetch_candle_data(market, interval, count, start_date, end_date, ctx) if not candles_data: return {"success": False, "error": "캔들 데이터를 가져올 수 없습니다."} # 차트 생성 chart_path = await create_chart( candles_data, market, interval, chart_type, include_volume, include_ma, ctx ) if not chart_path: return {"success": False, "error": "차트 생성에 실패했습니다."} # URL 생성 (기존 Nginx 서버 사용) filename = os.path.basename(chart_path) image_url = f"https://charts.resteful3.shop/{filename}" # 차트 전용 서브도메인 URL return { "success": True, "file_path": chart_path, "image_url": image_url, "filename": filename, "message": f"{market} {interval} 차트가 성공적으로 생성되었습니다." } except Exception as e: error_msg = f"차트 생성 중 오류 발생: {str(e)}" if ctx: ctx.error(error_msg) return {"success": False, "error": error_msg} async def fetch_candle_data(market: str, interval: str, count: int, start_date: Optional[str] = None, end_date: Optional[str] = None, ctx: Optional[Context] = None) -> list: """Upbit API에서 캔들 데이터를 조회합니다.""" # interval에 따라 API 엔드포인트 조정 if interval in ["day", "week", "month"]: url_interval = f"{interval}s" elif interval.startswith("minute"): url_interval = interval else: url_interval = interval url = f"{API_BASE}/candles/{url_interval}" params = { 'market': market, 'count': str(count) } # 날짜 범위가 지정된 경우 to 파라미터 추가 if end_date: try: # end_date를 ISO 형식으로 변환 (YYYY-MM-DDTHH:mm:ss) end_datetime = datetime.strptime(end_date, "%Y-%m-%d") # 하루의 마지막 시간으로 설정 end_datetime = end_datetime.replace(hour=23, minute=59, second=59) params['to'] = end_datetime.strftime("%Y-%m-%dT%H:%M:%S") except ValueError: if ctx: ctx.error(f"잘못된 종료 날짜 형식: {end_date}. YYYY-MM-DD 형식을 사용해주세요.") return [] try: async with httpx.AsyncClient() as client: res = await client.get(url, params=params) if res.status_code != 200: if ctx: ctx.error(f"API 오류: {res.status_code} - {res.text}") return [] candles = res.json() if not candles: if ctx: ctx.error("API 응답이 비어있습니다.") return [] # 시간 순서대로 정렬 (오래된 것부터) candles.sort(key=lambda x: x['candle_date_time_kst']) # 시작 날짜 필터링 (start_date가 지정된 경우) if start_date: try: start_datetime = datetime.strptime(start_date, "%Y-%m-%d") filtered_candles = [] for candle in candles: candle_date = datetime.fromisoformat(candle['candle_date_time_kst'].replace('T', ' ')) if candle_date.date() >= start_datetime.date(): filtered_candles.append(candle) candles = filtered_candles except ValueError: if ctx: ctx.error(f"잘못된 시작 날짜 형식: {start_date}. YYYY-MM-DD 형식을 사용해주세요.") return [] return candles except Exception as e: if ctx: ctx.error(f"캔들 데이터 조회 중 오류: {str(e)}") return [] async def create_chart( candles: list, market: str, interval: str, chart_type: str, include_volume: bool, include_ma: bool, ctx: Optional[Context] = None ) -> str: """차트 이미지를 생성하고 파일로 저장합니다.""" try: # 데이터 준비 dates = [datetime.fromisoformat(candle['candle_date_time_kst'].replace('T', ' ')) for candle in candles] opens = [float(candle['opening_price']) for candle in candles] highs = [float(candle['high_price']) for candle in candles] lows = [float(candle['low_price']) for candle in candles] closes = [float(candle['trade_price']) for candle in candles] volumes = [float(candle['candle_acc_trade_volume']) for candle in candles] # 차트 설정 if include_volume: fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), gridspec_kw={'height_ratios': [3, 1]}, sharex=True) else: fig, ax1 = plt.subplots(1, 1, figsize=(12, 8)) ax2 = None # 메인 차트 그리기 if chart_type == "candlestick": draw_candlestick(ax1, dates, opens, highs, lows, closes) elif chart_type == "ohlc": draw_ohlc(ax1, dates, opens, highs, lows, closes) else: # line ax1.plot(dates, closes, linewidth=1.5, color='blue') # 이동평균선 추가 if include_ma and len(closes) >= 20: ma_20 = calculate_moving_average(closes, 20) ma_50 = calculate_moving_average(closes, 50) if len(closes) >= 50 else None ax1.plot(dates[19:], ma_20, label='MA20', color='orange', linewidth=1) if ma_50: ax1.plot(dates[49:], ma_50, label='MA50', color='red', linewidth=1) ax1.legend() # 차트 스타일링 (날짜 범위 표시) title = f"{market} {interval.upper()} Chart" if dates: start_date_str = dates[0].strftime('%Y-%m-%d') end_date_str = dates[-1].strftime('%Y-%m-%d') title += f" ({start_date_str} ~ {end_date_str})" ax1.set_title(title, fontsize=14, fontweight='bold') ax1.set_ylabel('Price', fontsize=12) ax1.grid(True, alpha=0.3) # 거래량 차트 if include_volume and ax2 is not None: colors = ['red' if closes[i] >= opens[i] else 'blue' for i in range(len(closes))] ax2.bar(dates, volumes, color=colors, alpha=0.7, width=0.8) ax2.set_ylabel('Volume', fontsize=12) ax2.grid(True, alpha=0.3) # X축 날짜 포맷 if interval.startswith('minute'): ax1.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M')) else: ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) plt.xticks(rotation=45) plt.tight_layout() # 저장 디렉토리 생성 (기존 Nginx uploads 디렉토리 사용) charts_dir = "/app/uploads/charts" # 기존 Nginx와 공유되는 디렉토리 os.makedirs(charts_dir, exist_ok=True) # 파일명 생성 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"{market.replace('-', '_')}_{interval}_{chart_type}_{timestamp}.png" file_path = os.path.join(charts_dir, filename) # 이미지 저장 plt.savefig(file_path, dpi=150, bbox_inches='tight', facecolor='white', edgecolor='none') plt.close() if ctx: ctx.info(f"차트 저장 완료: {file_path}") return file_path except Exception as e: if ctx: ctx.error(f"차트 생성 중 오류: {str(e)}") return "" def draw_candlestick(ax, dates, opens, highs, lows, closes): """캔들스틱 차트를 그립니다.""" for i in range(len(dates)): color = 'red' if closes[i] >= opens[i] else 'blue' # 몸통 (body) body_height = abs(closes[i] - opens[i]) body_bottom = min(opens[i], closes[i]) rect = Rectangle((dates[i], body_bottom), timedelta(hours=12), body_height, facecolor=color, alpha=0.8, edgecolor='black', linewidth=0.5) ax.add_patch(rect) # 그림자 (shadow) ax.plot([dates[i], dates[i]], [lows[i], highs[i]], color='black', linewidth=0.8) def draw_ohlc(ax, dates, opens, highs, lows, closes): """OHLC 바 차트를 그립니다.""" for i in range(len(dates)): color = 'red' if closes[i] >= opens[i] else 'blue' # 세로선 ax.plot([dates[i], dates[i]], [lows[i], highs[i]], color=color, linewidth=1.5) # 시가 표시 (왼쪽) ax.plot([dates[i] - timedelta(hours=6), dates[i]], [opens[i], opens[i]], color=color, linewidth=1.5) # 종가 표시 (오른쪽) ax.plot([dates[i], dates[i] + timedelta(hours=6)], [closes[i], closes[i]], color=color, linewidth=1.5) def calculate_moving_average(prices: list, period: int) -> list: """이동평균을 계산합니다.""" ma = [] for i in range(period - 1, len(prices)): avg = sum(prices[i - period + 1:i + 1]) / period ma.append(avg) return ma # 테스트 함수 async def test_chart_generation(): """차트 생성 기능을 테스트합니다.""" # 2024년 6월-12월 비트코인 차트 테스트 result = await generate_chart_image( market="KRW-BTC", interval="day", chart_type="candlestick", count=200, start_date="2024-06-01", end_date="2024-12-31" ) print(result) if __name__ == "__main__": asyncio.run(test_chart_generation())

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/restful3/upbit-mcp-sse'

If you have feedback or need assistance with the MCP directory API, please join our Discord server