__init__.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. """Technical indicators and metadata for the crypto MCP."""
  2. from collections import OrderedDict
  3. from math import sqrt
  4. from errors import InsufficientDataError, UnsupportedIndicatorError
  5. def _closes(candles: list) -> list[float]:
  6. return [float(c[4]) for c in candles]
  7. def _highs(candles: list) -> list[float]:
  8. return [float(c[2]) for c in candles]
  9. def _lows(candles: list) -> list[float]:
  10. return [float(c[3]) for c in candles]
  11. def _volumes(candles: list) -> list[float]:
  12. return [float(c[5]) for c in candles]
  13. def _ema_series(values: list[float], period: int) -> list[float]:
  14. if len(values) < period:
  15. raise InsufficientDataError(f"EMA({period}) requires at least {period} candles, got {len(values)}")
  16. k = 2 / (period + 1)
  17. ema_vals = [sum(values[:period]) / period]
  18. for v in values[period:]:
  19. ema_vals.append(v * k + ema_vals[-1] * (1 - k))
  20. return ema_vals
  21. def ema(candles: list, period: int = 20) -> float:
  22. return round(_ema_series(_closes(candles), period)[-1], 6)
  23. def rsi(candles: list, period: int = 14) -> float:
  24. closes = _closes(candles)
  25. if len(closes) < period + 1:
  26. raise InsufficientDataError(f"RSI({period}) requires at least {period + 1} candles, got {len(closes)}")
  27. deltas = [closes[i] - closes[i - 1] for i in range(1, len(closes))]
  28. gains = [max(d, 0.0) for d in deltas]
  29. losses = [abs(min(d, 0.0)) for d in deltas]
  30. avg_gain = sum(gains[:period]) / period
  31. avg_loss = sum(losses[:period]) / period
  32. for i in range(period, len(deltas)):
  33. avg_gain = (avg_gain * (period - 1) + gains[i]) / period
  34. avg_loss = (avg_loss * (period - 1) + losses[i]) / period
  35. if avg_loss == 0:
  36. return 100.0
  37. rs = avg_gain / avg_loss
  38. return round(100 - (100 / (1 + rs)), 2)
  39. def macd(candles: list, fast_period: int = 12, slow_period: int = 26, signal_period: int = 9) -> dict:
  40. closes = _closes(candles)
  41. if len(closes) < slow_period + signal_period:
  42. raise InsufficientDataError("MACD requires more candles")
  43. fast_ema = _ema_series(closes, fast_period)
  44. slow_ema = _ema_series(closes, slow_period)
  45. offset = len(fast_ema) - len(slow_ema)
  46. macd_line = [fast_ema[i + offset] - slow_ema[i] for i in range(len(slow_ema))]
  47. signal_line = _ema_series(macd_line, signal_period)
  48. macd_val = round(macd_line[-1], 6)
  49. signal_val = round(signal_line[-1], 6)
  50. return {"macd": macd_val, "signal": signal_val, "histogram": round(macd_val - signal_val, 6)}
  51. def sma(candles: list, period: int = 20) -> float:
  52. closes = _closes(candles)
  53. if len(closes) < period:
  54. raise InsufficientDataError(f"SMA({period}) requires at least {period} candles, got {len(closes)}")
  55. return round(sum(closes[-period:]) / period, 6)
  56. def atr(candles: list, period: int = 14) -> float:
  57. highs = _highs(candles)
  58. lows = _lows(candles)
  59. closes = _closes(candles)
  60. if len(closes) < period + 1:
  61. raise InsufficientDataError(f"ATR({period}) requires at least {period + 1} candles, got {len(closes)}")
  62. true_ranges = []
  63. for i in range(1, len(candles)):
  64. high = highs[i]
  65. low = lows[i]
  66. prev_close = closes[i - 1]
  67. tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
  68. true_ranges.append(tr)
  69. first_atr = sum(true_ranges[:period]) / period
  70. atr_val = first_atr
  71. for tr in true_ranges[period:]:
  72. atr_val = ((atr_val * (period - 1)) + tr) / period
  73. return round(atr_val, 6)
  74. def bollinger_bands(candles: list, period: int = 20, multiplier: float = 2.0) -> dict:
  75. closes = _closes(candles)
  76. if len(closes) < period:
  77. raise InsufficientDataError(f"Bollinger Bands require at least {period} candles, got {len(closes)}")
  78. window = closes[-period:]
  79. middle = sum(window) / period
  80. variance = sum((price - middle) ** 2 for price in window) / period
  81. std_dev = sqrt(variance)
  82. upper = middle + multiplier * std_dev
  83. lower = middle - multiplier * std_dev
  84. return {"middle": round(middle, 6), "upper": round(upper, 6), "lower": round(lower, 6)}
  85. def vwap(candles: list, period: int | None = None) -> float:
  86. if period is not None and period <= 0:
  87. raise InsufficientDataError("VWAP period must be positive")
  88. subset = candles[-period:] if period is not None else candles
  89. if len(subset) < 1:
  90. raise InsufficientDataError("VWAP requires at least 1 candle")
  91. volumes = _volumes(subset)
  92. if sum(volumes) == 0:
  93. raise InsufficientDataError("VWAP requires non-zero volume")
  94. typical_prices = [((float(c[2]) + float(c[3]) + float(c[4])) / 3) for c in subset]
  95. pv = sum(tp * vol for tp, vol in zip(typical_prices, volumes))
  96. total_vol = sum(volumes)
  97. return round(pv / total_vol, 6)
  98. def _rsi_handler(candles: list, params: dict):
  99. return rsi(candles, period=int(params.get("period", 14)))
  100. def _ema_handler(candles: list, params: dict):
  101. return ema(candles, period=int(params.get("period", 20)))
  102. def _macd_handler(candles: list, params: dict):
  103. return macd(
  104. candles,
  105. fast_period=int(params.get("fast_period", 12)),
  106. slow_period=int(params.get("slow_period", 26)),
  107. signal_period=int(params.get("signal_period", 9)),
  108. )
  109. def _sma_handler(candles: list, params: dict):
  110. return sma(candles, period=int(params.get("period", 20)))
  111. def _atr_handler(candles: list, params: dict):
  112. return atr(candles, period=int(params.get("period", 14)))
  113. def _bollinger_handler(candles: list, params: dict):
  114. return bollinger_bands(
  115. candles,
  116. period=int(params.get("period", 20)),
  117. multiplier=float(params.get("multiplier", 2.0)),
  118. )
  119. def _vwap_handler(candles: list, params: dict):
  120. period = params.get("period")
  121. if period is not None:
  122. period = int(period)
  123. return vwap(candles, period=period)
  124. SUPPORTED_INDICATORS = OrderedDict(
  125. {
  126. "rsi": {
  127. "description": "Relative Strength Index (RSI) — momentum oscillator (0-100) derived from closing price gains/losses; higher values indicate stronger upward pressure.",
  128. "handler": _rsi_handler,
  129. "params": {"period": {"type": "integer", "default": 14, "min": 2}},
  130. "value_type": "number",
  131. },
  132. "ema": {
  133. "description": "Exponential Moving Average (EMA) — weighted moving average emphasizing recent closes to highlight near-term direction.",
  134. "handler": _ema_handler,
  135. "params": {"period": {"type": "integer", "default": 20, "min": 2}},
  136. "value_type": "number",
  137. },
  138. "sma": {
  139. "description": "Simple Moving Average (SMA) — unweighted rolling average of closes, useful for longer-term baselines (e.g., 200-period trend).",
  140. "handler": _sma_handler,
  141. "params": {"period": {"type": "integer", "default": 20, "min": 2}},
  142. "value_type": "number",
  143. },
  144. "macd": {
  145. "description": "Moving Average Convergence Divergence (MACD) — returns MACD, signal, and histogram values for spotting momentum shifts between fast/slow EMAs.",
  146. "handler": _macd_handler,
  147. "params": {
  148. "fast_period": {"type": "integer", "default": 12, "min": 2},
  149. "slow_period": {"type": "integer", "default": 26, "min": 3},
  150. "signal_period": {"type": "integer", "default": 9, "min": 2},
  151. },
  152. "value_type": "object",
  153. },
  154. "atr": {
  155. "description": "Average True Range (ATR) — classic volatility gauge derived from true range; higher values imply wider expected movement.",
  156. "handler": _atr_handler,
  157. "params": {"period": {"type": "integer", "default": 14, "min": 2}},
  158. "value_type": "number",
  159. },
  160. "bollinger": {
  161. "description": "Bollinger Bands — middle SMA with upper/lower bands offset by standard deviations; helpful for squeeze/mean-reversion checks.",
  162. "handler": _bollinger_handler,
  163. "params": {
  164. "period": {"type": "integer", "default": 20, "min": 2},
  165. "multiplier": {"type": "number", "default": 2.0, "min": 0.5},
  166. },
  167. "value_type": "object",
  168. },
  169. "vwap": {
  170. "description": "Volume Weighted Average Price (VWAP) — rolling average price weighted by traded volume; useful as an intraday fair-value anchor.",
  171. "handler": _vwap_handler,
  172. "params": {"period": {"type": "integer", "default": None, "nullable": True}},
  173. "value_type": "number",
  174. },
  175. }
  176. )
  177. def get_supported_indicators() -> list[dict]:
  178. entries: list[dict] = []
  179. for name, meta in SUPPORTED_INDICATORS.items():
  180. entries.append(
  181. {
  182. "name": name,
  183. "description": meta["description"],
  184. "params": meta["params"],
  185. "value_type": meta.get("value_type", "number"),
  186. }
  187. )
  188. return entries
  189. def compute_indicator(candles: list, indicator: str, params: dict) -> dict:
  190. ind = indicator.lower()
  191. if ind not in SUPPORTED_INDICATORS:
  192. raise UnsupportedIndicatorError(f"Unsupported indicator: {indicator}")
  193. handler = SUPPORTED_INDICATORS[ind]["handler"]
  194. value = handler(candles, params)
  195. return {"indicator": ind, "value": value}