import logging import pandas as pd import ta import config logger = logging.getLogger(__name__) def calculate_indicators(candles_df: pd.DataFrame) -> pd.DataFrame: """Add technical indicators to a candles DataFrame. Expected input columns: timestamp, open, close, high, low, volume. Returns the DataFrame with additional indicator columns. """ df = candles_df.copy() if df.empty or len(df) < 26: logger.warning("Not enough candle data to compute indicators (got %d rows)", len(df)) return df close = df["close"] high = df["high"] low = df["low"] volume = df["volume"] # RSI(14) df["rsi"] = ta.momentum.rsi(close, window=14) # MACD(12, 26, 9) macd = ta.trend.MACD(close, window_slow=26, window_fast=12, window_sign=9) df["macd"] = macd.macd() df["macd_signal"] = macd.macd_signal() df["macd_hist"] = macd.macd_diff() # Bollinger Bands(20, 2) bb = ta.volatility.BollingerBands(close, window=20, window_dev=2) df["bb_upper"] = bb.bollinger_hband() df["bb_middle"] = bb.bollinger_mavg() df["bb_lower"] = bb.bollinger_lband() # EMA(9) and EMA(21) df["ema9"] = ta.trend.ema_indicator(close, window=9) df["ema21"] = ta.trend.ema_indicator(close, window=21) # VWAP (cumulative for the session) tp = (high + low + close) / 3 cumvol = volume.cumsum() df["vwap"] = (tp * volume).cumsum() / cumvol.replace(0, float("nan")) # ATR(14) df["atr"] = ta.volatility.average_true_range(high, low, close, window=14) # Volume moving average (20) df["vol_ma20"] = volume.rolling(window=20).mean() return df def summarize_indicators(df: pd.DataFrame, symbol: str) -> str: """Produce a concise text summary of the latest indicator values for LLM consumption.""" if df.empty or len(df) < 2: return f"{symbol}: insufficient data\n" last = df.iloc[-1] prev = df.iloc[-2] name = config.SYMBOL_NAMES.get(symbol, symbol) lines = [f"### {name} ({symbol})"] lines.append(f"Price: {last['close']:.6g} | High: {last['high']:.6g} | Low: {last['low']:.6g}") lines.append(f"Volume: {last['volume']:.2f} | Vol MA20: {last.get('vol_ma20', 0):.2f}") if pd.notna(last.get("rsi")): lines.append(f"RSI(14): {last['rsi']:.1f}") if pd.notna(last.get("macd")): hist_dir = "↑" if last["macd_hist"] > prev.get("macd_hist", 0) else "↓" lines.append( f"MACD: {last['macd']:.6g} | Signal: {last['macd_signal']:.6g} | " f"Hist: {last['macd_hist']:.6g} ({hist_dir})" ) if pd.notna(last.get("bb_upper")): lines.append( f"BB: Upper={last['bb_upper']:.6g} Mid={last['bb_middle']:.6g} Lower={last['bb_lower']:.6g}" ) if pd.notna(last.get("ema9")): trend = "bullish" if last["ema9"] > last["ema21"] else "bearish" lines.append(f"EMA9: {last['ema9']:.6g} | EMA21: {last['ema21']:.6g} → {trend}") if pd.notna(last.get("atr")): lines.append(f"ATR(14): {last['atr']:.6g}") if pd.notna(last.get("vwap")): lines.append(f"VWAP: {last['vwap']:.6g}") return "\n".join(lines) + "\n" def summarize_all(indicators_by_symbol: dict[str, pd.DataFrame]) -> str: """Build a combined indicator summary for all symbols.""" parts = [] for sym, df in sorted(indicators_by_symbol.items()): parts.append(summarize_indicators(df, sym)) return "\n".join(parts)