wickbot / indicators.py
Joedroid's picture
deploy: update wickbot codebase (part 7)
4550bff verified
Raw
History Blame Contribute Delete
3.75 kB
"""
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