HypeAdvisor / src /indicators.py
Daniel Alnawaz
first push
aa66616 unverified
Raw History Blame Contribute Delete
7.54 kB
"""Technical indicators computed from a candle DataFrame."""
from __future__ import annotations
from dataclasses import dataclass, field
import numpy as np
import pandas as pd
# ---------------------------------------------------------------------------
# Core indicator helpers
# ---------------------------------------------------------------------------
def _ema(series: pd.Series, period: int) -> pd.Series:
return series.ewm(span=period, adjust=False).mean()
def _rsi(close: pd.Series, period: int = 14) -> pd.Series:
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=period - 1, adjust=False).mean()
avg_loss = loss.ewm(com=period - 1, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, np.nan)
return 100 - (100 / (1 + rs))
def _atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series:
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(com=period - 1, adjust=False).mean()
def _macd(close: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9):
ema_fast = _ema(close, fast)
ema_slow = _ema(close, slow)
macd_line = ema_fast - ema_slow
signal_line = _ema(macd_line, signal)
histogram = macd_line - signal_line
return macd_line, signal_line, histogram
def _bollinger(close: pd.Series, period: int = 20, std_dev: float = 2.0):
mid = close.rolling(period).mean()
std = close.rolling(period).std(ddof=0)
upper = mid + std_dev * std
lower = mid - std_dev * std
return upper, mid, lower
# ---------------------------------------------------------------------------
# Public surface
# ---------------------------------------------------------------------------
@dataclass
class IndicatorSnapshot:
"""Latest values for all indicators, ready for the LLM prompt."""
price: float
rsi_14: float
ema_20: float
ema_50: float
ema_200: float
macd_line: float
macd_signal: float
macd_hist: float
bb_upper: float
bb_mid: float
bb_lower: float
atr_14: float
# Derived context
ema_trend: str # "bullish" | "bearish" | "mixed"
bb_position: str # "above_upper" | "near_upper" | "mid" | "near_lower" | "below_lower"
rsi_zone: str # "overbought" | "neutral" | "oversold"
volume_vs_avg: float # current candle volume / 20-bar avg volume
# Recent price structure (last N candles)
recent_highs: list[float] = field(default_factory=list)
recent_lows: list[float] = field(default_factory=list)
# ATR-based TP/SL suggestions (2:1 R/R)
suggested_long_tp: float = 0.0
suggested_long_sl: float = 0.0
suggested_short_tp: float = 0.0
suggested_short_sl: float = 0.0
def to_prompt_block(self) -> str:
lines = [
f"Current price : {self.price:.4f}",
"",
"--- Trend ---",
f"EMA 20 : {self.ema_20:.4f}",
f"EMA 50 : {self.ema_50:.4f}",
f"EMA 200 : {self.ema_200:.4f}",
f"EMA trend : {self.ema_trend}",
"",
"--- Momentum ---",
f"RSI(14) : {self.rsi_14:.2f} [{self.rsi_zone}]",
f"MACD line : {self.macd_line:.4f}",
f"MACD signal : {self.macd_signal:.4f}",
f"MACD hist : {self.macd_hist:.4f} ({'positive' if self.macd_hist > 0 else 'negative'})",
"",
"--- Volatility ---",
f"BB upper : {self.bb_upper:.4f}",
f"BB mid : {self.bb_mid:.4f}",
f"BB lower : {self.bb_lower:.4f}",
f"Price vs BB : {self.bb_position}",
f"ATR(14) : {self.atr_14:.4f}",
"",
"--- Volume ---",
f"Vol / 20-avg : {self.volume_vs_avg:.2f}x",
"",
"--- Recent swing highs (last 5) ---",
", ".join(f"{h:.4f}" for h in self.recent_highs[-5:]),
"--- Recent swing lows (last 5) ---",
", ".join(f"{l:.4f}" for l in self.recent_lows[-5:]),
"",
"--- ATR-based TP/SL (2:1 R/R) ---",
f"LONG TP : {self.suggested_long_tp:.4f} | SL : {self.suggested_long_sl:.4f}",
f"SHORT TP : {self.suggested_short_tp:.4f} | SL : {self.suggested_short_sl:.4f}",
]
return "\n".join(lines)
def compute_indicators(df: pd.DataFrame, atr_multiplier_tp: float = 2.0, atr_multiplier_sl: float = 1.0) -> IndicatorSnapshot:
"""Compute all indicators from a candle DataFrame and return the latest snapshot."""
close = df["close"]
high = df["high"]
low = df["low"]
volume = df["volume"]
rsi = _rsi(close)
ema20 = _ema(close, 20)
ema50 = _ema(close, 50)
ema200 = _ema(close, 200)
macd_line, macd_sig, macd_hist = _macd(close)
bb_upper, bb_mid, bb_lower = _bollinger(close)
atr = _atr(high, low, close)
vol_avg = volume.rolling(20).mean()
price = float(close.iloc[-1])
atr_val = float(atr.iloc[-1])
# EMA trend
e20, e50, e200 = float(ema20.iloc[-1]), float(ema50.iloc[-1]), float(ema200.iloc[-1])
if price > e20 > e50 > e200:
ema_trend = "strongly_bullish"
elif price > e20 and e20 > e50:
ema_trend = "bullish"
elif price < e20 < e50 < e200:
ema_trend = "strongly_bearish"
elif price < e20 and e20 < e50:
ema_trend = "bearish"
else:
ema_trend = "mixed"
# BB position
bbu, bbm, bbl = float(bb_upper.iloc[-1]), float(bb_mid.iloc[-1]), float(bb_lower.iloc[-1])
band_width = bbu - bbl
if price > bbu:
bb_pos = "above_upper"
elif price > bbu - 0.1 * band_width:
bb_pos = "near_upper"
elif price < bbl:
bb_pos = "below_lower"
elif price < bbl + 0.1 * band_width:
bb_pos = "near_lower"
else:
bb_pos = "mid"
# RSI zone
rsi_val = float(rsi.iloc[-1])
if rsi_val >= 70:
rsi_zone = "overbought"
elif rsi_val <= 30:
rsi_zone = "oversold"
else:
rsi_zone = "neutral"
# Volume ratio
vol_ratio = float(volume.iloc[-1] / vol_avg.iloc[-1]) if float(vol_avg.iloc[-1]) > 0 else 1.0
# Swing highs / lows (simple local extrema over last 20 candles)
window = df.tail(20)
recent_highs = list(window["high"].nlargest(5).sort_index().values)
recent_lows = list(window["low"].nsmallest(5).sort_index().values)
# TP / SL
long_tp = price + atr_multiplier_tp * atr_val
long_sl = price - atr_multiplier_sl * atr_val
short_tp = price - atr_multiplier_tp * atr_val
short_sl = price + atr_multiplier_sl * atr_val
return IndicatorSnapshot(
price=price,
rsi_14=rsi_val,
ema_20=e20,
ema_50=e50,
ema_200=e200,
macd_line=float(macd_line.iloc[-1]),
macd_signal=float(macd_sig.iloc[-1]),
macd_hist=float(macd_hist.iloc[-1]),
bb_upper=bbu,
bb_mid=bbm,
bb_lower=bbl,
atr_14=atr_val,
ema_trend=ema_trend,
bb_position=bb_pos,
rsi_zone=rsi_zone,
volume_vs_avg=vol_ratio,
recent_highs=recent_highs,
recent_lows=recent_lows,
suggested_long_tp=round(long_tp, 4),
suggested_long_sl=round(long_sl, 4),
suggested_short_tp=round(short_tp, 4),
suggested_short_sl=round(short_sl, 4),
)