__init__.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. """Service layer."""
  2. import time
  3. from config import DEFAULT_OHLCV_LIMIT, MAX_OHLCV_LIMIT, TIMEFRAME_TO_BINANCE
  4. from cache import get_cached_price, set_cached_price, get_cached_ohlcv, set_cached_ohlcv
  5. import providers
  6. import indicators as ind_module
  7. async def get_price(symbol: str) -> dict:
  8. symbol = symbol.upper()
  9. cached = get_cached_price(symbol)
  10. if cached:
  11. return cached
  12. data = await providers.fetch_price(symbol)
  13. set_cached_price(symbol, data)
  14. return data
  15. async def get_ohlcv(symbol: str, timeframe: str, limit: int = DEFAULT_OHLCV_LIMIT) -> dict:
  16. symbol = symbol.upper()
  17. limit = min(max(limit, 1), MAX_OHLCV_LIMIT)
  18. cached = get_cached_ohlcv(symbol, timeframe)
  19. if cached:
  20. result = dict(cached)
  21. result["candles"] = cached["candles"][-limit:]
  22. return result
  23. data = await providers.fetch_ohlcv(symbol, timeframe, limit=MAX_OHLCV_LIMIT)
  24. set_cached_ohlcv(symbol, timeframe, data)
  25. result = dict(data)
  26. result["candles"] = data["candles"][-limit:]
  27. return result
  28. async def get_indicator(symbol: str, indicator: str, timeframe: str = "1h", params: dict = None, limit: int = 200) -> dict:
  29. params = params or {}
  30. symbol = symbol.upper()
  31. ohlcv_data = await get_ohlcv(symbol, timeframe, limit=limit)
  32. result = ind_module.compute_indicator(ohlcv_data["candles"], indicator, params)
  33. return {"symbol": symbol, "indicator": result["indicator"], "timeframe": timeframe, "value": result["value"], "timestamp": int(time.time())}
  34. async def get_market_snapshot(symbol: str) -> dict:
  35. symbol = symbol.upper()
  36. price_data = await get_price(symbol)
  37. ohlcv_data = await get_ohlcv(symbol, "1h", limit=200)
  38. candles = ohlcv_data["candles"]
  39. price = price_data["price"]
  40. snapshot = {
  41. "symbol": symbol,
  42. "price": price,
  43. "rsi_1h": None,
  44. "ema_20_1h": None,
  45. "ema_50_1h": None,
  46. "ema_200_1h": None,
  47. "macd_histogram_1h": None,
  48. "atr_1h": None,
  49. "atr_percent_1h": None,
  50. "bollinger_1h": None,
  51. "vwap_1h": None,
  52. "trend_bias": None,
  53. "timestamp": price_data["timestamp"],
  54. }
  55. def _compute(name: str, params: dict):
  56. return ind_module.compute_indicator(candles, name, params)["value"]
  57. for key, ind, params in [
  58. ("rsi_1h", "rsi", {"period": 14}),
  59. ("ema_20_1h", "ema", {"period": 20}),
  60. ("ema_50_1h", "ema", {"period": 50}),
  61. ("ema_200_1h", "sma", {"period": 200}),
  62. ]:
  63. try:
  64. snapshot[key] = _compute(ind, params)
  65. except Exception:
  66. continue
  67. try:
  68. macd_vals = _compute("macd", {"fast_period": 12, "slow_period": 26, "signal_period": 9})
  69. if isinstance(macd_vals, dict):
  70. snapshot["macd_histogram_1h"] = macd_vals.get("histogram")
  71. except Exception:
  72. pass
  73. try:
  74. atr_val = _compute("atr", {"period": 14})
  75. snapshot["atr_1h"] = atr_val
  76. if atr_val is not None and price:
  77. snapshot["atr_percent_1h"] = round((atr_val / price) * 100, 4)
  78. except Exception:
  79. pass
  80. try:
  81. snapshot["bollinger_1h"] = _compute("bollinger", {"period": 20, "multiplier": 2.0})
  82. except Exception:
  83. pass
  84. try:
  85. snapshot["vwap_1h"] = _compute("vwap", {"period": 48})
  86. except Exception:
  87. pass
  88. ema_fast = snapshot.get("ema_20_1h")
  89. ema_slow = snapshot.get("ema_50_1h")
  90. if ema_fast is not None and ema_slow not in (None, 0):
  91. delta = (ema_fast - ema_slow) / ema_slow
  92. if delta > 0.002:
  93. snapshot["trend_bias"] = "bull"
  94. elif delta < -0.002:
  95. snapshot["trend_bias"] = "bear"
  96. else:
  97. snapshot["trend_bias"] = "range"
  98. return snapshot
  99. async def get_top_movers(limit: int = 10) -> dict:
  100. return await providers.fetch_top_movers(min(max(limit, 1), 50))
  101. async def get_capabilities() -> dict:
  102. timeframe_descriptions = {
  103. "1m": "1-minute candles — ultra-fast scalping / heartbeat data (highest volatility, shortest TTL)",
  104. "5m": "5-minute candles — intraday momentum and micro-structure",
  105. "15m": "15-minute candles — short-term swings, aligns with many bot cycles",
  106. "1h": "1-hour candles — default for balanced trend/momentum reads",
  107. "4h": "4-hour candles — swing/position traders",
  108. "1d": "1-day candles — macro trend / higher timeframe context",
  109. }
  110. timeframes = []
  111. for tf, interval in TIMEFRAME_TO_BINANCE.items():
  112. timeframes.append(
  113. {
  114. "timeframe": tf,
  115. "provider_interval": interval,
  116. "description": timeframe_descriptions.get(tf, ""),
  117. }
  118. )
  119. return {
  120. "description": "Supported technical indicators (with params/defaults) and the timeframes you can request via get_ohlcv / get_indicator / get_regime.",
  121. "indicators": ind_module.get_supported_indicators(),
  122. "timeframes": timeframes,
  123. }
  124. async def get_regime(symbol: str, timeframe: str = "1h", limit: int = 200) -> dict:
  125. symbol = symbol.upper()
  126. ohlcv_data = await get_ohlcv(symbol, timeframe, limit=limit)
  127. candles = ohlcv_data["candles"]
  128. close_price = float(candles[-1][4]) if candles else None
  129. timestamp = int(time.time())
  130. def _compute(name: str, params: dict | None = None):
  131. try:
  132. return ind_module.compute_indicator(candles, name, params or {})["value"]
  133. except Exception:
  134. return None
  135. ema_fast = _compute("ema", {"period": 20})
  136. ema_slow = _compute("ema", {"period": 50})
  137. sma_long = _compute("sma", {"period": 200})
  138. atr_val = _compute("atr", {"period": 14})
  139. boll = _compute("bollinger", {"period": 20, "multiplier": 2.0})
  140. vwap_val = _compute("vwap", {"period": 48})
  141. rsi_val = _compute("rsi", {"period": 14})
  142. macd_vals = _compute("macd", {"fast_period": 12, "slow_period": 26, "signal_period": 9})
  143. regime_delta = None
  144. if ema_fast is not None and ema_slow is not None and ema_slow != 0:
  145. regime_delta = (ema_fast - ema_slow) / ema_slow
  146. if regime_delta is None:
  147. trend_state = "unknown"
  148. elif regime_delta > 0.002:
  149. trend_state = "bull"
  150. elif regime_delta < -0.002:
  151. trend_state = "bear"
  152. else:
  153. trend_state = "range"
  154. if rsi_val is None:
  155. momentum_state = "unknown"
  156. elif rsi_val >= 60:
  157. momentum_state = "bull"
  158. elif rsi_val <= 40:
  159. momentum_state = "bear"
  160. else:
  161. momentum_state = "neutral"
  162. atr_percent = None
  163. if atr_val is not None and close_price:
  164. atr_percent = round((atr_val / close_price) * 100, 4)
  165. macd_hist = macd_vals.get("histogram") if isinstance(macd_vals, dict) else None
  166. # --- Early reversal score (heuristic) ---
  167. # Goal: flag plausible “pivot attempts” before full trend confirmation.
  168. # Note: without previous-bar indicator deltas, this is a conservative, snapshot-based score.
  169. reversal = {"direction": "none", "score": 0, "triggers": []}
  170. if close_price is not None and vwap_val is not None and isinstance(boll, dict):
  171. upper = boll.get("upper")
  172. lower = boll.get("lower")
  173. if upper and lower and (upper > lower):
  174. dist_to_upper = (upper - close_price) / close_price if close_price else None
  175. dist_to_lower = (close_price - lower) / close_price if close_price else None
  176. else:
  177. dist_to_upper = None
  178. dist_to_lower = None
  179. # Bullish reversal attempt while trend is bearish
  180. if trend_state == "bear" and macd_hist is not None and rsi_val is not None and macd_hist > 0:
  181. score = 0
  182. if rsi_val >= 45:
  183. score += 25
  184. reversal["triggers"].append("RSI rising/near-neutral (>=45)")
  185. if vwap_val is not None and close_price > vwap_val:
  186. score += 25
  187. reversal["triggers"].append("Price reclaimed above VWAP")
  188. if dist_to_lower is not None and dist_to_lower <= 0.01:
  189. score += 20
  190. reversal["triggers"].append("Near lower Bollinger band")
  191. if atr_percent is not None and atr_percent >= 0.8:
  192. score += 10
  193. reversal["triggers"].append("Volatility elevated (ATR% >= 0.8)")
  194. if score >= 40:
  195. reversal.update({"direction": "bullish", "score": min(score, 95)})
  196. # Bearish reversal attempt while trend is bullish
  197. if trend_state == "bull" and macd_hist is not None and rsi_val is not None and macd_hist < 0:
  198. score = 0
  199. if rsi_val <= 55:
  200. score += 25
  201. reversal["triggers"].append("RSI falling/near-neutral (<=55)")
  202. if vwap_val is not None and close_price < vwap_val:
  203. score += 25
  204. reversal["triggers"].append("Price rejected below VWAP")
  205. if dist_to_upper is not None and dist_to_upper <= 0.01:
  206. score += 20
  207. reversal["triggers"].append("Near upper Bollinger band")
  208. if atr_percent is not None and atr_percent >= 0.8:
  209. score += 10
  210. reversal["triggers"].append("Volatility elevated (ATR% >= 0.8)")
  211. if score >= 40:
  212. reversal.update({"direction": "bearish", "score": min(score, 95)})
  213. return {
  214. "symbol": symbol,
  215. "timeframe": timeframe,
  216. "timestamp": timestamp,
  217. "price": close_price,
  218. "trend": {
  219. "ema_fast": ema_fast,
  220. "ema_slow": ema_slow,
  221. "sma_long": sma_long,
  222. "state": trend_state,
  223. },
  224. "momentum": {
  225. "rsi": rsi_val,
  226. "macd_histogram": macd_hist,
  227. "state": momentum_state,
  228. },
  229. "volatility": {
  230. "atr": atr_val,
  231. "atr_percent": atr_percent,
  232. },
  233. "bands": {
  234. "bollinger": boll,
  235. },
  236. "vwap": vwap_val,
  237. "reversal": reversal,
  238. }