tradusai/analysis/llm_context.py
2025-12-04 01:27:58 +08:00

527 lines
18 KiB
Python

"""
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'],
}