From f6e15d4a48bb9f3622520acf3022e8ac8c81aeaa Mon Sep 17 00:00:00 2001 From: aaron <> Date: Thu, 26 Feb 2026 00:02:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A1=BA=E5=8A=BF=E8=80=8C?= =?UTF-8?q?=E4=B8=BA=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/crypto_agent/crypto_agent.py | 139 ++++++++++++++--- .../crypto_agent/trading_decision_maker.py | 146 +++++++++++++++++- backend/app/services/paper_trading_service.py | 38 +++++ backend/app/services/real_trading_service.py | 83 ++++++++++ backend/test_market_signal.py | 142 +++++++++++++++++ 5 files changed, 520 insertions(+), 28 deletions(-) create mode 100644 backend/test_market_signal.py diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index d2d7b6f..cdcf345 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -514,9 +514,9 @@ class CryptoAgent: # 模拟交易决策 if paper_trading_enabled: logger.info(f"\n📊 【模拟交易决策】") - positions, account = self._get_trading_state(use_real_trading=False) + positions, account, pending_orders = self._get_trading_state(use_real_trading=False) paper_decision = await self.decision_maker.make_decision( - market_signal, positions, account, current_price + market_signal, positions, account, current_price, pending_orders ) self._log_trading_decision(paper_decision) else: @@ -527,9 +527,9 @@ class CryptoAgent: logger.info(f"\n💰 【实盘交易决策】") # 检查是否开启自动交易 if self.real_trading and self.real_trading.get_auto_trading_status(): - positions, account = self._get_trading_state(use_real_trading=True) + positions, account, pending_orders = self._get_trading_state(use_real_trading=True) real_decision = await self.decision_maker.make_decision( - market_signal, positions, account, current_price + market_signal, positions, account, current_price, pending_orders ) self._log_trading_decision(real_decision) else: @@ -657,6 +657,9 @@ class CryptoAgent: Args: use_real_trading: True 获取实盘状态,False 获取模拟交易状态 + + Returns: + (positions, account, pending_orders) - 持仓列表、账户状态、挂单列表 """ if use_real_trading and self.real_trading: # 实盘交易 @@ -667,21 +670,33 @@ class CryptoAgent: active_orders = self.paper_trading.get_active_orders() account = self.paper_trading.get_account_status() - # 转换为标准格式(只返回已成交的订单作为持仓) + # 分离持仓和挂单 position_list = [] + pending_orders = [] for order in active_orders: - # 只返回已成交(OPEN 状态且有 filled_price)的订单 if order.get('status') == 'open' and order.get('filled_price'): + # 已成交的订单作为持仓 position_list.append({ 'symbol': order.get('symbol'), 'side': order.get('side'), - 'holding': order.get('quantity', 0), # 使用 quantity 作为持仓量 + 'holding': order.get('quantity', 0), 'entry_price': order.get('filled_price') or order.get('entry_price'), 'stop_loss': order.get('stop_loss'), 'take_profit': order.get('take_profit') }) + elif order.get('status') == 'pending': + # 未成交的订单作为挂单 + pending_orders.append({ + 'order_id': order.get('order_id'), + 'symbol': order.get('symbol'), + 'side': order.get('side'), + 'entry_price': order.get('entry_price'), + 'quantity': order.get('quantity', 0), + 'entry_type': order.get('entry_type', 'market'), + 'confidence': order.get('confidence', 0) + }) - return position_list, account + return position_list, account, pending_orders async def _execute_decisions(self, paper_decision: Dict[str, Any], real_decision: Dict[str, Any], @@ -721,22 +736,28 @@ class CryptoAgent: logger.info(f"\n📊 【执行模拟交易】") if decision_type in ['OPEN', 'ADD']: - # 先执行交易 - result = await self._execute_paper_trade(paper_decision, market_signal, current_price) - # 检查是否成功执行 - order = result.get('order') if result else None - if order: - # 只有成功创建订单后才发送通知 - await self._send_signal_notification(market_signal, paper_decision, current_price, is_paper=True) - paper_executed = True - else: - # 有信号但订单创建失败,发送未执行通知 - reason = result.get('message', '订单创建失败') if result else '订单创建失败' - await self._notify_signal_not_executed(market_signal, paper_decision, current_price, is_paper=True, reason=reason) - logger.warning(f" ⚠️ 模拟交易未执行,已发送通知") + # 先执行交易 + result = await self._execute_paper_trade(paper_decision, market_signal, current_price) + # 检查是否成功执行 + order = result.get('order') if result else None + if order: + # 只有成功创建订单后才发送通知 + await self._send_signal_notification(market_signal, paper_decision, current_price, is_paper=True) + paper_executed = True + else: + # 有信号但订单创建失败,发送未执行通知 + reason = result.get('message', '订单创建失败') if result else '订单创建失败' + await self._notify_signal_not_executed(market_signal, paper_decision, current_price, is_paper=True, reason=reason) + logger.warning(f" ⚠️ 模拟交易未执行,已发送通知") elif decision_type == 'CLOSE': await self._execute_close(paper_decision, paper_trading=True) paper_executed = True + elif decision_type == 'CANCEL_PENDING': + await self._execute_cancel_pending(paper_decision, paper_trading=True) + paper_executed = True + elif decision_type == 'REDUCE': + await self._execute_reduce(paper_decision, paper_trading=True) + paper_executed = True # ============================================================ # 执行实盘交易决策 @@ -770,6 +791,12 @@ class CryptoAgent: elif decision_type == 'CLOSE': await self._execute_close(real_decision, paper_trading=False) real_executed = True + elif decision_type == 'CANCEL_PENDING': + await self._execute_cancel_pending(real_decision, paper_trading=False) + real_executed = True + elif decision_type == 'REDUCE': + await self._execute_reduce(real_decision, paper_trading=False) + real_executed = True # 如果都没有执行,给出提示 if not paper_executed and not real_executed: @@ -1260,6 +1287,76 @@ class CryptoAgent: except Exception as e: logger.error(f"执行平仓失败: {e}") + async def _execute_cancel_pending(self, decision: Dict[str, Any], paper_trading: bool = True): + """执行取消挂单 + + Args: + decision: 交易决策 + paper_trading: True=模拟交易, False=实盘交易 + """ + try: + symbol = decision.get('symbol') + orders_to_cancel = decision.get('orders_to_cancel', []) + + if not orders_to_cancel: + logger.info(f" ⚠️ 没有需要取消的订单") + return + + trading_service = self.paper_trading if paper_trading else self.real_trading + trading_type = "模拟" if paper_trading else "实盘" + + if not trading_service: + logger.warning(f" {trading_type}交易服务未初始化") + return + + logger.info(f" 🚫 {trading_type}取消挂单: {symbol}") + logger.info(f" 取消订单数量: {len(orders_to_cancel)}") + + cancelled_count = 0 + for order_id in orders_to_cancel: + try: + # 取消订单 + result = trading_service.cancel_order(order_id) + if result.get('success'): + cancelled_count += 1 + logger.info(f" ✅ 已取消订单: {order_id}") + else: + logger.warning(f" ⚠️ 取消订单失败: {order_id} | {result.get('message', '')}") + except Exception as e: + logger.error(f" ❌ 取消订单异常: {order_id} | {e}") + + logger.info(f" 📊 成功取消 {cancelled_count}/{len(orders_to_cancel)} 个订单") + + except Exception as e: + logger.error(f"执行取消挂单失败: {e}") + + async def _execute_reduce(self, decision: Dict[str, Any], paper_trading: bool = True): + """执行减仓 + + Args: + decision: 交易决策 + paper_trading: True=模拟交易, False=实盘交易 + """ + try: + symbol = decision.get('symbol') + trading_type = "模拟" if paper_trading else "实盘" + + logger.info(f" 📤 {trading_type}减仓: {symbol}") + logger.info(f" 理由: {decision.get('reasoning', '')}") + + trading_service = self.paper_trading if paper_trading else self.real_trading + + if not trading_service: + logger.warning(f" {trading_type}交易服务未初始化") + return + + # TODO: 实现减仓逻辑 + # 减仓可以是部分平仓,需要根据决策中的参数执行 + logger.info(f" ⚠️ 减仓功能待实现") + + except Exception as e: + logger.error(f"执行减仓失败: {e}") + def _convert_to_paper_signal(self, symbol: str, signal: Dict[str, Any], current_price: float) -> Dict[str, Any]: """转换 LLM 信号格式为模拟交易格式""" diff --git a/backend/app/crypto_agent/trading_decision_maker.py b/backend/app/crypto_agent/trading_decision_maker.py index f8ebc55..7ea20e0 100644 --- a/backend/app/crypto_agent/trading_decision_maker.py +++ b/backend/app/crypto_agent/trading_decision_maker.py @@ -33,6 +33,22 @@ class TradingDecisionMaker: 3. 账户状态(余额、已用保证金、杠杆等) ## 决策类型 +### 🚨 铁律(必须首先检查) +**趋势与信号方向一致性检查(第一优先级)**: + +| 当前趋势 | 信号方向 | 允许操作 | 条件 | +|---------|---------|---------|------| +| `uptrend` (上升) | buy (做多) | ✅ 允许 | 正常仓位 | +| `uptrend` (上升) | sell (做空) | ❌ 禁止 | **除非**有极强的多重反转信号(背离+放量+关键形态)+ confidence >= 85 | +| `downtrend` (下降) | sell (做空) | ✅ 允许 | 正常仓位 | +| `downtrend` (下降) | buy (做多) | ❌ 禁止 | **除非**有极强的多重反转信号(背离+放量+关键形态)+ confidence >= 85 | +| `neutral` (震荡) | buy/sell | ✅ 允许 | 轻仓操作 | + +**趋势强度限制**: +- `strong` 趋势:**严禁逆势开仓**,直接返回 HOLD +- `medium` 趋势:逆势开仓需 confidence >= 85 + 多重反转信号 +- `weak` 或 `neutral`:可双向交易,但谨慎仓位 + ### 1. 开仓(OPEN) **时机**:无持仓或可以加仓时 **要求**: @@ -43,6 +59,34 @@ class TradingDecisionMaker: - 账户有足够的可用杠杆空间 - 风险可控(止损明确) +**顺势 vs 逆势仓位规则(重要)**: + +| 情况 | 仓位限制 | 保证金倍率 | +|-----|---------|-----------| +| **顺势交易**(信号与趋势同向) | 正常仓位 | 100% | +| **震荡市交易**(neutral 趋势) | 降级处理 | 70% | +| **逆势交易**(信号与趋势反向) | **大幅降级** | 30% | + +**具体规则**: +- 顺势 A 级 → heavy(12% 保证金) +- 顺势 B 级 → medium(6% 保证金) +- 顺势 C 级 → light(3% 保证金) + +- 震荡 A 级 → medium(6% × 70% ≈ 4%) +- 震荡 B 级 → light(3% × 70% ≈ 2%) +- 震荡 C 级 → micro(1.5% 保证金) + +- **逆势交易**(仅允许在 medium/weak 趋势 + confidence >= 85): + - 逆势 A 级 → light(3% × 30% ≈ 1%) + - 逆势 B/C 级 → **禁止**,返回 HOLD + +**示例**: +- 账户余额 $10,000 +- 顺势 heavy:保证金 $1,200 → 持仓 $24,000 +- 顺势 medium:保证金 $600 → 持仓 $12,000 +- 顺势 light:保证金 $300 → 持仓 $6,000 +- 逆势 light(极少数情况):保证金 $100 → 持仓 $2,000 + ### 2. 平仓(CLOSE) **时机**: - 触发止损/止盈 @@ -67,7 +111,24 @@ class TradingDecisionMaker: - 降低风险敞口 - 不确定增加 -### 5. 观望(HOLD) +### 5. 取消挂单(CANCEL_PENDING) +**时机**: +- **趋势反转时自动取消反向挂单**(重要): + - 当前是 `uptrend`(上升趋势)时,取消所有做空(short)挂单 + - 当前是 `downtrend`(下降趋势)时,取消所有做多(long)挂单 + - 趋势强度为 `strong` 时,必须立即取消反向挂单 +- **信号方向与挂单方向相反**: + - 新信号是 buy,但存在 sell 挂单 → 取消 sell 挂单 + - 新信号是 sell,但存在 buy 挂单 → 取消 buy 挂单 +- **价格偏离过大**: + - 当前价格距离挂单价超过 3%,建议取消重新挂单 + +**输出格式**: +- `decision: "CANCEL_PENDING"` +- `orders_to_cancel`: ["order_id_1", "order_id_2"] - 要取消的订单ID列表 +- `reasoning`: "取消原因" + +### 6. 观望(HOLD) **时机**: - 信号不明确 - 风险过大 @@ -99,6 +160,7 @@ class TradingDecisionMaker: - **heavy**:使用保证金 = 账户余额 × 12% - **medium**:使用保证金 = 账户余额 × 6% - **light**:使用保证金 = 账户余额 × 3% +- **micro**:使用保证金 = 账户余额 × 1.5%(极小仓位,仅用于震荡市或特殊逆势情况) #### 4. 选择逻辑示例 假设当前可用杠杆空间为 50%: @@ -135,21 +197,36 @@ class TradingDecisionMaker: ```json { - "decision": "OPEN/CLOSE/ADD/REDUCE/HOLD", + "decision": "OPEN/CLOSE/ADD/REDUCE/CANCEL_PENDING/HOLD", "symbol": "BTC/USDT", "side": "buy/sell", "action": "open_long/close_short/add_long/...", - "position_size": "heavy/medium/light", + "position_size": "heavy/medium/light/micro", "quantity": 1200, + "position_multiplier": 1.0, "confidence": 0-100, "reasoning": "简洁的决策理由(1句话,15字以内)", "risk_analysis": "核心风险点(1句话,15字以内)", "stop_loss": 65500, "take_profit": 67500, + "orders_to_cancel": ["order_id_1", "order_id_2"], "notes": "其他说明" } ``` +**注意**: +- `position_size` 可选值:`heavy`/`medium`/`light`/`micro` + - `heavy`:12% 保证金(顺势A级) + - `medium`:6% 保证金(顺势B级 或 震荡A级) + - `light`:3% 保证金(顺势C级 或 震荡B级) + - `micro`:1.5% 保证金(震荡C级 或 特殊逆势情况) +- `position_multiplier`:仓位倍率(可选,默认1.0) + - 顺势交易:1.0 + - 震荡市:0.7 + - 逆势交易(极少数情况):0.3 +- 如果 `decision` 是 `CANCEL_PENDING`,需要提供 `orders_to_cancel` 字段(要取消的订单ID列表) +- 如果 `decision` 是 `OPEN/ADD/CLOSE/REDUCE`,不需要 `orders_to_cancel` 字段 + ## 重要说明 - **所有价格必须是纯数字**,不要加 $ 符号、逗号或其他格式 - `stop_loss`、`take_profit` 必须是数字类型 @@ -182,7 +259,8 @@ class TradingDecisionMaker: market_signal: Dict[str, Any], positions: List[Dict[str, Any]], account: Dict[str, Any], - current_price: float = None) -> Dict[str, Any]: + current_price: float = None, + pending_orders: List[Dict[str, Any]] = None) -> Dict[str, Any]: """ 做出交易决策 @@ -191,6 +269,7 @@ class TradingDecisionMaker: positions: 当前持仓列表 account: 账户状态 current_price: 当前价格(用于判断入场方式) + pending_orders: 未成交的挂单列表 Returns: 交易决策字典 @@ -198,7 +277,7 @@ class TradingDecisionMaker: try: # 1. 准备决策上下文 decision_context = self._prepare_decision_context( - market_signal, positions, account, current_price + market_signal, positions, account, current_price, pending_orders or [] ) # 2. 构建提示词 @@ -229,15 +308,19 @@ class TradingDecisionMaker: market_signal: Dict[str, Any], positions: List[Dict[str, Any]], account: Dict[str, Any], - current_price: float = None) -> 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 } @@ -281,7 +364,13 @@ class TradingDecisionMaker: prompt_parts.append(f"## 市场信号") prompt_parts.append(f"交易对: {context['symbol']}") prompt_parts.append(f"市场状态: {context.get('market_state')}") - prompt_parts.append(f"趋势: {context.get('trend')}") + + # 趋势信息(新增) + 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') @@ -306,6 +395,36 @@ class TradingDecisionMaker: 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: @@ -353,6 +472,19 @@ class TradingDecisionMaker: else: prompt_parts.append("无持仓") + # 当前挂单 + pending_orders = context.get('pending_orders', []) + prompt_parts.append(f"\n## 当前挂单") + if pending_orders: + for order in pending_orders: + side_icon = "🟢" if order.get('side') == 'long' else "🔴" + entry_type = "现价单" if order.get('entry_type') == 'market' else "挂单" + prompt_parts.append(f"- {side_icon} {order.get('symbol')}: {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', {}) diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index 77332f0..798a6eb 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -1000,6 +1000,44 @@ class PaperTradingService: finally: db.close() + def cancel_order(self, order_id: str) -> Dict[str, Any]: + """ + 取消挂单 + + Args: + order_id: 订单ID + + Returns: + 取消结果字典 + """ + if order_id not in self.active_orders: + return { + 'success': False, + 'message': f'订单不存在: {order_id}' + } + + order = self.active_orders[order_id] + + # 只能取消挂单状态的订单 + if order.status != OrderStatus.PENDING: + return { + 'success': False, + 'message': f'只能取消挂单状态的订单,当前状态: {order.status.value}' + } + + result = self._cancel_pending_order(order) + if result: + return { + 'success': True, + 'order_id': order_id, + 'message': '挂单已取消' + } + else: + return { + 'success': False, + 'message': '取消挂单失败' + } + def _cancel_pending_order(self, order: PaperOrder) -> Dict[str, Any]: """取消挂单""" db = db_service.get_session() diff --git a/backend/app/services/real_trading_service.py b/backend/app/services/real_trading_service.py index ba9b8f2..da5ca06 100644 --- a/backend/app/services/real_trading_service.py +++ b/backend/app/services/real_trading_service.py @@ -553,6 +553,89 @@ class RealTradingService: return order.to_dict() return None + def cancel_order(self, order_id: str) -> Dict[str, Any]: + """ + 取消挂单 + + Args: + order_id: 订单ID + + Returns: + 取消结果字典 + """ + if order_id not in self.active_orders: + return { + 'success': False, + 'message': f'订单不存在: {order_id}' + } + + order = self.active_orders[order_id] + + # 只能取消挂单状态的订单 + if order.status != OrderStatus.PENDING: + return { + 'success': False, + 'message': f'只能取消挂单状态的订单,当前状态: {order.status.value}' + } + + # 调用交易所 API 取消订单 + if not self.trading_api: + return { + 'success': False, + 'message': '交易 API 未初始化' + } + + try: + # 获取原始订单ID(如果有) + original_order_id = order.original_order_id or order_id + + # 调用交易所取消订单API + success = self.trading_api.cancel_order(symbol=order.symbol, order_id=original_order_id) + + if success: + # 更新本地订单状态 + order.status = OrderStatus.CANCELLED + order.closed_at = datetime.utcnow() + + # 保存到数据库 + db = db_service.get_session() + try: + db_order = db.query(RealOrder).filter(RealOrder.order_id == order_id).first() + if db_order: + db_order.status = OrderStatus.CANCELLED + db_order.closed_at = datetime.utcnow() + db.merge(db_order) + db.commit() + except Exception as e: + logger.error(f"更新数据库订单状态失败: {e}") + db.rollback() + finally: + db.close() + + # 从活跃订单缓存中移除 + if order_id in self.active_orders: + del self.active_orders[order_id] + + logger.info(f"实盘挂单已取消: {order_id} | {order.symbol}") + + return { + 'success': True, + 'order_id': order_id, + 'message': '挂单已取消' + } + else: + return { + 'success': False, + 'message': '交易所取消订单失败' + } + + except Exception as e: + logger.error(f"取消实盘挂单失败: {e}") + return { + 'success': False, + 'message': f'取消订单异常: {e}' + } + def sync_positions_from_exchange(self) -> List[Dict]: """ 从交易所同步持仓状态 diff --git a/backend/test_market_signal.py b/backend/test_market_signal.py new file mode 100644 index 0000000..56c65dd --- /dev/null +++ b/backend/test_market_signal.py @@ -0,0 +1,142 @@ +""" +测试市场信号分析 - 验证趋势判断功能 +""" +import asyncio +import sys +import os +import json + +# 添加项目路径 +sys.path.insert(0, os.path.dirname(__file__)) + +from app.crypto_agent.market_signal_analyzer import MarketSignalAnalyzer +from app.utils.logger import logger +from app.services.bitget_service import BitgetService + + +async def test_trend_analysis(symbol: str = "BTCUSDT"): + """测试趋势分析""" + + logger.info("=" * 60) + logger.info(f"开始测试市场信号分析: {symbol}") + logger.info("=" * 60) + + # 1. 获取 K 线数据 + logger.info(f"\n[1/3] 获取 {symbol} 的 K 线数据...") + bitget_service = BitgetService() + data = bitget_service.get_multi_timeframe_data(symbol) + + if not data: + logger.error(f"❌ 无法获取 {symbol} 的 K 线数据") + return + + logger.info(f"✅ K 线数据获取成功") + for tf, df in data.items(): + if df is not None and len(df) > 0: + latest_price = float(df.iloc[-1]['close']) + logger.info(f" {tf}: 最新价格 ${latest_price:,.2f}, 数据量 {len(df)} 条") + + # 2. 分析市场信号 + logger.info(f"\n[2/3] 分析市场信号...") + analyzer = MarketSignalAnalyzer() + result = await analyzer.analyze(symbol, data) + + # 3. 打印分析结果 + logger.info(f"\n[3/3] 分析结果:") + logger.info("-" * 60) + + # 打印趋势判断(新功能) + logger.info("\n📊 趋势判断(新增):") + logger.info(f" 趋势方向: {result.get('trend_direction', 'unknown')}") + logger.info(f" 趋势强度: {result.get('trend_strength', 'unknown')}") + + # 打印市场状态 + logger.info(f"\n📈 市场状态:") + logger.info(f" {result.get('market_state', 'unknown')}") + logger.info(f" 分析摘要: {result.get('analysis_summary', 'unknown')}") + logger.info(f" 量价分析: {result.get('volume_analysis', 'unknown')}") + + # 打印关键价位 + if result.get('key_levels'): + logger.info(f"\n💰 关键价位:") + levels = result['key_levels'] + if levels.get('support'): + logger.info(f" 支撑位: {[f'${s:,.0f}' for s in levels['support'] if s]}") + if levels.get('resistance'): + logger.info(f" 阻力位: {[f'${r:,.0f}' for r in levels['resistance'] if r]}") + + # 打印信号 + logger.info(f"\n🎯 交易信号:") + signals = result.get('signals', []) + if not signals: + logger.warning(" ⚠️ 无交易信号(观望)") + else: + for i, sig in enumerate(signals, 1): + action_emoji = "🟢" if sig.get('action') == 'buy' else "🔴" if sig.get('action') == 'sell' else "⏪" + grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(sig.get('grade', ''), '') + entry_type_emoji = "⚡现价" if sig.get('entry_type') == 'market' else "⏳挂单" + + logger.info(f"\n 信号 #{i} {action_emoji} {grade_icon}") + logger.info(f" ├─ 类型: {sig.get('timeframe', 'unknown')} | {entry_type_emoji}") + logger.info(f" ├─ 操作: {sig.get('action', 'unknown').upper()}") + logger.info(f" ├─ 信心度: {sig.get('confidence', 0)}% | 等级: {sig.get('grade', 'unknown')}") + logger.info(f" ├─ 入场价: ${sig.get('entry_zone', 0):,.2f}") + logger.info(f" ├─ 止损: ${sig.get('stop_loss', 0):,.2f} | 止盈: ${sig.get('take_profit', 0):,.2f}") + logger.info(f" └─ 理由: {sig.get('reasoning', 'unknown')}") + + # 检查是否顺势 + trend_dir = result.get('trend_direction', 'neutral') + action = sig.get('action', '') + if trend_dir == 'uptrend' and action == 'sell': + logger.warning(f" ⚠️ 警告: 上升趋势中做空(逆势)") + elif trend_dir == 'downtrend' and action == 'buy': + logger.warning(f" ⚠️ 警告: 下降趋势中做多(逆势)") + elif trend_dir == 'uptrend' and action == 'buy': + logger.info(f" ✅ 顺势交易") + elif trend_dir == 'downtrend' and action == 'sell': + logger.info(f" ✅ 顺势交易") + + logger.info("\n" + "=" * 60) + logger.info("测试完成") + logger.info("=" * 60) + + return result + + +async def test_multiple_symbols(): + """测试多个交易对""" + symbols = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT'] + + for symbol in symbols: + logger.info(f"\n\n{'#' * 60}") + logger.info(f"# 测试 {symbol}") + logger.info(f"{'#' * 60}") + + try: + await test_trend_analysis(symbol) + except Exception as e: + logger.error(f"❌ {symbol} 测试失败: {e}") + import traceback + traceback.print_exc() + + # 等待一段时间,避免 API 限流 + await asyncio.sleep(2) + + +def main(): + import argparse + + parser = argparse.ArgumentParser(description='测试市场信号分析') + parser.add_argument('--symbol', default='BTCUSDT', help='交易对(默认: BTCUSDT)') + parser.add_argument('--multi', action='store_true', help='测试多个交易对') + + args = parser.parse_args() + + if args.multi: + asyncio.run(test_multiple_symbols()) + else: + asyncio.run(test_trend_analysis(args.symbol)) + + +if __name__ == '__main__': + main()