648 lines
29 KiB
Python
648 lines
29 KiB
Python
"""
|
||
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,
|
||
}
|