"""Market data fetching and metric computation for precheck.""" from __future__ import annotations import os import ccxt from .data_utils import norm_symbol, to_float def get_exchange(): from ..runtime import load_env_file load_env_file() api_key = os.getenv("BINANCE_API_KEY") secret = os.getenv("BINANCE_API_SECRET") if not api_key or not secret: raise RuntimeError("Missing BINANCE_API_KEY or BINANCE_API_SECRET in ~/.hermes/.env") ex = ccxt.binance({ "apiKey": api_key, "secret": secret, "options": {"defaultType": "spot"}, "enableRateLimit": True, }) ex.load_markets() return ex def fetch_ohlcv_batch(ex, symbols: set, timeframe: str, limit: int): results = {} for sym in sorted(symbols): try: ohlcv = ex.fetch_ohlcv(sym, timeframe=timeframe, limit=limit) if ohlcv and len(ohlcv) >= 2: results[sym] = ohlcv except Exception: pass return results def compute_ohlcv_metrics(ohlcv_1h, ohlcv_4h, current_price, volume_24h=None): metrics = {} if ohlcv_1h and len(ohlcv_1h) >= 2: closes = [c[4] for c in ohlcv_1h] volumes = [c[5] for c in ohlcv_1h] metrics["change_1h_pct"] = round((closes[-1] - closes[-2]) / closes[-2] * 100, 2) if closes[-2] != 0 else None if len(closes) >= 5: metrics["change_4h_pct"] = round((closes[-1] - closes[-5]) / closes[-5] * 100, 2) if closes[-5] != 0 else None recent_vol = sum(volumes[-4:]) / 4 if len(volumes) >= 4 else None metrics["volume_1h_avg"] = round(recent_vol, 2) if recent_vol else None highs = [c[2] for c in ohlcv_1h[-4:]] lows = [c[3] for c in ohlcv_1h[-4:]] metrics["high_4h"] = round(max(highs), 8) if highs else None metrics["low_4h"] = round(min(lows), 8) if lows else None if ohlcv_4h and len(ohlcv_4h) >= 2: closes_4h = [c[4] for c in ohlcv_4h] volumes_4h = [c[5] for c in ohlcv_4h] metrics["change_4h_pct_from_4h"] = round((closes_4h[-1] - closes_4h[-2]) / closes_4h[-2] * 100, 2) if closes_4h[-2] != 0 else None recent_vol_4h = sum(volumes_4h[-2:]) / 2 if len(volumes_4h) >= 2 else None metrics["volume_4h_avg"] = round(recent_vol_4h, 2) if recent_vol_4h else None highs_4h = [c[2] for c in ohlcv_4h] lows_4h = [c[3] for c in ohlcv_4h] metrics["high_24h_calc"] = round(max(highs_4h), 8) if highs_4h else None metrics["low_24h_calc"] = round(min(lows_4h), 8) if lows_4h else None if highs_4h and lows_4h: avg_price = sum(closes_4h) / len(closes_4h) metrics["volatility_4h_pct"] = round((max(highs_4h) - min(lows_4h)) / avg_price * 100, 2) if current_price: if metrics.get("high_4h"): metrics["distance_from_4h_high_pct"] = round((metrics["high_4h"] - current_price) / metrics["high_4h"] * 100, 2) if metrics.get("low_4h"): metrics["distance_from_4h_low_pct"] = round((current_price - metrics["low_4h"]) / metrics["low_4h"] * 100, 2) if metrics.get("high_24h_calc"): metrics["distance_from_24h_high_pct"] = round((metrics["high_24h_calc"] - current_price) / metrics["high_24h_calc"] * 100, 2) if metrics.get("low_24h_calc"): metrics["distance_from_24h_low_pct"] = round((current_price - metrics["low_24h_calc"]) / metrics["low_24h_calc"] * 100, 2) if volume_24h and volume_24h > 0 and metrics.get("volume_1h_avg"): daily_avg_1h = volume_24h / 24 metrics["volume_1h_multiple"] = round(metrics["volume_1h_avg"] / daily_avg_1h, 2) if volume_24h and volume_24h > 0 and metrics.get("volume_4h_avg"): daily_avg_4h = volume_24h / 6 metrics["volume_4h_multiple"] = round(metrics["volume_4h_avg"] / daily_avg_4h, 2) return metrics def enrich_candidates_and_positions(global_candidates, candidate_layers, positions_view, tickers, ex): symbols = set() for c in global_candidates: symbols.add(c["symbol"]) for p in positions_view: sym = p.get("symbol") if sym: sym_ccxt = norm_symbol(sym) symbols.add(sym_ccxt) ohlcv_1h = fetch_ohlcv_batch(ex, symbols, "1h", 24) ohlcv_4h = fetch_ohlcv_batch(ex, symbols, "4h", 12) def _apply(target_list): for item in target_list: sym = item.get("symbol") if not sym: continue sym_ccxt = norm_symbol(sym) v24h = to_float(tickers.get(sym_ccxt, {}).get("quoteVolume")) metrics = compute_ohlcv_metrics( ohlcv_1h.get(sym_ccxt), ohlcv_4h.get(sym_ccxt), item.get("price") or item.get("last_price"), volume_24h=v24h, ) item["metrics"] = metrics _apply(global_candidates) for band_list in candidate_layers.values(): _apply(band_list) _apply(positions_view) return global_candidates, candidate_layers, positions_view def regime_from_pct(pct: float | None) -> str: if pct is None: return "unknown" if pct >= 2.0: return "bullish" if pct <= -2.0: return "bearish" return "neutral"