""" LLM Decision Maker - Use Claude/GPT for trading decisions """ import logging import json import re from typing import Dict, Any, Optional from datetime import datetime import os logger = logging.getLogger(__name__) class LLMDecisionMaker: """Generate trading decisions using LLM (Claude or OpenAI)""" def __init__(self, provider: str = 'claude', api_key: Optional[str] = None): """ Initialize LLM decision maker Args: provider: 'claude' or 'openai' api_key: API key (or use environment variable) """ self.provider = provider.lower() self.api_key = api_key or self._get_api_key() if not self.api_key: logger.warning(f"No API key found for {provider}. LLM decisions will be disabled.") self.enabled = False else: self.enabled = True self._init_client() def _get_api_key(self) -> Optional[str]: """Get API key from environment""" if self.provider == 'claude': return os.getenv('ANTHROPIC_API_KEY') elif self.provider == 'openai': return os.getenv('OPENAI_API_KEY') return None def _init_client(self): """Initialize LLM client""" try: if self.provider == 'claude': import anthropic self.client = anthropic.Anthropic(api_key=self.api_key) self.model = "claude-3-5-sonnet-20241022" elif self.provider == 'openai': import openai # Support custom base URL (for Deepseek, etc.) base_url = os.getenv('OPENAI_BASE_URL') if base_url: self.client = openai.OpenAI( api_key=self.api_key, base_url=base_url ) # Use appropriate model for the endpoint if 'deepseek' in base_url.lower(): self.model = "deepseek-chat" logger.info("Using Deepseek API endpoint") else: self.model = "gpt-4-turbo-preview" else: self.client = openai.OpenAI(api_key=self.api_key) self.model = "gpt-4-turbo-preview" logger.info(f"Initialized {self.provider} client with model {self.model}") except ImportError as e: logger.error(f"Failed to import {self.provider} library: {e}") self.enabled = False except Exception as e: logger.error(f"Failed to initialize {self.provider} client: {e}") self.enabled = False def generate_decision( self, market_context: Dict[str, Any], analysis: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """ Generate trading decision using LLM Args: market_context: LLM context from LLMContextBuilder analysis: Optional full analysis for additional details Returns: Decision dict with signal, reasoning, and levels """ if not self.enabled: return self._disabled_response() try: # Build prompt prompt = self._build_prompt(market_context, analysis) # Log the complete prompt being sent to LLM logger.info("=" * 80) logger.info("📤 完整的 LLM 提示词 (发送给 Deepseek):") logger.info("=" * 80) logger.info(prompt) logger.info("=" * 80) # Call LLM response_text = self._call_llm(prompt) # Log the LLM response logger.info("=" * 80) logger.info("📥 LLM 原始响应:") logger.info("=" * 80) logger.info(response_text) logger.info("=" * 80) # Parse response decision = self._parse_response(response_text, market_context) logger.info( f"LLM decision: {decision['signal_type']} " f"(confidence: {decision.get('confidence', 0):.2f})" ) return decision except Exception as e: logger.error(f"Error generating LLM decision: {e}", exc_info=True) logger.debug(f"Market context: {market_context}") return self._error_response(str(e)) def _build_prompt( self, market_context: Dict[str, Any], analysis: Optional[Dict[str, Any]] ) -> str: """Build trading decision prompt""" # Extract context elements market_state = market_context.get('market_state', {}) key_prices = market_context.get('key_prices', {}) momentum = market_context.get('momentum', {}) signal_consensus = market_context.get('signal_consensus', 0.5) current_price = market_context.get('current_price', 0) # Build structured prompt prompt = f"""你是一个专业的加密货币交易分析师。基于以下多时间周期市场分析数据,提供分层次的交易建议。 ## 当前价格 ${current_price:,.2f} ## 请提供以下内容 (使用JSON格式): {{ "signal": "BUY" | "SELL" | "HOLD", "confidence": 0.0-1.0, // 分时间级别的交易机会分析 "opportunities": {{ "short_term_5m_15m_1h": {{ "exists": true/false, "timeframe_label": "短期 (5m/15m/1h)", "direction": "LONG" | "SHORT" | null, "entry_price": 进场价格数值或null, "stop_loss": 止损价格数值或null, "take_profit": 止盈价格数值或null, "reasoning": "短期日内机会说明", "持仓时间": "几分钟到几小时" }}, "medium_term_4h_1d": {{ "exists": true/false, "timeframe_label": "中期 (4h/1d)", "direction": "LONG" | "SHORT" | null, "entry_price": 进场价格数值或null, "stop_loss": 止损价格数值或null, "take_profit": 止盈价格数值或null, "reasoning": "中期波段机会说明", "持仓时间": "数天到一周" }}, "long_term_1d_1w": {{ "exists": true/false, "timeframe_label": "长期 (1d/1w)", "direction": "LONG" | "SHORT" | null, "entry_price": 进场价格数值或null, "stop_loss": 止损价格数值或null, "take_profit": 止盈价格数值或null, "reasoning": "长期趋势机会说明", "持仓时间": "数周到数月" }}, "ambush": {{ "exists": true/false, "price_level": 埋伏价格数值或null, "reasoning": "埋伏点位说明 (等待回调/反弹到关键位)", "timeframe": "基于哪个时间级别的关键位" }} }}, // 分级别操作建议(必填,即使某级别无机会也要说明原因) "recommendations_by_timeframe": {{ "short_term": "短期(5m/15m/1h)操作建议", "medium_term": "中期(4h/1d)操作建议", "long_term": "长期(1d/1w)操作建议" }}, // 综合分析 "reasoning": "多周期综合分析 (3-5句话,说明各周期是否一致)", "risk_level": "LOW" | "MEDIUM" | "HIGH", "key_factors": ["影响因素1", "影响因素2", ...] }} **输出说明**: 1. **signal**: 主要交易信号 (BUY/SELL/HOLD) 2. **confidence**: 对主要信号的信心度 (0-1) 3. **opportunities**: 分时间级别详细分析 - **short_term_5m_15m_1h**: 短期日内交易机会 (持仓几分钟到几小时) - 基于5m/15m/1h周期共振 - 止损: 5m/15m ATR × 1.5, 通常0.3%-0.5% - 止盈: 1h压力/支撑位, 风险回报比≥1:2 - **medium_term_4h_1d**: 中期波段交易机会 (持仓数天到一周) - 基于4h/1d周期趋势 - 止损: 4h ATR × 1.5, 通常1%-2% - 止盈: 日线关键位, 风险回报比≥1:2.5 - **long_term_1d_1w**: 长期趋势交易机会 (持仓数周到数月) - 基于1d/1w周期趋势 - 止损: 日线ATR × 1.5, 通常2%-4% - 止盈: 周线关键位, 风险回报比≥1:3 - **ambush**: 埋伏点位机会 - 基于日线/周线关键支撑压力位 - 等待价格到达后再决定入场 4. **recommendations_by_timeframe**: 各级别操作建议(必填) - 即使某级别无明确机会,也要说明原因和观望理由 5. **reasoning**: 多周期综合分析,说明各周期是否一致,存在哪些分歧 **重要原则**: 1. **平等对待所有时间级别** - 不要偏向任何周期,根据量化评分客观分析 2. **可以同时存在多级别机会** - 例如: 短期做多(日内) + 中期观望 + 长期做空(趋势) 3. **各级别独立分析** - 短期、中期、长期分别给出建议,不要混淆 4. **必须填写recommendations_by_timeframe** - 即使是HOLD也要说明理由 5. **止损止盈必须匹配时间级别** - 短期用小止损,长期用大止损 6. **响应必须是有效的JSON格式** - 不要包含注释 """ # Add comprehensive multi-timeframe analysis if available if 'multi_timeframe' in market_context: mtf = market_context['multi_timeframe'] prompt += f"\n## 多时间框架技术分析 (完整指标)\n\n" # Define timeframe order and display names tf_order = [ ('5m', '5分钟'), ('15m', '15分钟'), ('1h', '1小时'), ('4h', '4小时'), ('1d', '日线'), ('1w', '周线') ] for tf_key, tf_name in tf_order: if tf_key not in mtf: continue data = mtf[tf_key] quant = data.get('quantitative', {}) prompt += f"### {tf_name}周期 ({tf_key})\n" # ===== NEW: 量化评分优先展示 ===== prompt += f"**量化评分**: {quant.get('composite_score', 0):.1f} (信号: {quant.get('signal_type', 'HOLD')}, 置信度: {quant.get('confidence', 0):.0%})\n" prompt += f"- 趋势得分: {quant.get('trend_score', 0):.1f} | 动量得分: {quant.get('momentum_score', 0):.1f} | 订单流: {quant.get('orderflow_score', 0):.1f}\n" # 原有技术指标 prompt += f"- 趋势: {data.get('trend_direction', '未知')} (强度: {data.get('trend_strength', 'weak')})\n" prompt += f"- RSI: {data.get('rsi', 50):.1f} ({data.get('rsi_status', '中性')})\n" prompt += f"- MACD: {data.get('macd_signal', '未知')} (柱状图: {data.get('macd_hist', 0):.2f})\n" # Support/Resistance support = data.get('support') resistance = data.get('resistance') support_str = f"${support:,.0f}" if support else "无" resistance_str = f"${resistance:,.0f}" if resistance else "无" prompt += f"- 支撑位: {support_str} | 压力位: {resistance_str}\n" # Volatility atr = data.get('atr', 0) atr_pct = data.get('atr_pct', 0) prompt += f"- 波动率: ATR ${atr:.2f} ({atr_pct:.2f}%)\n" # Volume vol_ratio = data.get('volume_ratio', 1) vol_status = "放量" if vol_ratio > 1.2 else "缩量" if vol_ratio < 0.8 else "正常" prompt += f"- 成交量: {vol_status} (比率: {vol_ratio:.2f}x)\n" prompt += "\n" # Add cross-timeframe analysis insights prompt += "### 多周期分析方法\n\n" prompt += "#### 📊 分时间级别交易框架\n\n" prompt += "**1️⃣ 短期交易 (short_term_5m_15m_1h)** - 持仓: 几分钟到几小时\n\n" prompt += "判断标准:\n" prompt += "- ✅ **短周期共振**: 5m/15m/1h趋势方向一致\n" prompt += "- ✅ **动量确认**: 5m MACD金叉/死叉 + 15m MACD同向\n" prompt += "- ✅ **RSI信号**: 5m/15m RSI从超卖(<30)反弹或超买(>70)回落\n" prompt += "- ✅ **价格位置**: 触及1h或4h支撑/压力位后反弹\n" prompt += "- ⚠️ **大趋势**: 日线/周线至少不强烈相反\n" prompt += "- ✅ **成交量**: 5m/15m放量确认突破/反转\n\n" prompt += "入场条件:\n" prompt += "- 做多: 5m/15m/1h上涨 + 5m金叉 + 价格>1h支撑 + 放量\n" prompt += "- 做空: 5m/15m/1h下跌 + 5m死叉 + 价格<1h压力 + 放量\n\n" prompt += "止盈止损:\n" prompt += "- 止损: 5m ATR × 1.5 或15m最近低/高点, 约0.3%-0.5%\n" prompt += "- 止盈: 1h压力/支撑位, 风险回报比≥1:2\n" prompt += "- 策略: 快进快出, 达成50%目标后移动止损到成本\n\n" prompt += "**2️⃣ 中期交易 (medium_term_4h_1d)** - 持仓: 数天到一周\n\n" prompt += "判断标准:\n" prompt += "- ✅ **中周期趋势**: 4h/1d方向一致且趋势明显\n" prompt += "- ✅ **量化评分**: 4h和1d的量化综合得分方向一致\n" prompt += "- ✅ **MACD共振**: 日线金叉/死叉 + 周线趋势确认\n" prompt += "- ✅ **关键位突破**: 突破或回踩日线/周线支撑压力位\n" prompt += "- ✅ **RSI位置**: 日线RSI从超卖(<30)反转或超买(>70)回落\n" prompt += "- ✅ **入场时机**: 4h/1h回调到位,提供更好入场点\n" prompt += "- ✅ **成交量**: 日线放量突破确认趋势\n\n" prompt += "入场条件:\n" prompt += "- 做多: 日线+周线上涨 + 日线金叉 + 4h回调到日线支撑 + 1h反弹\n" prompt += "- 做空: 日线+周线下跌 + 日线死叉 + 4h反弹到日线压力 + 1h回落\n\n" prompt += "止盈止损:\n" prompt += "- 止损: 4h ATR × 1.5, 约1%-2%\n" prompt += "- 止盈: 日线关键位, 风险回报比≥1:2.5\n" prompt += "- 策略: 波段持仓,关注日线趋势变化\n\n" prompt += "**3️⃣ 长期交易 (long_term_1d_1w)** - 持仓: 数周到数月\n\n" prompt += "判断标准:\n" prompt += "- ✅ **大周期趋势**: 1d/1w方向一致且强劲(strong/moderate)\n" prompt += "- ✅ **量化评分**: 日线和周线的量化综合得分方向一致且分值高\n" prompt += "- ✅ **周线MACD**: 周线金叉/死叉确认趋势\n" prompt += "- ✅ **关键位突破**: 突破周线/月线级别支撑压力位\n" prompt += "- ✅ **趋势确认**: 多个大周期指标共振,形成明确趋势\n\n" prompt += "入场条件:\n" prompt += "- 做多: 日线+周线上涨 + 周线金叉 + 日线回调到周线支撑 + 4h反弹\n" prompt += "- 做空: 日线+周线下跌 + 周线死叉 + 日线反弹到周线压力 + 4h回落\n\n" prompt += "止盈止损:\n" prompt += "- 止损: 日线ATR × 1.5, 约2%-4%\n" prompt += "- 止盈: 周线压力/支撑位, 风险回报比≥1:3\n" prompt += "- 策略: 长期持仓,趋势不破不出,移动止损锁定利润\n\n" prompt += "**4️⃣ 埋伏点位 (ambush)** - 提前布局等待机会\n\n" prompt += "适用场景:\n" prompt += "- 📌 **当前位置不佳**: 价格处于中间位置,没有好的入场点\n" prompt += "- 📌 **关键位等待**: 有明确的日线/周线支撑压力位可等待\n" prompt += "- 📌 **趋势延续**: 大周期趋势明确,等待回调/反弹入场\n" prompt += "- 📌 **反转布局**: 价格接近关键转折点,等待突破确认\n\n" prompt += "埋伏位置示例:\n" prompt += "- 做多埋伏: 等待回调到周线/日线支撑位 (例: 价格90500,埋伏88900)\n" prompt += "- 做空埋伏: 等待反弹到周线/日线压力位 (例: 价格90500,埋伏93000)\n" prompt += "- 突破埋伏: 等待突破关键位后回踩 (例: 突破91000后回踩90800)\n\n" prompt += "埋伏策略:\n" prompt += "- 基于: 日线/周线的关键支撑压力位\n" prompt += "- 触发: 价格到达埋伏位 + 短周期(1h/4h)出现反转信号\n" prompt += "- 止损: 埋伏位下方/上方1-2个ATR\n" prompt += "- 止盈: 下一个日线/周线关键位\n\n" prompt += "**5️⃣ 观望情况** - recommendations_by_timeframe中标注\n" prompt += "- ❌ 某周期趋势不明确或震荡\n" prompt += "- ❌ 量化评分接近0 (无明确方向)\n" prompt += "- ❌ 多个周期趋势严重分歧\n" prompt += "- ❌ 成交量萎缩,市场缺乏动能\n" prompt += "- ❌ 价格在关键位之间震荡\n\n" prompt += "#### 🎯 关键分析要点\n" prompt += "1. **平等对待各周期** - 周线、日线、小时级别都重要,根据持仓时间选择\n" prompt += "2. **利用量化评分** - 每个周期都有量化综合得分,优先参考这个数值\n" prompt += "3. **分级别独立分析** - 短期、中期、长期可以有不同甚至相反的建议\n" prompt += "4. **趋势共振**: 同级别内多周期一致时,信号最强\n" prompt += "5. **分歧利用**: 短期看多+长期看空 = 日内做多但不持仓过夜\n" prompt += "6. **必须填写所有级别建议** - recommendations_by_timeframe三个字段都要填\n\n" return prompt def _call_llm(self, prompt: str) -> str: """Call LLM API""" if self.provider == 'claude': return self._call_claude(prompt) elif self.provider == 'openai': return self._call_openai(prompt) else: raise ValueError(f"Unsupported provider: {self.provider}") def _call_claude(self, prompt: str) -> str: """Call Claude API""" try: response = self.client.messages.create( model=self.model, max_tokens=1500, temperature=0.7, messages=[ {"role": "user", "content": prompt} ] ) return response.content[0].text except Exception as e: logger.error(f"Claude API error: {e}") raise def _call_openai(self, prompt: str) -> str: """Call OpenAI API""" try: response = self.client.chat.completions.create( model=self.model, messages=[ { "role": "system", "content": "You are a professional cryptocurrency trading analyst. Provide trading advice in JSON format." }, {"role": "user", "content": prompt} ], max_tokens=1500, temperature=0.7, ) return response.choices[0].message.content except Exception as e: logger.error(f"OpenAI API error: {e}") raise def _parse_response( self, response_text: str, market_context: Dict[str, Any] ) -> Dict[str, Any]: """Parse LLM response into structured decision""" # Try to extract JSON from response json_match = re.search(r'\{[\s\S]*\}', response_text) if not json_match: logger.warning("No JSON found in LLM response, using fallback parsing") return self._fallback_parse(response_text, market_context) try: llm_decision = json.loads(json_match.group()) logger.debug(f"Parsed LLM JSON: {llm_decision}") except json.JSONDecodeError as e: logger.warning(f"Failed to parse JSON: {e}, using fallback") logger.debug(f"JSON match was: {json_match.group()[:500]}") return self._fallback_parse(response_text, market_context) # Helper function to safely convert to float def safe_float(value, default=0.0): """Safely convert value to float, handling None and invalid values""" if value is None: return default try: return float(value) except (ValueError, TypeError): return default # Parse opportunities structure (support both old and new format) opportunities = llm_decision.get('opportunities', {}) # Try new format first short_term = opportunities.get('short_term_5m_15m_1h', {}) medium_term = opportunities.get('medium_term_4h_1d', {}) long_term = opportunities.get('long_term_1d_1w', {}) ambush = opportunities.get('ambush', {}) # Fallback to old format for backward compatibility if not short_term and not medium_term and not long_term: intraday = opportunities.get('intraday', {}) swing = opportunities.get('swing', {}) # Map old format to new format short_term = intraday medium_term = swing long_term = {} # Determine primary levels (priority: short > medium > long) entry = market_context.get('current_price', 0) stop_loss = 0 take_profit = 0 if short_term.get('exists'): entry = safe_float(short_term.get('entry_price'), market_context.get('current_price', 0)) stop_loss = safe_float(short_term.get('stop_loss'), 0) take_profit = safe_float(short_term.get('take_profit'), 0) elif medium_term.get('exists'): entry = safe_float(medium_term.get('entry_price'), market_context.get('current_price', 0)) stop_loss = safe_float(medium_term.get('stop_loss'), 0) take_profit = safe_float(medium_term.get('take_profit'), 0) elif long_term.get('exists'): entry = safe_float(long_term.get('entry_price'), market_context.get('current_price', 0)) stop_loss = safe_float(long_term.get('stop_loss'), 0) take_profit = safe_float(long_term.get('take_profit'), 0) # Get recommendations by timeframe recommendations = llm_decision.get('recommendations_by_timeframe', {}) # Validate and structure decision decision = { 'timestamp': datetime.now().isoformat(), 'signal_type': llm_decision.get('signal', 'HOLD').upper(), 'confidence': safe_float(llm_decision.get('confidence'), 0.5), 'trade_type': 'MULTI_TIMEFRAME', # New format uses multiple timeframes 'reasoning': llm_decision.get('reasoning', ''), # New opportunities breakdown (multi-timeframe) 'opportunities': { 'short_term_5m_15m_1h': { 'exists': short_term.get('exists', False), 'direction': short_term.get('direction'), 'entry_price': safe_float(short_term.get('entry_price'), 0), 'stop_loss': safe_float(short_term.get('stop_loss'), 0), 'take_profit': safe_float(short_term.get('take_profit'), 0), 'reasoning': short_term.get('reasoning', '') }, 'medium_term_4h_1d': { 'exists': medium_term.get('exists', False), 'direction': medium_term.get('direction'), 'entry_price': safe_float(medium_term.get('entry_price'), 0), 'stop_loss': safe_float(medium_term.get('stop_loss'), 0), 'take_profit': safe_float(medium_term.get('take_profit'), 0), 'reasoning': medium_term.get('reasoning', '') }, 'long_term_1d_1w': { 'exists': long_term.get('exists', False), 'direction': long_term.get('direction'), 'entry_price': safe_float(long_term.get('entry_price'), 0), 'stop_loss': safe_float(long_term.get('stop_loss'), 0), 'take_profit': safe_float(long_term.get('take_profit'), 0), 'reasoning': long_term.get('reasoning', '') }, 'ambush': { 'exists': ambush.get('exists', False), 'price_level': safe_float(ambush.get('price_level'), 0), 'reasoning': ambush.get('reasoning', '') }, # Keep old format for backward compatibility 'intraday': { 'exists': short_term.get('exists', False), 'direction': short_term.get('direction'), 'entry_price': safe_float(short_term.get('entry_price'), 0), 'stop_loss': safe_float(short_term.get('stop_loss'), 0), 'take_profit': safe_float(short_term.get('take_profit'), 0), 'reasoning': short_term.get('reasoning', '') }, 'swing': { 'exists': medium_term.get('exists', False) or long_term.get('exists', False), 'direction': medium_term.get('direction') or long_term.get('direction'), 'entry_price': safe_float(medium_term.get('entry_price') or long_term.get('entry_price'), 0), 'stop_loss': safe_float(medium_term.get('stop_loss') or long_term.get('stop_loss'), 0), 'take_profit': safe_float(medium_term.get('take_profit') or long_term.get('take_profit'), 0), 'reasoning': medium_term.get('reasoning', '') or long_term.get('reasoning', '') }, }, # Recommendations by timeframe 'recommendations_by_timeframe': { 'short_term': recommendations.get('short_term', ''), 'medium_term': recommendations.get('medium_term', ''), 'long_term': recommendations.get('long_term', '') }, # Primary levels (for backward compatibility) 'levels': { 'current_price': market_context.get('current_price', 0), 'entry': entry, 'stop_loss': stop_loss, 'take_profit_1': take_profit, 'take_profit_2': take_profit, 'take_profit_3': take_profit, }, 'risk_level': llm_decision.get('risk_level', 'MEDIUM'), 'key_factors': llm_decision.get('key_factors', []), 'raw_response': response_text, } # Calculate risk-reward ratio entry = decision['levels']['entry'] stop_loss = decision['levels']['stop_loss'] tp1 = decision['levels']['take_profit_1'] if entry and stop_loss and tp1 and entry != stop_loss: risk = abs(entry - stop_loss) reward = abs(tp1 - entry) decision['risk_reward_ratio'] = round(reward / risk, 2) if risk > 0 else 0 else: decision['risk_reward_ratio'] = 0 return decision def _fallback_parse( self, response_text: str, market_context: Dict[str, Any] ) -> Dict[str, Any]: """Fallback parsing when JSON extraction fails""" # Simple keyword-based signal extraction text_lower = response_text.lower() if 'buy' in text_lower or '买入' in response_text or '做多' in response_text: signal_type = 'BUY' confidence = 0.6 elif 'sell' in text_lower or '卖出' in response_text or '做空' in response_text: signal_type = 'SELL' confidence = 0.6 else: signal_type = 'HOLD' confidence = 0.5 return { 'timestamp': datetime.now().isoformat(), 'signal_type': signal_type, 'confidence': confidence, 'reasoning': response_text[:500], # First 500 chars 'levels': { 'current_price': market_context.get('current_price', 0), 'entry': 0, 'stop_loss': 0, 'take_profit_1': 0, 'take_profit_2': 0, 'take_profit_3': 0, }, 'risk_level': 'MEDIUM', 'time_horizon': 'MEDIUM', 'key_factors': [], 'raw_response': response_text, 'warning': 'Fallback parsing used - levels not available', } def _disabled_response(self) -> Dict[str, Any]: """Return response when LLM is disabled""" return { 'timestamp': datetime.now().isoformat(), 'signal_type': 'HOLD', 'confidence': 0, 'reasoning': 'LLM decision maker is disabled (no API key)', 'enabled': False, } def _error_response(self, error_msg: str) -> Dict[str, Any]: """Return error response""" return { 'timestamp': datetime.now().isoformat(), 'signal_type': 'HOLD', 'confidence': 0, 'reasoning': f'Error generating decision: {error_msg}', 'error': error_msg, }