""" Technical indicators for grid-bot strategy. Pure-math module (no API calls, no I/O). Used by main.py to compute SMA(20) and ATR(14) over Tradernet's `getHloc` candles, replacing the reactive "center = current_price" logic with a stable, MA-anchored grid. Functions: compute_sma(prices, period=20) compute_atr(candles, period=14) compute_sma_atr(candles, sma_period=20, atr_period=14) """ from typing import Sequence def compute_sma(prices: Sequence[float], period: int = 20) -> float | None: """Simple Moving Average over the last `period` values of `prices`. Returns None if fewer than `period` prices available. """ if len(prices) < period: return None return sum(prices[-period:]) / period def compute_atr(candles: Sequence[dict], period: int = 14) -> float | None: """Average True Range over the last `period` candles. Each candle: dict with keys 'o','h','l','c','v' (or 'open','high','low','close','volume'). True Range (TR) = max(high - low, |high - prev_close|, |low - prev_close|) ATR = mean of last `period` TRs. Returns None if fewer than `period + 1` candles available (we need `period` TRs, each TR compares to previous close). """ if len(candles) < period + 1: return None def _h(c): return c.get("h", c.get("high")) def _l(c): return c.get("l", c.get("low")) def _c(c): return c.get("c", c.get("close")) trs = [] for i in range(1, len(candles)): h, l, pc = _h(candles[i]), _l(candles[i]), _c(candles[i - 1]) if None in (h, l, pc): continue high_low = h - l high_pc = abs(h - pc) low_pc = abs(l - pc) tr = max(high_low, high_pc, low_pc) trs.append(tr) if len(trs) < period: return None return sum(trs[-period:]) / period def compute_sma_atr( candles: Sequence[dict], sma_period: int = 20, atr_period: int = 14, width_atr_mult: float = 2.0, ) -> dict: """Compute SMA + ATR + derived grid range in one call. Returns: { "sma": float | None, "atr": float | None, "sma_period": int, "atr_period": int, "width_atr_mult": float, "n_candles": int, "last_close": float | None, "range_h": float | None, # sma + width_atr_mult * atr (upper grid bound) "range_l": float | None, # sma - width_atr_mult * atr (lower grid bound) } All values None if insufficient data. """ closes = [] for c in candles: cl = c.get("c", c.get("close")) if cl is not None: closes.append(cl) sma = compute_sma(closes, sma_period) atr = compute_atr(candles, atr_period) out = { "sma": sma, "atr": atr, "sma_period": sma_period, "atr_period": atr_period, "width_atr_mult": width_atr_mult, "n_candles": len(candles), "last_close": closes[-1] if closes else None, "range_h": None, "range_l": None, } if sma is not None and atr is not None: out["range_h"] = sma + width_atr_mult * atr out["range_l"] = sma - width_atr_mult * atr return out if __name__ == "__main__": # Smoke test on synthetic data synth_prices = [100 + i * 0.1 + (i % 5) * 0.05 for i in range(30)] synth_candles = [ {"o": p, "h": p + 0.5, "l": p - 0.5, "c": p, "v": 100} for p in synth_prices ] print("Synthetic smoke test:") print(f" Last close: {synth_prices[-1]:.4f}") print(f" SMA(20): {compute_sma(synth_prices, 20):.4f}") print(f" ATR(14): {compute_atr(synth_candles, 14):.4f}") full = compute_sma_atr(synth_candles) print(f" Range: ${full['range_l']:.4f} — ${full['range_h']:.4f}")