527 lines
18 KiB
Python
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'],
|
|
}
|