bifitnex-trading/indicators.py
kroutony 972d66ab1b Initial commit: LLM-driven crypto trading bot
Includes: Bitfinex API integration, technical indicators,
LLM signal generation, risk management, Slack notifications.

Recent fixes:
- SELL orders use position value instead of total balance
- SELL signals always close full position
- Failed orders added to rejected list for Slack reporting
- Position/exposure limits auto-cap to remaining room
- BUY order minimum raised to 10% of portfolio

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 03:25:18 +00:00

102 lines
3.4 KiB
Python

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)