Files

53 lines
1.3 KiB
Python

import math
import numpy as np
__all__ = ["ema"]
EPSILON = 1e-10 # bias-compensator cutoff
def ema(source, period: int = 10, *, alpha: float | None = None) -> np.ndarray:
"""Bias-compensated EMA.
Use *period* (default) or explicit *alpha* (keyword-only).
If *alpha* is given, *period* is ignored.
"""
if alpha is not None:
if not (0.0 < alpha <= 1.0):
raise ValueError(f"alpha must be in (0, 1], got {alpha}")
else:
if period <= 0:
raise ValueError(f"period must be > 0, got {period}")
alpha = 2.0 / (period + 1)
src = np.asarray(source, dtype=np.float64)
if src.ndim == 0 or src.size == 0:
raise ValueError("source must not be empty")
src = src.ravel()
out = np.empty(len(src), dtype=np.float64)
decay = 1.0 - alpha
ema_val = 0.0
e = 1.0
last_valid = 0.0
has_valid = False
for i, v in enumerate(src):
if math.isfinite(v):
last_valid = v
has_valid = True
elif has_valid:
v = last_valid
else:
out[i] = math.nan
continue
ema_val = ema_val * decay + alpha * v
if e > EPSILON:
e *= decay
out[i] = ema_val / (1.0 - e)
else:
out[i] = ema_val
return out