"""
Technical indicators computed from OHLCV data.
Pure Python — no external dependencies.
"""
from typing import List, Optional, Tuple
def sma(closes: List[float], period: int = 20) -> List[Optional[float]]:
"""Simple Moving Average."""
result: List[Optional[float]] = [None] * len(closes)
for i in range(period - 1, len(closes)):
result[i] = sum(closes[i - period + 1: i + 1]) / period
return result
def ema(closes: List[float], period: int = 20) -> List[Optional[float]]:
"""Exponential Moving Average."""
if len(closes) < period:
return [None] * len(closes)
result: List[Optional[float]] = [None] * len(closes)
k = 2.0 / (period + 1)
result[period - 1] = sum(closes[:period]) / period
for i in range(period, len(closes)):
result[i] = closes[i] * k + result[i - 1] * (1 - k)
return result
def rsi(closes: List[float], period: int = 14) -> List[Optional[float]]:
"""Relative Strength Index."""
if len(closes) < period + 1:
return [None] * len(closes)
result: List[Optional[float]] = [None] * len(closes)
gains = []
losses = []
for i in range(1, len(closes)):
delta = closes[i] - closes[i - 1]
gains.append(max(delta, 0))
losses.append(max(-delta, 0))
avg_gain = sum(gains[:period]) / period
avg_loss = sum(losses[:period]) / period
if avg_loss == 0:
result[period] = 100.0
else:
result[period] = 100.0 - (100.0 / (1.0 + avg_gain / avg_loss))
for i in range(period, len(gains)):
avg_gain = (avg_gain * (period - 1) + gains[i]) / period
avg_loss = (avg_loss * (period - 1) + losses[i]) / period
if avg_loss == 0:
result[i + 1] = 100.0
else:
result[i + 1] = 100.0 - (100.0 / (1.0 + avg_gain / avg_loss))
return result
def macd(
closes: List[float],
fast: int = 12,
slow: int = 26,
signal: int = 9,
) -> Tuple[List[Optional[float]], List[Optional[float]], List[Optional[float]]]:
"""MACD line, Signal line, Histogram."""
ema_fast = ema(closes, fast)
ema_slow = ema(closes, slow)
n = len(closes)
macd_line: List[Optional[float]] = [None] * n
for i in range(n):
if ema_fast[i] is not None and ema_slow[i] is not None:
macd_line[i] = ema_fast[i] - ema_slow[i]
signal_line: List[Optional[float]] = [None] * n
macd_vals = [v for v in macd_line if v is not None]
sig = ema(macd_vals, signal)
j = 0
for i in range(n):
if macd_line[i] is not None:
signal_line[i] = sig[j] if j < len(sig) else None
j += 1
histogram: List[Optional[float]] = [None] * n
for i in range(n):
if macd_line[i] is not None and signal_line[i] is not None:
histogram[i] = macd_line[i] - signal_line[i]
return macd_line, signal_line, histogram
def bollinger(
closes: List[float],
period: int = 20,
std_dev: float = 2.0,
) -> Tuple[List[Optional[float]], List[Optional[float]], List[Optional[float]]]:
"""Upper, Middle (SMA), Lower Bollinger Bands."""
n = len(closes)
middle = sma(closes, period)
upper: List[Optional[float]] = [None] * n
lower: List[Optional[float]] = [None] * n
for i in range(period - 1, n):
window = closes[i - period + 1: i + 1]
mean = middle[i]
variance = sum((x - mean) ** 2 for x in window) / period
std = variance ** 0.5
upper[i] = mean + std_dev * std
lower[i] = mean - std_dev * std
return upper, middle, lower