diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 42fef15..2cea9bc 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -295,6 +295,10 @@ class CryptoAgent: logger.info(f" 支撑位: {support_str or '-'}") logger.info(f" 阻力位: {resistance_str or '-'}") + # 2.5. 回顾并调整现有持仓(LLM 主动管理) + if self.paper_trading_enabled and self.paper_trading: + await self._review_and_adjust_positions(symbol, data) + # 3. 处理信号 signals = result.get('signals', []) @@ -449,6 +453,156 @@ class CryptoAgent: return True + async def _review_and_adjust_positions( + self, + symbol: str, + data: Dict[str, pd.DataFrame] + ): + """ + 回顾并调整现有持仓(LLM 主动管理) + + 每次分析后自动回顾该交易对的所有持仓,让 LLM 决定是否需要: + - 调整止损止盈 + - 部分平仓 + - 全部平仓 + """ + try: + # 获取该交易对的所有活跃持仓(只看已成交的) + active_orders = self.paper_trading.get_active_orders() + positions = [ + order for order in active_orders + if order.get('symbol') == symbol + and order.get('status') == 'open' + and order.get('filled_price') # 只处理已成交的订单 + ] + + if not positions: + return # 没有持仓需要回顾 + + logger.info(f"\n🔄 【LLM 回顾持仓中...】共 {len(positions)} 个持仓") + + # 准备持仓数据 + position_data = [] + for order in positions: + entry_price = order.get('filled_price') or order.get('entry_price', 0) + current_price = self.binance.get_ticker(symbol).get('lastPrice', entry_price) + if isinstance(current_price, str): + current_price = float(current_price) + + # 计算盈亏百分比 + side = order.get('side') + if side == 'long': + pnl_percent = ((current_price - entry_price) / entry_price) * 100 + else: + pnl_percent = ((entry_price - current_price) / entry_price) * 100 + + position_data.append({ + 'order_id': order.get('order_id'), + 'side': side, + 'entry_price': entry_price, + 'current_price': current_price, + 'stop_loss': order.get('stop_loss', 0), + 'take_profit': order.get('take_profit', 0), + 'quantity': order.get('quantity', 0), + 'pnl_percent': pnl_percent, + 'open_time': order.get('open_time') + }) + + # 调用 LLM 回顾分析 + decisions = await self.llm_analyzer.review_positions(symbol, position_data, data) + + if not decisions: + logger.info(" LLM 建议保持所有持仓不变") + return + + # 执行 LLM 的调整建议 + logger.info(f"\n📝 【LLM 调整建议】共 {len(decisions)} 个") + + for decision in decisions: + order_id = decision.get('order_id') + action = decision.get('action') + reason = decision.get('reason', '') + + action_map = { + 'HOLD': '保持', + 'ADJUST_SL_TP': '调整止损止盈', + 'PARTIAL_CLOSE': '部分平仓', + 'FULL_CLOSE': '全部平仓' + } + action_text = action_map.get(action, action) + + logger.info(f" 订单 {order_id[:8]}: {action_text} - {reason}") + + # 执行调整 + result = self.paper_trading.adjust_position_by_llm( + order_id=order_id, + action=action, + new_sl=decision.get('new_sl'), + new_tp=decision.get('new_tp'), + close_percent=decision.get('close_percent') + ) + + if result.get('success'): + # 发送通知 + await self._notify_position_adjustment(symbol, order_id, decision, result) + + # 如果是平仓操作,从活跃订单中移除 + if action in ['PARTIAL_CLOSE', 'FULL_CLOSE']: + closed_result = result.get('pnl', {}) + if closed_result: + pnl = closed_result.get('pnl', 0) + pnl_percent = closed_result.get('pnl_percent', 0) + logger.info(f" ✅ 已平仓: PnL ${pnl:+.2f} ({pnl_percent:+.1f}%)") + else: + logger.warning(f" ❌ 执行失败: {result.get('error')}") + + except Exception as e: + logger.error(f"持仓回顾失败: {e}", exc_info=True) + + async def _notify_position_adjustment( + self, + symbol: str, + order_id: str, + decision: Dict[str, Any], + result: Dict[str, Any] + ): + """发送持仓调整通知""" + action = decision.get('action') + reason = decision.get('reason', '') + + action_map = { + 'ADJUST_SL_TP': '🔄 调整止损止盈', + 'PARTIAL_CLOSE': '📤 部分平仓', + 'FULL_CLOSE': '🚪 全部平仓' + } + action_text = action_map.get(action, action) + + message = f"""{action_text} + +交易对: {symbol} +订单: {order_id[:8]} +原因: {reason}""" + + if action == 'ADJUST_SL_TP': + changes = result.get('changes', []) + message += f"\n调整内容: {', '.join(changes)}" + + elif action in ['PARTIAL_CLOSE', 'FULL_CLOSE']: + pnl_info = result.get('pnl', {}) + if pnl_info: + pnl = pnl_info.get('pnl', 0) + pnl_percent = pnl_info.get('pnl_percent', 0) + message += f"\n实现盈亏: ${pnl:+.2f} ({pnl_percent:+.1f}%)" + + if action == 'PARTIAL_CLOSE': + close_percent = decision.get('close_percent', 0) + remaining = result.get('remaining_quantity', 0) + message += f"\n平仓比例: {close_percent:.0f}%" + message += f"\n剩余仓位: ${remaining:,.0f}" + + await self.feishu.send_text(message) + await self.telegram.send_message(message) + async def analyze_once(self, symbol: str) -> Dict[str, Any]: """单次分析(用于测试或手动触发)""" data = self.binance.get_multi_timeframe_data(symbol) diff --git a/backend/app/crypto_agent/llm_signal_analyzer.py b/backend/app/crypto_agent/llm_signal_analyzer.py index 2ce7681..826bb58 100644 --- a/backend/app/crypto_agent/llm_signal_analyzer.py +++ b/backend/app/crypto_agent/llm_signal_analyzer.py @@ -1180,3 +1180,242 @@ class LLMSignalAnalyzer: 'content': '\n'.join(content_parts), 'color': color } + + # 持仓回顾分析的 System Prompt + POSITION_REVIEW_PROMPT = """你是一个专业的加密货币交易风险管理专家。你的任务是回顾现有持仓,根据最新市场行情决定是否需要调整。 + +## 你的职责 + +对于每个持仓,你需要分析: +1. 当前持仓状态(盈亏、持仓时间、风险敞口) +2. 最新市场行情(趋势、支撑阻力、技术指标) +3. 原有交易逻辑是否依然有效 +4. 是否需要调整止损止盈 +5. 是否需要平仓(部分或全部) + +## 决策类型 + +### 1. HOLD(保持) +- 适用场景:行情符合预期,趋势延续 +- 操作:不改变任何设置 + +### 2. ADJUST_SL_TP(调整止损止盈) +- 适用场景: + - **盈利状态**:趋势强劲,可以收紧止损锁定更多利润 + - **亏损状态**:支撑/阻力位变化,需要调整止损到更合理位置 + - **目标接近**:原止盈目标接近,但趋势仍强,可上移止盈 +- 操作:更新 stop_loss 和/或 take_profit + +### 3. PARTIAL_CLOSE(部分平仓) +- 适用场景: + - 盈利较大,但不确定性增加 + - 重要阻力位附近,锁定部分利润 + - 趋势有转弱迹象 +- 操作:平掉 close_percent 比例的仓位 + +### 4. FULL_CLOSE(全部平仓) +- 适用场景: + - **止损型**:趋势明确反转,止损信号出现 + - **止盈型**:目标达成,或出现更好的机会 + - **风险型**:重大利空/利好的不确定性 +- 操作:平掉全部仓位 + +## 调整原则 + +### 盈利状态(盈亏 > 0) +1. **收紧止损**:如果盈利 > 2%,可以将止损移至保本或盈利 1% 位置 +2. **部分止盈**:如果盈利 > 5% 且接近重要阻力位,可平掉 30-50% 仓位 +3. **继续持有**:如果趋势强劲,可以放宽止损让利润奔跑 + +### 亏损状态(盈亏 < 0) +1. **提前止损**:如果出现明确的反转信号,不要等止损触发 +2. **调整止损**:如果关键支撑/阻力位变化,更新止损位置 +3. **继续持有**:如果只是正常波动,原交易逻辑未变,继续持有 + +### 重要技术信号 +1. **趋势反转**:多周期共振转反、跌破/突破关键 MA +2. **量价背离**:价格新高但成交量萎缩 +3. **MACD 背离**:价格新高/新低但 MACD 未确认 +4. **RSI 极端**:RSI > 75 或 < 25 后掉头 + +## 输出格式 + +对于每个持仓,输出 JSON: +```json +{ + "order_id": "订单ID", + "action": "HOLD | ADJUST_SL_TP | PARTIAL_CLOSE | FULL_CLOSE", + "new_sl": 新止损价格(仅 ADJUST_SL_TP 时), + "new_tp": 新止盈价格(仅 ADJUST_SL_TP 时), + "close_percent": 平仓比例 0-100(仅 PARTIAL_CLOSE 时), + "reason": "调整原因(简明扼要,20字以内)" +} +``` + +## 重要原则 +1. **主动管理**:不要被动等待止损触发,主动识别风险 +2. **保护利润**:盈利状态下,优先考虑锁定利润 +3. **果断止损**:亏损状态下,如果趋势反转,果断离场 +4. **灵活调整**:根据最新行情,不局限于开仓时的判断 +5. **考虑成本**:频繁调整会增加交易成本,只在有明确信号时调整 +""" + + async def review_positions( + self, + symbol: str, + positions: List[Dict[str, Any]], + data: Dict[str, pd.DataFrame] + ) -> List[Dict[str, Any]]: + """ + 回顾并分析现有持仓,给出调整建议 + + Args: + symbol: 交易对 + positions: 持仓列表,每个持仓包含: + - order_id: 订单ID + - side: 'long' or 'short' + - entry_price: 开仓价格 + - current_price: 当前价格 + - stop_loss: 当前止损 + - take_profit: 当前止盈 + - quantity: 仓位数量 + - pnl_percent: 盈亏百分比 + - open_time: 开仓时间 + data: 多周期K线数据 + + Returns: + 调整建议列表 + """ + if not positions: + return [] + + try: + # 构建持仓分析提示 + prompt = self._build_position_review_prompt(symbol, positions, data) + + # 调用 LLM + response = llm_service.chat([ + {"role": "system", "content": self.POSITION_REVIEW_PROMPT}, + {"role": "user", "content": prompt} + ], model_override=self.model_override) + + if not response: + logger.warning(f"{symbol} 持仓回顾 LLM 分析无响应") + return [] + + # 解析响应 + return self._parse_position_review_response(response) + + except Exception as e: + logger.error(f"持仓回顾分析失败: {e}", exc_info=True) + return [] + + def _build_position_review_prompt( + self, + symbol: str, + positions: List[Dict[str, Any]], + data: Dict[str, pd.DataFrame] + ) -> str: + """构建持仓分析提示""" + lines = [f"# {symbol} 持仓回顾分析", f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"] + lines.append("\n## 当前持仓") + + for idx, pos in enumerate(positions, 1): + side_text = "做多 📈" if pos['side'] == 'long' else "做空 📉" + pnl_text = f"+{pos['pnl_percent']:.1f}%" if pos['pnl_percent'] >= 0 else f"{pos['pnl_percent']:.1f}%" + pnl_emoji = "✅" if pos['pnl_percent'] >= 0 else "❌" + + lines.append(f"\n### 持仓 {idx}: {pos['order_id']}") + lines.append(f"- 方向: {side_text}") + lines.append(f"- 开仓价: ${pos['entry_price']:,.2f}") + lines.append(f"- 当前价: ${pos['current_price']:,.2f}") + lines.append(f"- 盈亏: {pnl_emoji} {pnl_text}") + lines.append(f"- 止损: ${pos['stop_loss']:,.2f}") + lines.append(f"- 止盈: ${pos['take_profit']:,.2f}") + lines.append(f"- 仓位: ${pos['quantity']:,.0f}") + + # 计算持仓时间 + if 'open_time' in pos: + open_time = pos['open_time'] + if isinstance(open_time, str): + open_time = datetime.fromisoformat(open_time) + duration = datetime.now() - open_time + hours = duration.total_seconds() / 3600 + lines.append(f"- 持仓时间: {hours:.1f} 小时") + + # 添加市场分析 + lines.append("\n## 最新市场分析") + + # 使用 1h 和 4h 数据分析 + for interval in ['4h', '1h']: + df = data.get(interval) + if df is None or len(df) < 20: + continue + + latest = df.iloc[-1] + prev = df.iloc[-2] + + lines.append(f"\n### {interval} 周期") + lines.append(f"- 当前价格: ${latest['close']:,.2f}") + lines.append(f"- 涨跌幅: {((latest['close'] - prev['close']) / prev['close'] * 100):+.2f}%") + + if 'ma5' in df.columns and pd.notna(latest['ma5']): + lines.append(f"- MA5: ${latest['ma5']:,.2f}") + if 'ma20' in df.columns and pd.notna(latest['ma20']): + lines.append(f"- MA20: ${latest['ma20']:,.2f}") + + if 'rsi' in df.columns and pd.notna(latest['rsi']): + rsi_val = latest['rsi'] + rsi_status = "超买" if rsi_val > 70 else "超卖" if rsi_val < 30 else "正常" + lines.append(f"- RSI: {rsi_val:.1f} ({rsi_status})") + + if 'macd' in df.columns and pd.notna(latest['macd']): + macd_trend = "多头" if latest['macd'] > 0 else "空头" + lines.append(f"- MACD: {latest['macd']:.4f} ({macd_trend})") + + # 添加趋势判断 + lines.append("\n## 请给出调整建议") + lines.append("对于每个持仓,请分析是否需要调整,并按 JSON 格式输出。") + + return "\n".join(lines) + + def _parse_position_review_response(self, response: str) -> List[Dict[str, Any]]: + """解析持仓回顾响应""" + try: + # 尝试提取 JSON 数组 + import json + import re + + # 查找 JSON 数组 + json_match = re.search(r'\[\s*\{.*?\}\s*\]', response, re.DOTALL) + if json_match: + json_str = json_match.group(0) + decisions = json.loads(json_str) + + # 验证每个决策的格式 + valid_decisions = [] + for decision in decisions: + if 'order_id' in decision and 'action' in decision: + valid_decisions.append(decision) + else: + logger.warning(f"无效的决策格式: {decision}") + + return valid_decisions + + # 如果找不到 JSON 数组,尝试解析单个对象 + json_match = re.search(r'\{[^{}]*"action"[^{}]*\}', response, re.DOTALL) + if json_match: + json_str = json_match.group(0) + decision = json.loads(json_str) + if 'order_id' in decision and 'action' in decision: + return [decision] + + logger.warning(f"无法解析持仓回顾响应: {response[:200]}") + return [] + + except json.JSONDecodeError as e: + logger.error(f"解析持仓回顾 JSON 失败: {e}") + return [] + except Exception as e: + logger.error(f"解析持仓回顾响应时出错: {e}") + return [] diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index f091a9e..2c0a15d 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -1304,6 +1304,178 @@ class PaperTradingService: return "\n".join(lines) + def adjust_position_by_llm( + self, + order_id: str, + action: str, + new_sl: Optional[float] = None, + new_tp: Optional[float] = None, + close_percent: Optional[float] = None + ) -> Dict[str, Any]: + """ + 根据 LLM 建议调整持仓 + + Args: + order_id: 订单ID + action: 操作类型 ('HOLD', 'ADJUST_SL_TP', 'PARTIAL_CLOSE', 'FULL_CLOSE') + new_sl: 新止损价格 + new_tp: 新止盈价格 + close_percent: 平仓比例 (0-100) + + Returns: + 调整结果 + """ + if order_id not in self.active_orders: + return {'success': False, 'error': f'订单 {order_id} 不存在或已关闭'} + + order = self.active_orders[order_id] + + # 如果订单是挂单状态,不能调整 + if order.status != OrderStatus.OPEN: + return {'success': False, 'error': f'订单状态为 {order.status.value},无法调整'} + + db = db_service.get_session() + try: + if action == 'HOLD': + return {'success': True, 'message': '保持持仓不变'} + + elif action == 'ADJUST_SL_TP': + # 调整止损止盈 + updated = False + changes = [] + + if new_sl is not None: + order.stop_loss = new_sl + changes.append(f"止损→${new_sl:,.2f}") + updated = True + + if new_tp is not None: + order.take_profit = new_tp + changes.append(f"止盈→${new_tp:,.2f}") + updated = True + + if updated: + db.commit() + logger.info(f"LLM 调整订单 {order_id}: {', '.join(changes)}") + return { + 'success': True, + 'action': 'adjusted', + 'changes': changes, + 'order': order.to_dict() + } + else: + return {'success': False, 'error': '未提供新的止损或止盈价格'} + + elif action == 'PARTIAL_CLOSE': + # 部分平仓 + if close_percent is None or close_percent <= 0 or close_percent > 100: + return {'success': False, 'error': f'无效的平仓比例: {close_percent}'} + + # 计算平仓数量 + close_quantity = order.quantity * (close_percent / 100) + remaining_quantity = order.quantity - close_quantity + + if remaining_quantity < 10: # 剩余数量太小,直接全部平仓 + return self._close_order_llm(order, db, 'PARTIAL_CLOSE', '部分平仓后剩余过少,直接全部平仓') + + # 执行部分平仓 + entry_price = order.filled_price or order.entry_price + current_price = self._get_current_price(order.symbol) + pnl = self._calculate_pnl(order, current_price) + + # 更新订单数量 + order.quantity = remaining_quantity + + # 记录部分平仓 + logger.info(f"LLM 部分平仓: {order_id} 平掉 {close_percent:.1f}% ({close_quantity:.0f})") + + db.commit() + + return { + 'success': True, + 'action': 'partial_close', + 'close_percent': close_percent, + 'close_quantity': close_quantity, + 'remaining_quantity': remaining_quantity, + 'pnl': pnl, + 'order': order.to_dict() + } + + elif action == 'FULL_CLOSE': + # 全部平仓 + return self._close_order_llm(order, db, 'FULL_CLOSE', 'LLM 建议平仓') + + else: + return {'success': False, 'error': f'未知的操作类型: {action}'} + + except Exception as e: + db.rollback() + logger.error(f"调整订单失败: {e}", exc_info=True) + return {'success': False, 'error': str(e)} + finally: + db.close() + + def _close_order_llm( + self, + order: PaperOrder, + db, + close_reason: str, + reason_detail: str + ) -> Dict[str, Any]: + """ + LLM 触发的平仓操作 + + Args: + order: 订单对象 + db: 数据库会话 + close_reason: 平仓原因类型 + reason_detail: 详细原因 + + Returns: + 平仓结果 + """ + # 获取当前价格 + current_price = self._get_current_price(order.symbol) + + # 计算盈亏 + pnl = self._calculate_pnl(order, current_price) + + # 更新订单状态 + order.status = OrderStatus.CLOSED + order.close_price = current_price + order.close_time = datetime.utcnow() + order.pnl = pnl['pnl'] + order.pnl_percent = pnl['pnl_percent'] + order.close_reason = close_reason + + # 从活跃订单中移除 + if order.order_id in self.active_orders: + del self.active_orders[order.order_id] + + db.commit() + + logger.info(f"LLM 平仓: {order.order_id} | 原因: {reason_detail} | 盈亏: ${pnl['pnl']:+.2f} ({pnl['pnl_percent']:+.1f}%)") + + return { + 'success': True, + 'action': 'closed', + 'close_reason': close_reason, + 'reason_detail': reason_detail, + 'pnl': pnl, + 'order': order.to_dict() + } + + def _get_current_price(self, symbol: str) -> float: + """获取交易对当前价格""" + try: + from app.services.binance_service import binance_service + ticker = binance_service.get_ticker(symbol) + if ticker and 'lastPrice' in ticker: + return float(ticker['lastPrice']) + except Exception as e: + logger.warning(f"获取 {symbol} 当前价格失败: {e}") + return 0.0 + def reset_all_data(self) -> Dict[str, Any]: """ 重置所有模拟交易数据