| """ |
| indicators.py |
| Plain pandas/numpy implementations — no TA-Lib dependency, so it installs |
| cleanly on Windows without a compiler toolchain. |
| """ |
| import numpy as np |
| import pandas as pd |
|
|
|
|
| def ema(series: pd.Series, period: int) -> pd.Series: |
| return series.ewm(span=period, adjust=False).mean() |
|
|
|
|
| def sma(series: pd.Series, period: int) -> pd.Series: |
| return series.rolling(period).mean() |
|
|
|
|
| def rsi(series: pd.Series, period: int = 14) -> pd.Series: |
| delta = series.diff() |
| gain = delta.clip(lower=0) |
| loss = -delta.clip(upper=0) |
| avg_gain = gain.ewm(alpha=1 / period, adjust=False).mean() |
| avg_loss = loss.ewm(alpha=1 / period, adjust=False).mean() |
| rs = avg_gain / avg_loss.replace(0, np.nan) |
| out = 100 - (100 / (1 + rs)) |
| return out.fillna(50) |
|
|
|
|
| def atr(df: pd.DataFrame, period: int = 14) -> pd.Series: |
| high, low, close = df["high"], df["low"], df["close"] |
| prev_close = close.shift(1) |
| tr = pd.concat( |
| [(high - low), (high - prev_close).abs(), (low - prev_close).abs()], axis=1 |
| ).max(axis=1) |
| return tr.ewm(alpha=1 / period, adjust=False).mean() |
|
|
|
|
| def macd(series: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9): |
| fast_ema = ema(series, fast) |
| slow_ema = ema(series, slow) |
| macd_line = fast_ema - slow_ema |
| signal_line = ema(macd_line, signal) |
| hist = macd_line - signal_line |
| return macd_line, signal_line, hist |
|
|
|
|
| def bollinger_bands(series: pd.Series, period: int = 20, std_mult: float = 2.0): |
| mid = sma(series, period) |
| std = series.rolling(period).std() |
| upper = mid + std_mult * std |
| lower = mid - std_mult * std |
| return upper, mid, lower |
|
|
|
|
| def stochastic_oscillator(df: pd.DataFrame, k_period: int = 8, k_slowing: int = 5, d_period: int = 3): |
| """'Slow stochastic', the common retail convention for three numbers |
| like (8, 5, 3): a raw %K over `k_period` bars, smoothed by a |
| `k_slowing`-period SMA to produce the displayed %K line, then %D is |
| a `d_period`-period SMA of that already-slowed %K. Note some |
| platforms label these three inputs in a different order (e.g. MT5's |
| dialog is %K period / %D period / Slowing) — if your source used a |
| different platform's convention, the numbers may need reordering to |
| match; this function documents exactly which role each argument |
| plays so that's easy to check.""" |
| high, low, close = df["high"], df["low"], df["close"] |
| lowest_low = low.rolling(k_period).min() |
| highest_high = high.rolling(k_period).max() |
| raw_k = 100 * (close - lowest_low) / (highest_high - lowest_low).replace(0, np.nan) |
| slow_k = raw_k.rolling(k_slowing).mean() |
| d = slow_k.rolling(d_period).mean() |
| return slow_k.fillna(50), d.fillna(50) |
|
|
|
|
| def add_all_indicators(df: pd.DataFrame) -> pd.DataFrame: |
| """Attaches a standard indicator set used by strategies.py and rating.py.""" |
| out = df.copy() |
| out["ema_20"] = ema(out["close"], 20) |
| out["ema_50"] = ema(out["close"], 50) |
| out["ema_200"] = ema(out["close"], 200) |
| out["rsi_14"] = rsi(out["close"], 14) |
| out["atr_14"] = atr(out, 14) |
| macd_line, signal_line, hist = macd(out["close"]) |
| out["macd"] = macd_line |
| out["macd_signal"] = signal_line |
| out["macd_hist"] = hist |
| bb_up, bb_mid, bb_low = bollinger_bands(out["close"]) |
| out["bb_upper"] = bb_up |
| out["bb_mid"] = bb_mid |
| out["bb_lower"] = bb_low |
| stoch_k, stoch_d = stochastic_oscillator(out, 8, 5, 3) |
| out["stoch_k"] = stoch_k |
| out["stoch_d"] = stoch_d |
| out["body"] = (out["close"] - out["open"]).abs() |
| out["range"] = out["high"] - out["low"] |
| out["upper_wick"] = out["high"] - out[["close", "open"]].max(axis=1) |
| out["lower_wick"] = out[["close", "open"]].min(axis=1) - out["low"] |
| return out |
|
|