tradusai/signals/llm_decision.py
2025-12-02 22:54:03 +08:00

648 lines
29 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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