Skip to main content
Glama
technical_analysis.py16.1 kB
from fastmcp import Context import httpx import numpy as np from typing import List, Literal, Dict, Any, Optional, Set from config import API_BASE # 사용 가능한 지표 및 신호 키 정의 # AVAILABLE_INDICATOR_KEYS: Set[str] = { # "sma", "rsi", "bollinger_bands", "macd", "stochastic", "volume", "pivots", "current_price" # } # AVAILABLE_SIGNAL_KEYS: Set[str] = { # "ma_signal", "rsi_signal", "bb_signal", "macd_signal", "stoch_signal", "overall_signal" # } # ALL_KEYS: Set[str] = AVAILABLE_INDICATOR_KEYS.union(AVAILABLE_SIGNAL_KEYS) # overall_signal이 의존하는 개별 신호 키들 # OVERALL_SIGNAL_DEPENDENCIES: Set[str] = { # "ma_signal", "rsi_signal", "bb_signal", "macd_signal", "stoch_signal" # } async def technical_analysis( market: str, interval: Literal["minute1", "minute3", "minute5", "minute10", "minute15", "minute30", "minute60", "minute240", "day", "week", "month"] = "day", count: int = 200, ctx: Optional[Context] = None ) -> Dict[str, Any]: """ 지정된 마켓의 캔들 데이터를 기반으로 다양한 기술적 지표와 매매 신호를 계산합니다. Upbit API를 통해 캔들 데이터를 조회한 후, `numpy`를 사용하여 이동평균(SMA), 상대강도지수(RSI), 볼린저 밴드, MACD, 거래량 등 주요 기술적 지표를 계산합니다. 이러한 지표들을 종합하여 간단한 매매 신호("strong_buy", "buy", "sell", "strong_sell", "neutral")를 제공합니다. Args: market (str): 분석할 마켓 코드 (예: "KRW-BTC"). interval (Literal[...]): 캔들의 시간 간격. "minute1"~"minute240", "day", "week", "month" 중 선택. 기본값은 "day"입니다. count (int): 분석에 사용할 최신 캔들의 개수. 기본값 및 최대값은 200입니다. ctx (Context, optional): FastMCP 컨텍스트 객체. 함수 실행 중 정보나 오류를 로깅하는 데 사용됩니다. Returns: Dict[str, Any]: - 성공 시: 기술적 분석 결과를 담은 딕셔너리. 주요 키는 다음과 같습니다: - `market` (str): 분석한 마켓 코드 - `interval` (str): 사용된 캔들 인터벌 - `indicators` (Dict): 계산된 기술적 지표 값 - `current_price` (float): 현재가 - `sma` (Dict): 단순 이동 평균 (e.g., `sma_20`, `sma_50`) - `rsi` (float): 상대강도지수 - `bollinger_bands` (Dict): 볼린저 밴드 (`upper`, `middle`, `lower`) - `macd` (Dict): MACD (`line`, `signal`, `histogram`) - `volume_analysis` (Dict): 거래량 분석 (`current`, `sma_20`, `ratio`) - `signals` (Dict): 각 지표에 대한 신호 - `ma_signal` ('bullish'|'bearish'|'neutral') - `rsi_signal` ('overbought'|'oversold'|'neutral') - `bb_signal` ('overbought'|'oversold'|'neutral') - `macd_signal` ('bullish'|'bearish'|'neutral') - `overall_signal` (str): 모든 신호를 종합한 최종 매매 신호. - 실패 시: `{"error": "오류 메시지"}` 형식의 딕셔너리. Example: >>> btc_analysis = await technical_analysis(market="KRW-BTC", interval="day") >>> if "error" not in btc_analysis: ... print(f"BTC 일봉 분석 종합 신호: {btc_analysis['overall_signal']}") ... print(f"현재 RSI: {btc_analysis['indicators']['rsi']:.2f}") ... else: ... print(f"오류: {btc_analysis['error']}") """ if ctx: ctx.info(f"기술적 분석 시작: {market} {interval}") # 캔들스틱 데이터 가져오기 # interval에 따라 API 엔드포인트 포맷 조정 if interval in ["day", "week", "month"]: url_interval = f"{interval}s" elif interval.startswith("minute"): # 분봉의 경우 interval에 unit이 포함되어 있으므로 그대로 사용 # 예: minute1, minute3, ... minute240 url_interval = interval else: # 혹시 모를 다른 interval 값에 대한 기본 처리 (현재 정의된 Literal 타입에는 해당되지 않음) url_interval = interval url = f"{API_BASE}/candles/{url_interval}" params = { 'market': market, 'count': str(count) } try: async with httpx.AsyncClient() as client: res = await client.get(url, params=params) if res.status_code != 200: error_msg = f"업비트 API 오류: {res.status_code} - {res.text}" if ctx: ctx.error(error_msg) return {"error": error_msg} candles = res.json() if not candles: error_msg = "데이터를 가져오는데 실패했습니다. API 응답이 비어있습니다." if ctx: ctx.error(error_msg) return {"error": error_msg} except httpx.RequestError as e: error_msg = f"API 호출 중 httpx.RequestError 발생: {str(e)}" if ctx: ctx.error(error_msg) return {"error": error_msg} except Exception as e: error_msg = f"API 호출 중 알 수 없는 오류 발생: {str(e)}, Type: {type(e).__name__}" if ctx: ctx.error(error_msg) return {"error": error_msg} # 가격 데이터 추출 try: closes = np.array([float(candle["trade_price"]) for candle in candles]) highs = np.array([float(candle["high_price"]) for candle in candles]) lows = np.array([float(candle["low_price"]) for candle in candles]) volumes = np.array([float(candle["candle_acc_trade_volume"]) for candle in candles]) except (KeyError, ValueError) as e: error_msg = f"데이터 처리 중 오류 발생 (KeyError or ValueError): {str(e)}" if ctx: ctx.error(error_msg) return {"error": error_msg} except Exception as e: error_msg = f"데이터 처리 중 알 수 없는 오류 발생: {str(e)}, Type: {type(e).__name__}" if ctx: ctx.error(error_msg) return {"error": error_msg} # 기술적 지표 계산 try: # 1. 이동평균선 (SMA) sma_20 = np.mean(closes[-20:]) sma_50 = np.mean(closes[-50:]) sma_200 = np.mean(closes[-200:]) if len(closes) >= 200 else None # 2. RSI (14일) def calculate_rsi(prices, period=14): # Ensure prices has enough data if len(prices) < period + 1: # Need at least period + 1 for np.diff and initial mean return np.nan # Return NaN or some default if not enough data deltas = np.diff(prices) gains = np.where(deltas > 0, deltas, 0) losses = np.where(deltas < 0, -deltas, 0) # Ensure there are enough gains/losses for the initial average if len(gains) < period or len(losses) < period: return np.nan avg_gain = np.mean(gains[:period]) avg_loss = np.mean(losses[:period]) for i in range(period, len(deltas)): avg_gain = (avg_gain * (period - 1) + gains[i]) / period avg_loss = (avg_loss * (period - 1) + losses[i]) / period if avg_loss == 0: return 100.0 rs = avg_gain / avg_loss rsi_val = 100.0 - (100.0 / (1.0 + rs)) return rsi_val rsi = calculate_rsi(closes) # 3. 볼린저 밴드 (20일, 2 표준편차) def calculate_bollinger_bands(prices, period=20, num_std=2): if len(prices) < period: return np.nan, np.nan, np.nan sma = np.mean(prices[-period:]) std = np.std(prices[-period:]) upper_band = sma + (std * num_std) lower_band = sma - (std * num_std) return upper_band, sma, lower_band bb_upper, bb_middle, bb_lower = calculate_bollinger_bands(closes) # 4. MACD (12, 26, 9) def calculate_macd(prices, fast_period=12, slow_period=26, signal_period=9): if len(prices) < slow_period: return np.nan, np.nan, np.nan # Calculate Fast EMA ema_fast = np.full_like(prices, np.nan) ema_fast[fast_period-1] = np.mean(prices[:fast_period]) for i in range(fast_period, len(prices)): ema_fast[i] = prices[i] * (2 / (fast_period + 1)) + ema_fast[i-1] * (1 - (2 / (fast_period + 1))) # Calculate Slow EMA ema_slow = np.full_like(prices, np.nan) ema_slow[slow_period-1] = np.mean(prices[:slow_period]) for i in range(slow_period, len(prices)): ema_slow[i] = prices[i] * (2 / (slow_period + 1)) + ema_slow[i-1] * (1 - (2 / (slow_period + 1))) macd_line = ema_fast - ema_slow # Calculate Signal Line (EMA of MACD line) signal_line_val = np.full_like(macd_line, np.nan) # Ensure there are enough non-NaN MACD values to start signal calculation valid_macd_start_index = -1 for i in range(len(macd_line)): if not np.isnan(macd_line[i]): valid_macd_start_index = i break if valid_macd_start_index != -1 and len(macd_line) - valid_macd_start_index >= signal_period: signal_line_val[valid_macd_start_index + signal_period - 1] = np.mean(macd_line[valid_macd_start_index : valid_macd_start_index + signal_period]) for i in range(valid_macd_start_index + signal_period, len(macd_line)): signal_line_val[i] = macd_line[i] * (2 / (signal_period + 1)) + signal_line_val[i-1] * (1 - (2 / (signal_period + 1))) histogram_val = macd_line - signal_line_val # Return the last values return (macd_line[-1] if len(macd_line) > 0 and not np.isnan(macd_line[-1]) else np.nan, signal_line_val[-1] if len(signal_line_val) > 0 and not np.isnan(signal_line_val[-1]) else np.nan, histogram_val[-1] if len(histogram_val) > 0 and not np.isnan(histogram_val[-1]) else np.nan) macd, signal_line, histogram = calculate_macd(closes) # 5. 거래량 분석 current_volume = volumes[-1] if len(volumes) > 0 else np.nan if len(volumes) >= 20: volume_sma = np.mean(volumes[-20:]) volume_ratio = current_volume / volume_sma if volume_sma > 0 and not np.isnan(current_volume) else np.nan else: volume_sma = np.nan volume_ratio = np.nan # 신호 생성 current_price = closes[-1] if len(closes) > 0 else np.nan # 이동평균선 신호 ma_signal = "neutral" if not np.isnan(current_price) and not np.isnan(sma_20) and not np.isnan(sma_50) and sma_200 is not None and not np.isnan(sma_200): if current_price > sma_20 and sma_20 > sma_50: ma_signal = "bullish" elif current_price < sma_20 and sma_20 < sma_50: ma_signal = "bearish" # RSI 신호 rsi_signal = "neutral" if not np.isnan(rsi): if rsi > 70: rsi_signal = "overbought" elif rsi < 30: rsi_signal = "oversold" # 볼린저 밴드 신호 bb_signal = "neutral" if not np.isnan(current_price) and not np.isnan(bb_upper) and not np.isnan(bb_lower): if current_price > bb_upper: bb_signal = "overbought" elif current_price < bb_lower: bb_signal = "oversold" # MACD 신호 macd_signal = "neutral" if not np.isnan(macd) and not np.isnan(signal_line) and not np.isnan(histogram): if macd > signal_line and histogram > 0: macd_signal = "bullish" elif macd < signal_line and histogram < 0: macd_signal = "bearish" # 거래량 신호 volume_signal = "neutral" if not np.isnan(volume_ratio): if volume_ratio > 1.5: volume_signal = "high" elif volume_ratio < 0.5: volume_signal = "low" # 종합 신호 defined_signals = [sig for sig in [ma_signal, rsi_signal, bb_signal, macd_signal] if sig != "neutral"] bullish_signals = sum(1 for signal_val in defined_signals if signal_val in ["bullish", "oversold"]) bearish_signals = sum(1 for signal_val in defined_signals if signal_val in ["bearish", "overbought"]) overall_signal = "neutral" if len(defined_signals) > 0: if bullish_signals >= max(1, len(defined_signals) * 0.6): overall_signal = "strong_buy" elif bullish_signals > bearish_signals and bullish_signals >= max(1, len(defined_signals) * 0.4): overall_signal = "buy" elif bearish_signals >= max(1, len(defined_signals) * 0.6): overall_signal = "strong_sell" elif bearish_signals > bullish_signals and bearish_signals >= max(1, len(defined_signals) * 0.4): overall_signal = "sell" result = { "status": "ok", "market": market, "interval": interval, "current_price": current_price if not np.isnan(current_price) else "N/A", "indicators": { "sma": { "sma_20": sma_20 if not np.isnan(sma_20) else "N/A", "sma_50": sma_50 if not np.isnan(sma_50) else "N/A", "sma_200": sma_200 if sma_200 is not None and not np.isnan(sma_200) else "N/A" }, "rsi": rsi if not np.isnan(rsi) else "N/A", "bollinger_bands": { "upper": bb_upper if not np.isnan(bb_upper) else "N/A", "middle": bb_middle if not np.isnan(bb_middle) else "N/A", "lower": bb_lower if not np.isnan(bb_lower) else "N/A" }, "macd": { "macd": macd if not np.isnan(macd) else "N/A", "signal": signal_line if not np.isnan(signal_line) else "N/A", "histogram": histogram if not np.isnan(histogram) else "N/A" }, "volume": { "current": current_volume if not np.isnan(current_volume) else "N/A", "sma": volume_sma if not np.isnan(volume_sma) else "N/A", "ratio": volume_ratio if not np.isnan(volume_ratio) else "N/A" } }, "signals": { "ma_signal": ma_signal, "rsi_signal": rsi_signal, "bb_signal": bb_signal, "macd_signal": macd_signal, "volume_signal": volume_signal, "overall_signal": overall_signal } } if ctx: ctx.info(f"기술적 분석 완료: {market} {interval}") return result except Exception as e: error_msg = f"기술적 지표 계산 또는 신호 생성 중 알 수 없는 오류 발생: {str(e)}, Type: {type(e).__name__}" if ctx: ctx.error(error_msg) return {"error": error_msg} def main_test(): class MockContext: def info(self, msg): print(f"INFO: {msg}") def error(self, msg): print(f"ERROR: {msg}") def warning(self, msg): print(f"WARNING: {msg}") ctx = MockContext() print("SYNC_DEBUG: main_test는 서버를 통해 테스트해야 합니다.") if __name__ == '__main__': main_test()

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