""" LLM Context Builder - Generate structured market analysis for LLM decision making """ import logging from typing import Dict, Any, Optional, List from datetime import datetime import pandas as pd from .data_reader import MarketDataReader from .indicators import TechnicalIndicators from .market_structure import MarketStructureAnalyzer from .orderflow import OrderFlowAnalyzer from .config import config # Import QuantitativeSignalGenerator for scoring import sys sys.path.insert(0, '/app') from signals.quantitative import QuantitativeSignalGenerator logger = logging.getLogger(__name__) # K线数量配置 - 按时间周期不同 KLINE_LIMITS = { '5m': 288, # 1天 = 24*60/5 = 288 根 '15m': 96, # 1天 = 24*60/15 = 96 根 '1h': 72, # 3天 = 3*24 = 72 根 '4h': 18, # 3天 = 3*24/4 = 18 根 '1d': 30, # 30天 '1w': 60, # 60周 } class LLMContextBuilder: """Build structured context for LLM trading decisions""" def __init__(self): self.data_reader = MarketDataReader() def build_full_context(self, symbol: str = "BTCUSDT") -> Dict[str, Any]: """ Build complete market context for LLM analysis Args: symbol: Trading symbol (default: BTCUSDT) Returns: Dict with structured market analysis """ try: # Fetch multi-timeframe data with custom limits mtf_data = self._get_multi_timeframe_data_for_llm() if '5m' not in mtf_data or mtf_data['5m'].empty: logger.error("No 5m data available for analysis") return self._empty_context() # Use 5m as primary timeframe for real-time analysis df_5m = mtf_data['5m'] # Add technical indicators df_5m = TechnicalIndicators.add_all_indicators(df_5m) # Get current price current_price = float(df_5m.iloc[-1]['close']) # Fetch order book data depth_data = self.data_reader.read_latest_depth() # Build context sections (移除支撑位压力位计算) context = { 'timestamp': datetime.now().isoformat(), 'symbol': symbol, 'current_price': round(current_price, 2), 'market_state': self._build_market_state(df_5m, mtf_data), 'momentum': self._build_momentum_analysis(df_5m, depth_data), 'volatility_analysis': self._build_volatility_analysis(df_5m), 'volume_analysis': self._build_volume_analysis(df_5m), 'multi_timeframe': self._build_mtf_summary(mtf_data), 'kline_data': self._build_kline_data(mtf_data), # 新增 K 线数据 'signal_consensus': self._calculate_signal_consensus(df_5m, depth_data), 'risk_metrics': self._build_risk_metrics(df_5m, current_price), } logger.info(f"Built LLM context: trend={context['market_state']['trend_direction']}, consensus={context['signal_consensus']}") return context except Exception as e: logger.error(f"Error building LLM context: {e}", exc_info=True) return self._empty_context() def _get_multi_timeframe_data_for_llm(self) -> Dict[str, pd.DataFrame]: """ Fetch data from multiple timeframes with custom limits for LLM Returns: Dict mapping timeframe to DataFrame """ data = {} for tf, count in KLINE_LIMITS.items(): df = self.data_reader.fetch_klines(interval=tf, limit=count) if not df.empty: data[tf] = df return data def _build_kline_data(self, mtf_data: Dict[str, pd.DataFrame]) -> Dict[str, List[Dict]]: """ Build K-line data for LLM analysis 不同时间周期提供不同数量的K线数据 Returns: Dict mapping timeframe to list of kline dicts """ kline_data = {} for timeframe, df in mtf_data.items(): if df.empty: continue # 获取该周期需要的K线数量 limit = KLINE_LIMITS.get(timeframe, 50) # 取最近的N根K线 df_limited = df.tail(limit) # 转换为简洁格式 klines = [] for idx, row in df_limited.iterrows(): klines.append({ 't': idx.strftime('%Y-%m-%d %H:%M'), # 时间 'o': round(row['open'], 2), # 开盘价 'h': round(row['high'], 2), # 最高价 'l': round(row['low'], 2), # 最低价 'c': round(row['close'], 2), # 收盘价 'v': round(row['volume'], 2), # 成交量 }) kline_data[timeframe] = klines return kline_data def _build_market_state( self, df: pd.DataFrame, mtf_data: Dict[str, pd.DataFrame] ) -> Dict[str, Any]: """Build market state section""" trend_info = MarketStructureAnalyzer.identify_trend(df) # Get ATR for volatility measure latest = df.iloc[-1] atr = latest.get('atr', 0) current_price = latest['close'] atr_pct = (atr / current_price * 100) if current_price > 0 else 0 # Volatility classification if atr_pct > 1.5: vol_status = f"高 (ATR=${atr:.0f}, {atr_pct:.1f}%)" elif atr_pct > 0.8: vol_status = f"中等偏高 (ATR=${atr:.0f}, {atr_pct:.1f}%)" elif atr_pct > 0.5: vol_status = f"中等 (ATR=${atr:.0f}, {atr_pct:.1f}%)" else: vol_status = f"低 (ATR=${atr:.0f}, {atr_pct:.1f}%)" # Check higher timeframe alignment htf_alignment = self._check_htf_trend_alignment(mtf_data) return { 'trend_direction': trend_info.get('direction', 'unknown'), 'trend_strength': trend_info.get('strength', 'weak'), 'market_phase': trend_info.get('phase', '未知'), 'volatility': vol_status, 'adx': trend_info.get('adx', 0), 'higher_timeframe_alignment': htf_alignment, } def _build_momentum_analysis( self, df: pd.DataFrame, depth_data: Optional[Dict[str, Any]] ) -> Dict[str, Any]: """Build momentum analysis section""" momentum = MarketStructureAnalyzer.calculate_momentum(df) # RSI status with value rsi_display = f"{momentum['rsi_status']} ({momentum['rsi']:.0f})" # Order flow analysis orderflow_summary = "数据不可用" if depth_data: imbalance = OrderFlowAnalyzer.analyze_orderbook_imbalance(depth_data) orderflow_summary = imbalance.get('summary', '数据不可用') return { 'rsi_status': rsi_display, 'rsi_value': momentum['rsi'], 'rsi_trend': momentum['rsi_trend'], 'macd': momentum['macd_signal'], 'macd_hist': momentum['macd_hist'], 'orderflow': orderflow_summary, } def _build_volatility_analysis(self, df: pd.DataFrame) -> Dict[str, Any]: """Build volatility analysis""" latest = df.iloc[-1] atr = latest.get('atr', 0) bb_width = latest.get('bb_width', 0) hist_vol = latest.get('hist_vol', 0) # Bollinger Band squeeze detection if bb_width < 0.02: bb_status = '极度收窄 (即将突破)' elif bb_width < 0.04: bb_status = '收窄' elif bb_width > 0.08: bb_status = '扩张 (高波动)' else: bb_status = '正常' return { 'atr': round(atr, 2), 'bb_width': round(bb_width, 4), 'bb_status': bb_status, 'hist_volatility': round(hist_vol, 2), } def _build_volume_analysis(self, df: pd.DataFrame) -> Dict[str, Any]: """Build volume analysis""" latest = df.iloc[-1] volume_ratio = latest.get('volume_ratio', 1) obv = latest.get('obv', 0) # Volume status if volume_ratio > 2: volume_status = '异常放量' elif volume_ratio > 1.5: volume_status = '显著放量' elif volume_ratio > 1.1: volume_status = '温和放量' elif volume_ratio < 0.5: volume_status = '显著缩量' elif volume_ratio < 0.8: volume_status = '温和缩量' else: volume_status = '正常' # OBV trend if len(df) >= 5: obv_5_ago = df.iloc[-5].get('obv', 0) obv_trend = '上升' if obv > obv_5_ago else '下降' else: obv_trend = '中性' return { 'volume_ratio': round(volume_ratio, 2), 'volume_status': volume_status, 'obv_trend': obv_trend, } def _build_mtf_summary(self, mtf_data: Dict[str, pd.DataFrame]) -> Dict[str, Any]: """Build comprehensive multi-timeframe summary with detailed indicators and quantitative scores""" mtf_summary = {} for timeframe, df in mtf_data.items(): if df.empty: continue # Add indicators df = TechnicalIndicators.add_all_indicators(df) # Get latest candle latest = df.iloc[-1] current_price = latest['close'] # Get trend trend_info = MarketStructureAnalyzer.identify_trend(df) # Get momentum momentum = MarketStructureAnalyzer.calculate_momentum(df) # Get ATR (不再计算支撑位压力位) atr = latest.get('atr', 0) atr_pct = (atr / current_price * 100) if current_price > 0 else 0 # Get volume ratio volume_ratio = latest.get('volume_ratio', 1) # ===== Calculate quantitative scores for this timeframe ===== # Build mini analysis for this timeframe (不包含支撑位压力位) mini_analysis = { 'current_price': current_price, 'trend_analysis': trend_info, 'momentum': momentum, 'support_resistance': {'support': [], 'resistance': [], 'nearest_support': None, 'nearest_resistance': None}, 'breakout': {'has_breakout': False}, 'orderflow': None, 'indicators': {'atr': atr} } # Generate quantitative signal for this timeframe try: quant_signal = QuantitativeSignalGenerator.generate_signal(mini_analysis) quant_scores = { 'composite_score': quant_signal.get('composite_score', 0), 'trend_score': quant_signal['scores'].get('trend', 0), 'momentum_score': quant_signal['scores'].get('momentum', 0), 'orderflow_score': quant_signal['scores'].get('orderflow', 0), 'breakout_score': quant_signal['scores'].get('breakout', 0), 'consensus_score': quant_signal.get('consensus_score', 0), 'signal_type': quant_signal.get('signal_type', 'HOLD'), 'confidence': quant_signal.get('confidence', 0), } except Exception as e: logger.warning(f"Failed to calculate quant scores for {timeframe}: {e}") quant_scores = { 'composite_score': 0, 'trend_score': 0, 'momentum_score': 0, 'orderflow_score': 0, 'breakout_score': 0, 'consensus_score': 0, 'signal_type': 'HOLD', 'confidence': 0, } mtf_summary[timeframe] = { # Trend 'trend_direction': trend_info.get('direction', 'unknown'), 'trend_strength': trend_info.get('strength', 'weak'), 'ema_alignment': trend_info.get('ema_alignment', 'neutral'), # Momentum 'rsi': round(momentum.get('rsi', 50), 1), 'rsi_status': momentum.get('rsi_status', 'unknown'), 'macd_signal': momentum.get('macd_signal', 'unknown'), 'macd_hist': round(momentum.get('macd_hist', 0), 2), # Volatility 'atr': round(atr, 2), 'atr_pct': round(atr_pct, 2), # Volume 'volume_ratio': round(volume_ratio, 2), # Quantitative scores 'quantitative': quant_scores, } return mtf_summary def _check_htf_trend_alignment(self, mtf_data: Dict[str, pd.DataFrame]) -> str: """ Check if higher timeframe trends are aligned Returns: Alignment status string """ trends = [] for timeframe in ['15m', '1h', '4h']: if timeframe not in mtf_data or mtf_data[timeframe].empty: continue df = TechnicalIndicators.add_all_indicators(mtf_data[timeframe]) trend_info = MarketStructureAnalyzer.identify_trend(df) trends.append(trend_info['direction']) if not trends: return '数据不足' # Count trend directions uptrend_count = trends.count('上涨') downtrend_count = trends.count('下跌') if uptrend_count == len(trends): return '完全一致看涨' elif downtrend_count == len(trends): return '完全一致看跌' elif uptrend_count > downtrend_count: return '多数看涨' elif downtrend_count > uptrend_count: return '多数看跌' else: return '分歧' def _calculate_signal_consensus( self, df: pd.DataFrame, depth_data: Optional[Dict[str, Any]] ) -> float: """ Calculate signal consensus score (0-1) Combines multiple signals to determine overall market conviction """ signals = [] # 1. Trend signal (EMA alignment) latest = df.iloc[-1] ema_20 = latest.get(f'ema_{config.EMA_FAST}', 0) ema_50 = latest.get(f'ema_{config.EMA_SLOW}', 0) if ema_20 > ema_50 * 1.01: # Bullish with buffer signals.append(1) elif ema_20 < ema_50 * 0.99: # Bearish with buffer signals.append(-1) else: signals.append(0) # 2. MACD signal macd_hist = latest.get('macd_hist', 0) if macd_hist > 0: signals.append(1) elif macd_hist < 0: signals.append(-1) else: signals.append(0) # 3. RSI signal rsi = latest.get('rsi', 50) if rsi > 55 and rsi < 70: # Bullish but not overbought signals.append(1) elif rsi < 45 and rsi > 30: # Bearish but not oversold signals.append(-1) else: signals.append(0) # Neutral or extreme # 4. ADX strength adx = latest.get('adx', 0) if adx > 25: # Strong trend # Confirm with EMA direction if ema_20 > ema_50: signals.append(1) else: signals.append(-1) else: signals.append(0) # Weak trend # 5. Order flow signal (if available) if depth_data: imbalance = OrderFlowAnalyzer.analyze_orderbook_imbalance(depth_data) imbalance_val = imbalance.get('imbalance', 0) if imbalance_val > 0.15: signals.append(1) elif imbalance_val < -0.15: signals.append(-1) else: signals.append(0) # Calculate consensus if not signals: return 0.5 # Count aligned signals positive_signals = sum(1 for s in signals if s == 1) negative_signals = sum(1 for s in signals if s == -1) total_signals = len(signals) # Consensus is the proportion of aligned signals if positive_signals > negative_signals: consensus = positive_signals / total_signals elif negative_signals > positive_signals: consensus = negative_signals / total_signals else: consensus = 0.5 # No consensus return round(consensus, 2) def _build_risk_metrics(self, df: pd.DataFrame, current_price: float) -> Dict[str, Any]: """Build risk management metrics""" latest = df.iloc[-1] atr = latest.get('atr', 0) # Calculate stop loss based on ATR stop_loss_distance = atr * config.ATR_STOP_MULTIPLIER stop_loss_pct = (stop_loss_distance / current_price * 100) if current_price > 0 else 0 # Calculate position size based on risk risk_per_trade_usd = config.ACCOUNT_SIZE_USD * config.MAX_RISK_PCT position_size_btc = risk_per_trade_usd / stop_loss_distance if stop_loss_distance > 0 else 0 position_size_usd = position_size_btc * current_price return { 'stop_loss_distance': round(stop_loss_distance, 2), 'stop_loss_pct': round(stop_loss_pct, 2), 'suggested_position_size_btc': round(position_size_btc, 4), 'suggested_position_size_usd': round(position_size_usd, 2), 'risk_reward_ratio': '1:2', # Default, can be calculated based on targets } def _empty_context(self) -> Dict[str, Any]: """Return empty context when data is unavailable""" return { 'timestamp': datetime.now().isoformat(), 'error': 'Insufficient data for analysis', 'market_state': {}, 'momentum': {}, 'signal_consensus': 0.5, } def get_simplified_context(self) -> Dict[str, Any]: """ Get simplified context matching user's example format Returns: Simplified context dict """ full_context = self.build_full_context() if 'error' in full_context: return full_context # Extract and simplify (不再包含支撑位压力位) return { 'market_state': { 'trend_direction': full_context['market_state']['trend_direction'], 'market_phase': full_context['market_state']['market_phase'], 'volatility': full_context['market_state']['volatility'], }, 'momentum': { 'rsi_status': full_context['momentum']['rsi_status'], 'macd': full_context['momentum']['macd'], 'orderflow': full_context['momentum']['orderflow'], }, 'signal_consensus': full_context['signal_consensus'], }