124 lines
3.7 KiB
Python
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}") |