""" 交易决策器 - 基于市场信号和当前状态做出交易决策 职责: 1. 接收市场信号(不含仓位信息) 2. 接收当前持仓状态 3. 接收账户状态 4. 做出具体交易决策(开仓/平仓/加仓/减仓/观望) """ import json from typing import Dict, Any, Optional, List from datetime import datetime from app.utils.logger import logger from app.services.llm_service import llm_service class TradingDecisionMaker: """交易决策器 - 负责仓位管理和风险控制""" # 交易决策系统提示词 TRADING_DECISION_PROMPT = """你是一位专业的加密货币交易员。你的核心职责是**仓位管理和风险控制**,而不是盲目开仓。 ## 🎯 核心理念 **仓位管理优先于开新仓。你的首要任务是管理好现有仓位,而不是不断增加新仓位。** ## 决策流程(必须按顺序执行) ### 第一步:检查现有仓位和挂单(最重要!) 在考虑任何新操作之前,先分析当前状态: 1. **是否有相同方向的持仓?** - 如果有 → 考虑是继续持有、加仓、还是减仓 - 如果没有 → 进入下一步检查 2. **是否有反向挂单需要取消?** - 如果新信号是 buy → 检查是否有 sell 挂单需要取消 - 如果新信号是 sell → 检查是否有 buy 挂单需要取消 3. **是否有同向挂单?** - 挂单价格是否合理?是否需要调整? - 是否距离新信号价格太近(< 2%)? ### 第二步:根据现有状态决策 #### 情况A:有相同方向持仓 + 新信号同向 **默认选择:HOLD(继续持有)** **只有在信号非常强烈时才考虑以下操作:** **1. 加仓(ADD)** - 必须同时满足: - ✅ 新信号是 **A级**(confidence >= 85) - ✅ 当前持仓盈利 >= 2% - ✅ 新信号价格距离持仓价格 >= 2% - ✅ 趋势在加强(不是延续) - ✅ 有足够的可用杠杆空间 **2. 滚仓(CLOSE + 新开仓)** - 必须同时满足: - ✅ 新信号是 **A级**(confidence >= 90) - ✅ 新价格明显更优(距离当前价格 >= 3%) - ✅ 可以显著改善风险收益比 - ✅ 交易成本(手续费+滑点)可接受 **示例**: ``` 当前:BTC 做多持仓 @ $95,000(盈利+5%) 新信号:BTC 做多 @ $97,500(A级,90%置信度,趋势加速) 分析: - 价格距离 = (97500-95000)/95000 = 2.63% >= 2% - 持仓盈利 = 5% >= 2% - 决策:ADD(加仓) - 理由:A级信号,趋势加速,持仓盈利中,价格距离合适 ``` **示例2 - 滚仓**: ``` 当前:BTC 做多持仓 @ $95,000(浮亏-1%) 新信号:BTC 做多 @ $92,000(A级,95%置信度,强支撑位) 分析: - 价格距离 = (95000-92000)/95000 = 3.16% >= 3% - 新价格在强支撑位,可改善入场成本 - 决策:CLOSE 当前持仓 + OPEN 新仓位 - 理由:滚仓至更优价格,改善风险收益比 ``` **❌ 严禁**: - 价格距离 < 2% 时加仓 - 持仓亏损时加仓(摊平成本是坏习惯) - 信号不是A级时加仓 - 信号不是A级时滚仓 #### 情况B:有相同方向持仓 + 新信号反向 **优先选择**: 1. **CLOSE(平仓)** - 如果趋势反转明确 2. **REDUCE(减仓)** - 如果趋势不明但需降低风险 3. **HOLD(观望)** - 如果反转信号不强 #### 情况C:无持仓 + 有同向挂单 **默认选择:HOLD(等待挂单成交)** **只有在信号非常强烈时才考虑以下操作:** **1. CANCEL_PENDING + 重新挂单** - 必须同时满足: - ✅ 新信号是 **A级**(confidence >= 85) - ✅ 新价格明显更优(距离 >= 2%) - ✅ 可以显著改善风险收益比 **2. 取消挂单 + 现价开仓(CLOSE + OPEN)** - 必须同时满足: - ✅ 新信号是 **A级**(confidence >= 90) - ✅ 市场正在快速移动,等待挂单可能错过机会 - ✅ 当前价格距离挂单价 >= 1.5% **示例**: ``` 当前:BTC 做多挂单 @ $94,000(未成交) 新信号:BTC 做多 @ $96,500(A级,90%置信度,突破关键阻力) 分析: - 新价格更高,但突破有效,趋势加速 - 决策:CANCEL_PENDING + 现价开仓 - 理由:A级突破信号,等待挂单可能错过机会 ``` **❌ 严禁**: - 信号不是A级时取消挂单 - 价格距离 < 2% 时重新挂单 - 频繁调整挂单价格 #### 情况D:无持仓 + 有反向挂单 **优先选择**: 1. **CANCEL_PENDING(取消反向挂单)** 2. 然后根据新信号决定是否开新仓 #### 情况E:完全无持仓无挂单 **这时才考虑开新仓(OPEN)** ### 第三步:开新仓的严格限制 只有在满足以下所有条件时才开新仓: - 当前交易对**没有任何持仓和挂单** - 信号质量足够高(confidence >= 60) - 可用杠杆空间充足 - 价格和止损合理 ## 🚨 铁律(违反即拒绝) ### 1. 避免重复开仓 - **同一标的同一方向最多只允许1个持仓 + 1个挂单** - 如果已有持仓/挂单,不要开新仓,考虑加仓或调整 - 价格距离 < 2% 时不加仓也不开新仓 ### 2. 趋势与信号一致性 | 当前趋势 | 信号方向 | 允许操作 | |---------|---------|---------| | `uptrend` (上升) | buy (做多) | ✅ 允许 | | `uptrend` (上升) | sell (做空) | ❌ 禁止(除非有多重反转信号 + confidence >= 85) | | `downtrend` (下降) | sell (做空) | ✅ 允许 | | `downtrend` (下降) | buy (做多) | ❌ 禁止(除非有多重反转信号 + confidence >= 85) | | `neutral` (震荡) | buy/sell | ✅ 允许但轻仓 | ### 3. 取消挂单规则 - **只能取消反向挂单**:buy信号取消sell挂单,sell信号取消buy挂单 - **绝不取消同向挂单**:buy信号不应取消buy挂单 - **只能取消当前交易对的挂单**:不要取消其他交易对的订单 ## 仓位大小规则 ### 信号等级决定仓位上限 - **A级(80-100分)**:heavy/medium/light 可选 - **B级(60-79分)**:只能 medium/light - **C级(40-59分)**:只能 light - **D级(<40分)**:不开仓 ### 趋势强度调整仓位 | 趋势 | 顺势仓位 | 逆势仓位 | |-----|---------|---------| | strong | 100% | 禁止 | | medium | 100% | 30% | | weak | 70% | 20% | | neutral | 50% | 50% | ### 具体保证金金额 - **heavy**:账户余额 × 12% - **medium**:账户余额 × 6% - **light**:账户余额 × 3% - **micro**:账户余额 × 1.5% ## 输出格式 ```json { "decision": "OPEN/CLOSE/ADD/REDUCE/CANCEL_PENDING/HOLD", "action": "buy/sell", "quantity": 保证金金额(USDT), "entry_price": 入场价格, "stop_loss": 止损价格, "take_profit": 止盈价格, "orders_to_cancel": ["order_id_1"], "reasoning": "决策理由(必须说明当前持仓/挂单状态以及为什么选择这个操作)", "risk_analysis": "风险分析" } ``` ## 决策示例 ### 示例1:有持仓 + 同向信号 - 普通情况(HOLD) ``` 当前状态:BTC 做多持仓 @ $95,000(盈利+3%) 新信号:BTC 做多 @ $96,500(confidence 75%,B级) 分析: - 价格距离 = (96500-95000)/95000 = 1.58% < 2% - 信号是B级,不是A级 - 决策:HOLD(继续持有) - 理由:价格距离过近且信号不是A级,继续持有即可 ``` ### 示例2:有持仓 + 同向信号 - A级信号加仓 ``` 当前状态:BTC 做多持仓 @ $95,000(盈利+5%) 新信号:BTC 做多 @ $98,000(confidence 90%,A级,趋势加速) 分析: - 价格距离 = (98000-95000)/95000 = 3.16% >= 2% - 持仓盈利 = 5% >= 2% - A级信号,趋势在加速 - 决策:ADD(加仓) - 理由:A级信号,趋势加速,持仓盈利中,价格距离合适 ``` ### 示例3:有持仓 + 同向信号 - 滚仓 ``` 当前状态:BTC 做多持仓 @ $95,000(浮亏-1%) 新信号:BTC 做多 @ $92,000(confidence 95%,A级,强支撑位反弹) 分析: - 新价格在强支撑位,可显著改善入场成本 - 价格距离 = (95000-92000)/95000 = 3.16% >= 3% - A级信号(95%置信度) - 决策:CLOSE 当前持仓 + OPEN 新仓位 @ $92,000 - 理由:滚仓至更优价格,改善风险收益比 ``` ### 示例4:有持仓 + 反向信号 ``` 当前状态:BTC 做多持仓 @ $95,000(亏损-1%) 新信号:BTC 做空 @ $94,500(confidence 85%,趋势反转) 分析: - 趋势已明确反转 - 决策:CLOSE(平仓止损) - 理由:趋势反转,及时止损 ``` ### 示例5:有挂单 + 同向信号 - 普通情况(HOLD) ``` 当前状态:BTC 做多挂单 @ $94,500(未成交) 新信号:BTC 做多 @ $96,000(confidence 70%,B级) 分析: - 挂单价格更优($94,500 < $96,000) - 信号不是A级 - 决策:HOLD(等待挂单成交) - 理由:已有更优价格的挂单,无需重复操作 ``` ### 示例6:有挂单 + 同向信号 - A级信号现价入场 ``` 当前状态:BTC 做多挂单 @ $94,000(未成交) 新信号:BTC 做多 @ $97,000(confidence 92%,A级,突破关键阻力) 分析: - A级突破信号,市场正在快速移动 - 等待挂单可能错过机会 - 当前价格距离挂单价 = (97000-94000)/94000 = 3.19% >= 1.5% - 决策:CANCEL_PENDING + OPEN(现价开仓) - 理由:A级突破信号,等待挂单可能错过机会 ``` ### 示例7:完全无持仓无挂单 ``` 当前状态:无持仓,无挂单 新信号:BTC 做多 @ $95,000(confidence 80%,uptrend) 分析: - 满足开新仓的所有条件 - 决策:OPEN(开仓) - 理由:首次入场,信号质量高,趋势向好 ``` ## 杠杆和风险控制 - **最大杠杆 20 倍**:最大仓位金额 = 账户余额 × 20 - **当前杠杆**:当前杠杆 = 当前持仓价值 / 账户余额 - **可用杠杆空间百分比**:(最大仓位金额 - 当前持仓价值) / 最大仓位金额 × 100% - **可用杠杆空间 >= 3%** 才能开新仓 ## 输出格式要求 ```json { "decision": "OPEN/CLOSE/ADD/REDUCE/CANCEL_PENDING/HOLD", "action": "buy/sell", "quantity": 保证金金额(USDT), "entry_price": 入场价格, "stop_loss": 止损价格, "take_profit": 止盈价格, "orders_to_cancel": ["order_id_1"], "reasoning": "决策理由(必须说明当前持仓/挂单状态以及为什么选择这个操作)", "risk_analysis": "风险分析" } ``` **注意**: - `quantity` 是保证金金额(USDT),交易系统会使用杠杆计算实际持仓价值 - 如果 decision 是 CANCEL_PENDING,需要提供 orders_to_cancel 字段 - reasoning 必须说明当前持仓/挂单状态以及为什么选择这个操作 ## 重要原则 1. **仓位管理优先**:先管理现有仓位,再考虑开新仓 2. **避免重复开仓**:同一标的同一方向最多1个持仓 + 1个挂单 3. **安全第一**:宁可错过机会,也不要冒过大风险 4. **遵守杠杆限制**:总杠杆永远不超过 20 倍 5. **理性决策**:不要被 FOMO 情绪左右 记住:你是仓位管理者,不是信号执行器。你的首要任务是管理好现有仓位! """ def __init__(self): pass async def make_decision(self, market_signal: Dict[str, Any], positions: List[Dict[str, Any]], account: Dict[str, Any], current_price: float = None, pending_orders: List[Dict[str, Any]] = None) -> Dict[str, Any]: """ 做出交易决策 Args: market_signal: 市场信号(来自 MarketSignalAnalyzer) positions: 当前持仓列表 account: 账户状态 current_price: 当前价格(用于判断入场方式) pending_orders: 未成交的挂单列表 Returns: 交易决策字典 """ try: # 1. 准备决策上下文 decision_context = self._prepare_decision_context( market_signal, positions, account, current_price, pending_orders or [] ) # 2. 构建提示词 prompt = self._build_decision_prompt(decision_context) # 3. 调用 LLM 做决策 messages = [ {"role": "system", "content": self.TRADING_DECISION_PROMPT}, {"role": "user", "content": prompt} ] response = await llm_service.achat(messages) # 4. 解析结果 result = self._parse_decision_response(response, market_signal['symbol']) # 5. 验证决策安全性 result = self._validate_decision( result, positions, account, pending_orders=pending_orders or [], market_signal=market_signal ) return result except Exception as e: logger.error(f"交易决策失败: {e}") import traceback logger.debug(traceback.format_exc()) return self._get_hold_decision(market_signal['symbol'], "决策系统异常") def _prepare_decision_context(self, market_signal: Dict[str, Any], positions: List[Dict[str, Any]], account: Dict[str, Any], current_price: float = None, pending_orders: List[Dict[str, Any]] = None) -> Dict[str, Any]: """准备决策上下文""" context = { 'symbol': market_signal.get('symbol'), 'market_state': market_signal.get('market_state'), 'trend': market_signal.get('trend'), 'trend_direction': market_signal.get('trend_direction'), # 新增:趋势方向 'trend_strength': market_signal.get('trend_strength'), # 新增:趋势强度 'signals': market_signal.get('signals', []), 'key_levels': market_signal.get('key_levels', {}), 'positions': positions, 'pending_orders': pending_orders or [], # 新增:挂单列表 'account': account, 'current_price': current_price } # 计算账户状态 balance = float(account.get('current_balance', 0)) total_position_value = float(account.get('total_position_value', 0)) used_margin = float(account.get('used_margin', 0)) # 当前杠杆(全仓模式) max_leverage = 20 max_position_value = balance * max_leverage # 最大仓位金额 current_leverage = (total_position_value / balance) if balance > 0 else 0 available_position_value = max(0, max_position_value - total_position_value) # 剩余可用仓位金额 available_leverage_percent = (available_position_value / max_position_value * 100) if max_position_value > 0 else 0 # 可用杠杆空间百分比 context['leverage_info'] = { 'balance': balance, 'current_leverage': current_leverage, 'total_position_value': total_position_value, 'max_position_value': max_position_value, 'available_position_value': available_position_value, 'available_leverage_percent': available_leverage_percent, 'max_leverage': max_leverage } # 价格距离检查信息(用于 LLM 判断) context['price_distance_check'] = { 'enabled': True, 'min_distance_percent': 2.0, # 最小价格距离 2% 'no_exception': True # 没有例外情况 } return context def _build_decision_prompt(self, context: Dict[str, Any]) -> str: """构建决策提示词""" prompt_parts = [] # ============================================================ # 第一步:仓位管理决策流程摘要(最优先!) # ============================================================ prompt_parts.append("="*60) prompt_parts.append("## 🎯 仓位管理决策流程(按顺序执行)") prompt_parts.append("="*60) positions = context.get('positions', []) pending_orders = context.get('pending_orders', []) signals = context.get('signals', []) # 分析当前状态 has_positions = len([p for p in positions if p.get('symbol') == context['symbol']]) > 0 has_pending = len([o for o in pending_orders if o.get('symbol') == context['symbol']]) > 0 # 检查是否有强烈信号 strong_signals = [s for s in signals if s.get('confidence', 0) >= 85] has_strong_signal = len(strong_signals) > 0 if has_positions: prompt_parts.append("📊 当前状态:**有持仓**") prompt_parts.append("") prompt_parts.append("决策优先级:") prompt_parts.append("1️⃣ 首先检查是否需要平仓/减仓(信号反向或趋势减弱)") if has_strong_signal: prompt_parts.append("2️⃣ 然后检查是否需要加仓/滚仓(**有A级信号**,价格距离 >= 2%)") prompt_parts.append(" ⭐ A级信号(confidence >= 85)可考虑加仓或滚仓") else: prompt_parts.append("2️⃣ 然后检查是否需要加仓(价格距离 >= 2%,盈利 >= 2%)") prompt_parts.append(" ⚠️ 当前信号不是A级,不建议加仓") prompt_parts.append("3️⃣ ❌ 不要开新仓(已有持仓时优先管理现有仓位)") prompt_parts.append("") prompt_parts.append("⚠️ 严禁重复开仓!在有持仓时只选择 HOLD/ADD/CLOSE/REDUCE") elif has_pending: prompt_parts.append("📝 当前状态:**有挂单,无持仓**") prompt_parts.append("") prompt_parts.append("决策优先级:") prompt_parts.append("1️⃣ 首先检查是否需要取消挂单(信号反向或价格不优)") if has_strong_signal: prompt_parts.append("2️⃣ 然后检查是否需要调整挂单或现价入场(**有A级信号**)") prompt_parts.append(" ⭐ A级信号(confidence >= 85)可考虑取消挂单现价入场") else: prompt_parts.append("2️⃣ 然后检查是否需要调整挂单价格") prompt_parts.append(" ⚠️ 当前信号不是A级,不建议调整挂单") prompt_parts.append("3️⃣ ❌ 不要开新仓(已有挂单时等待成交或调整)") prompt_parts.append("") prompt_parts.append("⚠️ 严禁重复挂单!在有挂单时只选择 HOLD/CANCEL_PENDING") else: prompt_parts.append("✨ 当前状态:**完全无持仓无挂单**") prompt_parts.append("") prompt_parts.append("决策优先级:") prompt_parts.append("1️⃣ 这时才考虑开新仓(OPEN)") prompt_parts.append("2️⃣ 必须满足所有开仓条件(信号质量、杠杆空间、价格合理)") if not has_strong_signal: prompt_parts.append(" ⚠️ 当前信号不是A级,建议轻仓") prompt_parts.append("") prompt_parts.append("✅ 可以开新仓,但必须谨慎评估") prompt_parts.append("") prompt_parts.append("="*60) prompt_parts.append("") # 市场信号 prompt_parts.append(f"## 市场信号") prompt_parts.append(f"交易对: {context['symbol']}") prompt_parts.append(f"市场状态: {context.get('market_state')}") # 趋势信息(新增) trend_direction = context.get('trend_direction', 'neutral') trend_strength = context.get('trend_strength', 'weak') direction_text = {'uptrend': '📈 上升趋势', 'downtrend': '📉 下降趋势', 'neutral': '➖ 震荡'}.get(trend_direction, trend_direction) strength_text = {'strong': '强势', 'medium': '中等', 'weak': '弱势'}.get(trend_strength, trend_strength) prompt_parts.append(f"趋势: {direction_text} ({strength_text})") # 当前价格(如果有) current_price = context.get('current_price') if current_price: prompt_parts.append(f"当前价格: ${current_price:,.2f}") # 信号列表 signals = context.get('signals', []) if signals: prompt_parts.append(f"\n## 信号列表") for i, sig in enumerate(signals, 1): # timeframe 是 short_term/medium_term/long_term timeframe = sig.get('timeframe', 'N/A') action = sig.get('action', 'N/A') prompt_parts.append(f"{i}. {timeframe} | {action}") prompt_parts.append(f" 信心度: {sig.get('confidence', 0)}") # 添加入场价格信息 entry_zone = sig.get('entry_zone') if entry_zone: prompt_parts.append(f" 建议入场价: ${entry_zone:,.2f}") prompt_parts.append(f" 理由: {sig.get('reasoning', 'N/A')}") # 趋势一致性检查(新增) trend_direction = context.get('trend_direction', 'neutral') trend_strength = context.get('trend_strength', 'weak') prompt_parts.append(f"\n## 🚨 趋势一致性检查(第一优先级)") for i, sig in enumerate(signals, 1): action = sig.get('action', 'hold') is_aligned = (trend_direction == 'uptrend' and action == 'buy') or \ (trend_direction == 'downtrend' and action == 'sell') or \ (trend_direction == 'neutral') if is_aligned: prompt_parts.append(f"✅ 信号#{i} ({action}) 与趋势 ({trend_direction}) 一致 → 可正常开仓") else: # 逆势信号 if trend_strength == 'strong': prompt_parts.append(f"❌ 信号#{i} ({action}) 与强趋势 ({trend_direction}) 相反 → **严禁逆势,返回 HOLD**") elif trend_strength == 'medium': confidence = sig.get('confidence', 0) if confidence >= 85: prompt_parts.append(f"⚠️ 信号#{i} ({action}) 与中等趋势相反,但 confidence={confidence}>=85 → 可谨慎 micro 仓位") else: prompt_parts.append(f"❌ 信号#{i} ({action}) 与中等趋势相反,confidence不足 → 返回 HOLD") else: # weak or neutral confidence = sig.get('confidence', 0) if confidence >= 85: prompt_parts.append(f"⚠️ 信号#{i} ({action}) 与弱趋势相反,但 confidence={confidence}>=85 → 可 micro 仓位") else: prompt_parts.append(f"❌ 信号#{i} ({action}) 与弱趋势相反,confidence不足 → 返回 HOLD") # 关键价位 key_levels = context.get('key_levels', {}) if key_levels: prompt_parts.append(f"\n## 关键价位") if key_levels.get('support'): # 提取数字并格式化 import re def extract_num(val): if isinstance(val, (int, float)): return float(val) if isinstance(val, str): match = re.search(r'[\d,]+\.?\d*', val.replace(',', '')) if match: return float(match.group()) return None supports = [extract_num(s) for s in key_levels['support'][:3]] supports_str = ', '.join([f"${s:,.2f}" for s in supports if s is not None]) prompt_parts.append(f"支撑位: {supports_str}") if key_levels.get('resistance'): import re def extract_num(val): if isinstance(val, (int, float)): return float(val) if isinstance(val, str): match = re.search(r'[\d,]+\.?\d*', val.replace(',', '')) if match: return float(match.group()) return None resistances = [extract_num(r) for r in key_levels['resistance'][:3]] resistances_str = ', '.join([f"${r:,.2f}" for r in resistances if r is not None]) prompt_parts.append(f"阻力位: {resistances_str}") # 当前持仓 positions = context.get('positions', []) prompt_parts.append(f"\n## 当前持仓") if positions: for pos in positions: if pos.get('holding', 0) > 0: prompt_parts.append(f"- {pos.get('symbol')}: {pos.get('side')} {pos.get('holding')} USDT") prompt_parts.append(f" 开仓价: ${pos.get('entry_price')}") prompt_parts.append(f" 止损: ${pos.get('stop_loss')}") prompt_parts.append(f" 止盈: ${pos.get('take_profit')}") else: prompt_parts.append("无持仓") # 当前挂单 pending_orders = context.get('pending_orders', []) prompt_parts.append(f"\n## 当前挂单(仅 {context['symbol']} 的挂单)") if pending_orders: prompt_parts.append(f"⚠️ 重要:以下挂单都属于当前交易对 {context['symbol']},取消订单时只能选择这些订单ID") prompt_parts.append(f"⚠️ 取消规则:做空信号时只能取消做多(🟢long)挂单,做多信号时只能取消做空(🔴short)挂单") # 分类统计挂单方向 long_orders = [o for o in pending_orders if o.get('side') == 'long'] short_orders = [o for o in pending_orders if o.get('side') == 'short'] # 如果只有同向挂单,明确提示LLM signals = context.get('signals', []) if signals: main_action = signals[0].get('action', 'hold') if signals else 'hold' if main_action == 'sell' and not long_orders: prompt_parts.append(f"📌 注意:当前只有做空挂单,与做空信号同向,无需取消!") elif main_action == 'buy' and not short_orders: prompt_parts.append(f"📌 注意:当前只有做多挂单,与做多信号同向,无需取消!") for order in pending_orders: side_icon = "🟢" if order.get('side') == 'long' else "🔴" entry_type = "现价单" if order.get('entry_type') == 'market' else "挂单" side_text = "做多" if order.get('side') == 'long' else "做空" prompt_parts.append(f"- {side_icon} {order.get('symbol')}: {side_text}({order.get('side')}) | {entry_type}") prompt_parts.append(f" 挂单价: ${order.get('entry_price')} | 数量: {order.get('quantity')} USDT") prompt_parts.append(f" 订单ID: {order.get('order_id')}") else: prompt_parts.append("无挂单") # 账户状态 account = context.get('account', {}) lev_info = context.get('leverage_info', {}) prompt_parts.append(f"\n## 账户状态") prompt_parts.append(f"余额: ${account.get('current_balance', 0):.2f}") prompt_parts.append(f"可用: ${account.get('available', 0):.2f}") prompt_parts.append(f"已用保证金: ${account.get('used_margin', 0):.2f}") prompt_parts.append(f"持仓价值: ${account.get('total_position_value', 0):.2f}") prompt_parts.append(f"\n## 杠杆信息") prompt_parts.append(f"当前杠杆: {lev_info.get('current_leverage', 0):.1f}x") prompt_parts.append(f"最大仓位金额: ${lev_info.get('max_position_value', 0):,.2f}") prompt_parts.append(f"可用仓位金额: ${lev_info.get('available_position_value', 0):,.2f}") prompt_parts.append(f"可用杠杆空间: {lev_info.get('available_leverage_percent', 0):.1f}%") prompt_parts.append(f"最大杠杆限制: {lev_info.get('max_leverage', 20)}x") # 价格距离检查规则 price_check = context.get('price_distance_check', {}) if price_check.get('enabled'): min_distance = price_check.get('min_distance_percent', 2) prompt_parts.append(f"\n## 价格距离限制(必须遵守)") prompt_parts.append(f"⚠️ 重要:如果有相同方向的持仓/挂单,价格距离必须 >= {min_distance}%") prompt_parts.append(f"- 低于此距离不开新仓,避免风险过度集中") prompt_parts.append(f"- 此规则**没有例外**,无论信号等级多高都必须遵守") # 计算并显示当前价格距离 current_price = context.get('current_price') signals = context.get('signals', []) positions = context.get('positions', []) pending_orders = context.get('pending_orders', []) if signals and current_price: for sig in signals: sig_action = sig.get('action') sig_entry = sig.get('entry_zone') if sig_action and sig_entry: try: sig_entry = float(sig_entry) prompt_parts.append(f"\n当前信号 {sig_action} @ ${sig_entry:,.2f} 的价格距离检查:") # 检查持仓 for pos in positions: if pos.get('symbol') == context['symbol']: pos_side = pos.get('side') if (sig_action == 'buy' and pos_side == 'long') or (sig_action == 'sell' and pos_side == 'short'): pos_entry = float(pos.get('entry_price', 0)) distance = abs(sig_entry - pos_entry) / pos_entry * 100 status = "✅ 通过" if distance >= min_distance else f"❌ 拒绝 (距离 {distance:.2f}% < {min_distance}%)" prompt_parts.append(f" - 持仓 {pos_side} @ ${pos_entry:,.2f}: 距离 {distance:.2f}% {status}") # 检查挂单 for order in pending_orders: if order.get('symbol') == context['symbol']: order_side = order.get('side') if (sig_action == 'buy' and order_side == 'long') or (sig_action == 'sell' and order_side == 'short'): order_entry = float(order.get('entry_price', 0)) distance = abs(sig_entry - order_entry) / order_entry * 100 status = "✅ 通过" if distance >= min_distance else f"❌ 拒绝 (距离 {distance:.2f}% < {min_distance}%)" prompt_parts.append(f" - 挂单 {order_side} @ ${order_entry:,.2f}: 距离 {distance:.2f}% {status}") except (ValueError, TypeError): pass prompt_parts.append(f"\n请根据以上信息,做出交易决策。") return "\n".join(prompt_parts) def _parse_decision_response(self, response: str, symbol: str) -> Dict[str, Any]: """解析决策响应""" try: import re # 尝试提取 JSON json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response) if json_match: json_str = json_match.group(1) else: json_match = re.search(r'\{[\s\S]*\}', response) if json_match: json_str = json_match.group(0) else: raise ValueError("无法找到 JSON 响应") # 清理 JSON 字符串 json_str = self._clean_json_string(json_str) result = json.loads(json_str) # 清理价格字段 - 转换为 float result = self._clean_price_fields(result) # 添加元数据 result['symbol'] = symbol result['timestamp'] = datetime.now().isoformat() result['raw_response'] = response logger.info(f"✅ 交易决策完成: {symbol} | {result.get('decision', 'HOLD')}") return result except Exception as e: logger.warning(f"解析决策响应失败: {e}") logger.warning(f"原始响应: {response[:1000]}...") # 打印前1000字符 return self._get_hold_decision(symbol, "解析失败,默认观望") def _clean_price_fields(self, data: Dict[str, Any]) -> Dict[str, Any]: """清理价格字段,转换为 float""" def clean_price(price_value): if price_value is None: return None if isinstance(price_value, (int, float)): return float(price_value) if isinstance(price_value, str): # 移除 $ 符号和逗号 cleaned = price_value.replace('$', '').replace(',', '').strip() if cleaned: try: return float(cleaned) except ValueError: return None return None # 清理顶层价格字段 price_fields = ['stop_loss', 'take_profit', 'quantity'] for field in price_fields: if field in data: data[field] = clean_price(data[field]) # 验证止损止盈价格的合理性 data = self._validate_price_fields(data) return data def _validate_price_fields(self, data: Dict[str, Any]) -> Dict[str, Any]: """验证止损止盈价格的合理性,拒绝明显错误的值""" entry = data.get('entry_zone') stop_loss = data.get('stop_loss') take_profit = data.get('take_profit') action = data.get('decision', '') # OPEN/CLOSE/HOLD if not entry or entry <= 0: return data # 判断是做多还是做空 is_long = action == 'OPEN' and data.get('action') == 'buy' is_short = action == 'OPEN' and data.get('action') == 'sell' # 检查止损价格是否合理(偏离入场价不超过 50%) MAX_REASONABLE_DEVIATION = 0.50 # 50% if stop_loss is not None: deviation = abs(stop_loss - entry) / entry # 如果止损价格偏离入场价超过 50%,认为是错误的 if deviation > MAX_REASONABLE_DEVIATION: logger.warning(f"⚠️ 止损价格不合理: entry={entry}, stop_loss={stop_loss}, 偏离={deviation*100:.1f}%,已忽略") data['stop_loss'] = None else: # 做多:止损应该低于入场价 if is_long and stop_loss >= entry: logger.warning(f"⚠️ 做多止损错误: entry={entry}, stop_loss={stop_loss} 应该 < entry,已忽略") data['stop_loss'] = None # 做空:止损应该高于入场价 elif is_short and stop_loss <= entry: logger.warning(f"⚠️ 做空止损错误: entry={entry}, stop_loss={stop_loss} 应该 > entry,已忽略") data['stop_loss'] = None if take_profit is not None: deviation = abs(take_profit - entry) / entry # 如果止盈价格偏离入场价超过 50%,认为是错误的 if deviation > MAX_REASONABLE_DEVIATION: logger.warning(f"⚠️ 止盈价格不合理: entry={entry}, take_profit={take_profit}, 偏离={deviation*100:.1f}%,已忽略") data['take_profit'] = None else: # 做多:止盈应该高于入场价 if is_long and take_profit <= entry: logger.warning(f"⚠️ 做多止盈错误: entry={entry}, take_profit={take_profit} 应该 > entry,已忽略") data['take_profit'] = None # 做空:止盈应该低于入场价 elif is_short and take_profit >= entry: logger.warning(f"⚠️ 做空止盈错误: entry={entry}, take_profit={take_profit} 应该 < entry,已忽略") data['take_profit'] = None return data def _clean_json_string(self, json_str: str) -> str: """清理 JSON 字符串,移除可能导致解析错误的内容""" import re # 移除单行注释 // ... json_str = re.sub(r'//.*?(?=\n|$)', '', json_str) # 移除多行注释 /* ... */ json_str = re.sub(r'/\*[\s\S]*?\*/', '', json_str) # 移除尾随逗号(例如 {"a": 1,} -> {"a": 1}) json_str = re.sub(r',\s*([}\]])', r'\1', json_str) return json_str def _validate_decision(self, decision: Dict[str, Any], positions: List[Dict[str, Any]], account: Dict[str, Any], pending_orders: List[Dict[str, Any]] = None, market_signal: Dict[str, Any] = None) -> Dict[str, Any]: """验证决策安全性""" # 检查杠杆限制 if decision.get('decision') in ['OPEN', 'ADD']: balance = float(account.get('current_balance', 0)) total_position_value = float(account.get('total_position_value', 0)) max_leverage = 20 max_position_value = balance * max_leverage # quantity 是保证金金额,需要乘以杠杆得到持仓价值 margin = float(decision.get('quantity', 0)) position_value = margin * max_leverage # 使用最大杠杆计算持仓价值 new_total_value = total_position_value + position_value if new_total_value > max_position_value: logger.warning(f"⚠️ 决策被拒绝: 超过最大仓位金额 (保证金 ${margin:.2f} → 持仓价值 ${position_value:.2f}, 总计 ${new_total_value:,.2f} > ${max_position_value:,.2f})") return self._get_hold_decision( decision['symbol'], f"超过最大仓位金额 (保证金 ${margin:.2f} → 持仓价值 ${position_value:.2f}, 总计 ${new_total_value:,.2f} > ${max_position_value:,.2f})" ) # 价格距离检查:相同方向相同标的的挂单,价格距离 < 2% 时不加仓/开仓 action = decision.get('action', '') new_entry_price = decision.get('entry_price') if action and new_entry_price: try: new_entry_price = float(new_entry_price) min_distance_percent = 2.0 # 检查持仓 for pos in positions or []: if pos.get('symbol') == decision.get('symbol'): pos_side = pos.get('side', '') # 'long' or 'short' pos_entry = float(pos.get('entry_price', 0)) # 相同方向的持仓 if (action == 'buy' and pos_side == 'long') or (action == 'sell' and pos_side == 'short'): distance_percent = abs(new_entry_price - pos_entry) / pos_entry * 100 if distance_percent < min_distance_percent: logger.warning(f"⚠️ 决策被拒绝: 价格距离过近 (新价格 ${new_entry_price:,.2f} vs 持仓 ${pos_entry:,.2f}, 距离 {distance_percent:.2f}% < {min_distance_percent}%)") return self._get_hold_decision( decision['symbol'], f"价格距离持仓过近 (新价格 ${new_entry_price:,.2f} vs 持仓 ${pos_entry:,.2f}, 距离 {distance_percent:.2f}% < {min_distance_percent}%)" ) # 检查挂单 for order in pending_orders or []: if order.get('symbol') == decision.get('symbol'): order_side = order.get('side', '') order_entry = float(order.get('entry_price', 0)) # 相同方向的挂单 if (action == 'buy' and order_side == 'long') or (action == 'sell' and order_side == 'short'): distance_percent = abs(new_entry_price - order_entry) / order_entry * 100 if distance_percent < min_distance_percent: logger.warning(f"⚠️ 决策被拒绝: 价格距离过近 (新价格 ${new_entry_price:,.2f} vs 挂单 ${order_entry:,.2f}, 距离 {distance_percent:.2f}% < {min_distance_percent}%)") return self._get_hold_decision( decision['symbol'], f"价格距离挂单过近 (新价格 ${new_entry_price:,.2f} vs 挂单 ${order_entry:,.2f}, 距离 {distance_percent:.2f}% < {min_distance_percent}%)" ) except (ValueError, TypeError) as e: logger.warning(f"价格距离检查失败: {e}") return decision def _get_hold_decision(self, symbol: str, reason: str = "") -> Dict[str, Any]: """返回观望决策""" return { 'decision': 'HOLD', 'symbol': symbol, 'action': 'hold', 'reasoning': f'观望: {reason}', 'timestamp': datetime.now().isoformat() }