Files
gridbot/indicators.py
T

124 lines
3.7 KiB
Python

"""
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}")