diff --git a/docs/交易分析_2026-02-14_策略执行与优化建议.md b/docs/交易分析_2026-02-14_策略执行与优化建议.md new file mode 100644 index 0000000..5b17c1a --- /dev/null +++ b/docs/交易分析_2026-02-14_策略执行与优化建议.md @@ -0,0 +1,83 @@ +# 交易记录分析:2026-02-14 策略执行情况与优化建议 + +## 一、当日概览(清理后) + +| 指标 | 数值 | +|------|------| +| 总交易数 | 79 | +| 胜率 | 42.03% | +| 总盈亏 | 5.96 USDT | +| 平均盈亏 | 0.08 USDT | +| 平均持仓时长 | 23 分钟 | +| 盈亏比 | 1.66 : 1 | +| 总交易量 | 3372.52 USDT | + +**平仓原因分布**:止损 21 / 止盈 11 / 移动止损 5 / 手动 26 / 同步 6 + +--- + +## 二、按平仓原因的盈亏拆解(基于导出数据) + +| 平仓类型 | 笔数 | 盈利笔数 | 总盈亏(USDT) | 说明 | +|----------|------|----------|--------------|------| +| 手动平仓 | 26~27 | 24 | **+18.77** | 主要利润来源,多为主动止盈 | +| 移动止损 | 5 | 2 | **+5.40** | 锁定利润,表现正常 | +| 自动止盈 | 11 | 3 | **-4.04** | 多数笔实际亏损,异常 | +| 自动止损 | 21~27 | 1 | **-13.07** | 主要亏损来源 | +| 同步平仓 | 6~8 | 0 | -1.02 | 外部/漏跟,笔数不多 | + +结论:**盈利主要来自「手动平仓」和「移动止损」;亏损主要来自「自动止损」和标成「止盈」但实际亏损的订单。** + +--- + +## 三、发现的问题 + +### 1. 「自动平仓(止盈)」里大量实际亏损 + +- 11 笔止盈单中 **8 笔实际亏损**,总盈亏 -4.04 USDT,平均盈亏比例约 -5.5%。 +- 样本显示多为 **SELL 单、出场价高于入场价**(做空被价格向上打掉),说明: + - 要么 **exit_reason 标错**(实际是止损/移动止损,被记成 take_profit), + - 要么是 **分步止盈后剩余仓位止损**,但整笔记录的 exit 按止损价或市价算,导致整笔显示亏损却标成止盈。 +- **建议**:在同步/更新平仓原因时,若 `exit_price` 与 `entry_price` 方向对持仓不利且 PnL < 0,应优先标为 `stop_loss` 或 `trailing_stop`,避免“止盈单亏损”的统计噪音;并检查分步止盈后对剩余仓位平仓的记库逻辑(是否用错了价格或原因)。 + +### 2. 止损笔数多、单笔亏损偏大 + +- 止损 21~27 笔,总亏约 -13 USDT,中位数约 **-4% 保证金**,但有单笔到 **-38.9%**(及个别误标为止损的 +42.5%)。 +- 平均持仓仅 23 分钟,说明不少单子是 **快进快出被止损**,可能与当前 ATR/止损距离偏紧或入场质量有关。 +- **建议**: + - 已用 `STOP_LOSS_PERCENT` 做 ATR 下限,可观察接下来几天止损的保证金分布是否收敛; + - 若仍多笔在 -5%~-15% 被扫,可适当放宽 ATR 倍数或提高 `MIN_STOP_LOSS_PRICE_PCT`(在保证交易所最小距离前提下); + - 提高入场门槛(如 `MIN_SIGNAL_STRENGTH`、趋势过滤),减少“假突破后立刻止损”的笔数。 + +### 3. 手动平仓占比高且质量好 + +- 26 笔手动、24 笔盈利、+18.77 USDT,说明人工或半自动在合适时机平仓很有效。 +- 若这些多为“提前止盈/移动止损后手动确认”,可考虑: + - **移动止损**:适当放宽 `TRAILING_STOP_ACTIVATION` / `TRAILING_STOP_PROTECT`,让更多盈利单通过移动止损自动锁定,减少对手动平仓的依赖; + - **第一目标止盈**:若 TP1 命中率高,可维持或略调高比例,让“自动止盈”的统计更贴近真实止盈。 + +### 4. 同步平仓与数据质量 + +- 同步 6~8 笔、全亏,可能是币安端其它操作或漏跟。清理后这类应会减少;若仍出现,可定期用 `cleanup_non_system_trades.sql` 思路抽查无 `entry_order_id` 或异常来源的记录。 + +--- + +## 四、优化建议汇总 + +| 优先级 | 方向 | 具体建议 | +|--------|------|----------| +| 高 | 修正“止盈单亏损”统计与逻辑 | 1)平仓原因判定:亏损单且价格对持仓不利时,不标为 take_profit;2)检查分步止盈后剩余仓位平仓的 exit_price/exit_reason 写入 | +| 高 | 控制止损质量 | 1)观察 1~2 天止损的保证金%分布;2)若 -5%~-15% 仍多,适当放宽 ATR 或入场过滤,减少假突破被扫 | +| 中 | 提高“自动锁定利润”占比 | 1)适当放宽移动止损激活/保护参数;2)保持或微调 TP1,让更多盈利由系统自动止盈/移动止损完成 | +| 中 | 入场质量 | 在可接受交易频率下,略提高 MIN_SIGNAL_STRENGTH 或趋势过滤,减少短时被止损的笔数 | +| 低 | 监控与运维 | 定期看“止盈/止损/同步”的盈亏分布,发现异常及时查 exit_reason 与成交价 | + +--- + +## 五、简要结论 + +- **策略能盈利**(当日 +5.96 USDT),主要来自**手动平仓**和**移动止损**,说明方向和择时整体可行。 +- **主要拖累**:① 自动止损笔数多、总亏大;② “自动止盈”里多笔实际亏损,暴露出**平仓原因标记或分步止盈记库**问题。 +- **建议优先**:修正止盈/止损的标记与记库逻辑,再根据新数据微调止损宽度与入场过滤,让自动止盈与移动止损在统计和实盘上更一致、可解释。 + +(数据来源:`交易记录_2026-02-14T09-24-06.json`,清理后本系统订单。) diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 295ba3c..327236e 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -193,12 +193,14 @@ class BinanceClient: f"测试网: {self.testnet}, 超时: {timeout}秒)..." ) + # 不在此处设置全局 timeout,避免拖慢下单/止损止盈(需快速失败并重试);只读接口在各自方法内用 asyncio.wait_for 单独加长超时 + req_params = dict(requests_params or {}) # 创建客户端(使用最新的API密钥,如果为空则只能访问公开接口) self.client = await AsyncClient.create( api_key=self.api_key or None, # 空字符串转为 None api_secret=self.api_secret or None, testnet=self.testnet, - requests_params=requests_params + requests_params=req_params ) # 测试连接(带超时) @@ -763,13 +765,17 @@ class BinanceClient: Returns: 持仓列表 """ - retries = 3 + retries = 5 last_error = None + read_timeout = getattr(config, 'READ_ONLY_REQUEST_TIMEOUT', 45) for attempt in range(retries): try: - # 增加 recvWindow 以避免 -1021 错误 - positions = await self.client.futures_position_information(recvWindow=20000) + # 增加 recvWindow 以避免 -1021 错误;仅此只读接口用较长超时,不影响下单类接口 + positions = await asyncio.wait_for( + self.client.futures_position_information(recvWindow=20000), + timeout=read_timeout + ) # 只保留真实持仓:非零且名义价值 >= 1 USDT,避免灰尘持仓被当成“有仓”导致同步时批量创建假 manual_entry min_notional = 1.0 open_positions = [] @@ -805,8 +811,9 @@ class BinanceClient: if is_network_error: if attempt < retries - 1: - logger.warning(f"获取持仓信息失败 (第 {attempt + 1}/{retries} 次): {e},将在 1秒后重试...") - await asyncio.sleep(1) + wait = 2 if attempt >= 2 else 1 + logger.warning(f"获取持仓信息失败 (第 {attempt + 1}/{retries} 次): {_format_exception(e)},{wait}秒后重试...") + await asyncio.sleep(wait) continue logger.error(f"获取持仓信息失败: {_format_exception(e)}") @@ -822,7 +829,7 @@ class BinanceClient: async def get_recent_trades(self, symbol: str, limit: int = 50) -> List[Dict]: """ - 获取最近的成交记录 + 获取最近的成交记录(超时/网络错误时自动重试) Args: symbol: 交易对 @@ -831,11 +838,28 @@ class BinanceClient: Returns: 成交记录列表 """ - try: - return await self.client.futures_account_trades(symbol=symbol, limit=limit) - except Exception as e: - logger.error(f"获取成交记录失败 {symbol}: {_format_exception(e)}") - return [] + retries = 3 + read_timeout = getattr(config, 'READ_ONLY_REQUEST_TIMEOUT', 45) + for attempt in range(retries): + try: + return await asyncio.wait_for( + self.client.futures_account_trades(symbol=symbol, limit=limit, recvWindow=20000), + timeout=read_timeout + ) + except (asyncio.TimeoutError, BinanceAPIException) as e: + is_retryable = isinstance(e, asyncio.TimeoutError) or ( + isinstance(e, BinanceAPIException) and (e.code == -1021 or str(e.code).startswith('5')) + ) + if is_retryable and attempt < retries - 1: + logger.warning(f"获取成交记录失败 {symbol} (第 {attempt + 1}/{retries} 次): {_format_exception(e)},1秒后重试...") + await asyncio.sleep(1) + continue + logger.error(f"获取成交记录失败 {symbol}: {_format_exception(e)}") + return [] + except Exception as e: + logger.error(f"获取成交记录失败 {symbol}: {_format_exception(e)}") + return [] + return [] async def get_symbol_info(self, symbol: str) -> Optional[Dict]: """ diff --git a/trading_system/config.py b/trading_system/config.py index aa76ae3..9fa7833 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -389,6 +389,8 @@ def reload_config(): # 连接配置 CONNECTION_TIMEOUT = int(os.getenv('CONNECTION_TIMEOUT', '30')) # 连接超时时间(秒) CONNECTION_RETRIES = int(os.getenv('CONNECTION_RETRIES', '3')) # 连接重试次数 +# 仅用于 get_open_positions / get_recent_trades 等只读接口的单次等待时间,不影响下单/止损止盈的快速失败 +READ_ONLY_REQUEST_TIMEOUT = int(os.getenv('READ_ONLY_REQUEST_TIMEOUT', '45')) # Redis 缓存配置(优先从数据库,回退到环境变量和默认值) REDIS_URL = _get_config_value('REDIS_URL', os.getenv('REDIS_URL', 'redis://localhost:6379')) diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 0396b3f..632cbb8 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -2609,8 +2609,8 @@ class PositionManager: exit_reason = "stop_loss" logger.info(f"{trade.get('symbol')} [同步] 价格方向匹配止损,且亏损{pnl_percent_for_judge:.2f}% of margin,标记为止损") - # 2. 如果仍未确定,检查止盈价格匹配(作为备选) - if exit_reason == "sync" and ep > 0: + # 2. 如果仍未确定,检查止盈价格匹配(作为备选);仅盈利单可标为止盈 + if exit_reason == "sync" and ep > 0 and pnl_percent_for_judge > 0: if tp is not None and _close_to(ep, float(tp), max_pct=0.10): exit_reason = "take_profit" elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.10): @@ -2645,7 +2645,7 @@ class PositionManager: if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01): exit_reason = "trailing_stop" - # 5. 最后才看币安订单类型(作为兜底) + # 5. 最后才看币安订单类型(作为兜底);亏损单不标为止盈 if exit_reason == "sync" and latest_close_order and isinstance(latest_close_order, dict): otype = str( latest_close_order.get("type") @@ -2655,8 +2655,10 @@ class PositionManager: if "TRAILING" in otype: exit_reason = "trailing_stop" - elif "TAKE_PROFIT" in otype: + elif "TAKE_PROFIT" in otype and pnl_percent_for_judge > 0: exit_reason = "take_profit" + elif "TAKE_PROFIT" in otype and pnl_percent_for_judge < 0: + exit_reason = "stop_loss" if pnl_percent_for_judge < -5.0 else "manual" elif "STOP" in otype: exit_reason = "stop_loss" elif otype in ("MARKET", "LIMIT"):