This commit is contained in:
薇薇安 2026-02-14 17:48:50 +08:00
parent a88e114b4c
commit 1830444ef0
4 changed files with 127 additions and 16 deletions

View File

@ -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_profit2检查分步止盈后剩余仓位平仓的 exit_price/exit_reason 写入 |
| 高 | 控制止损质量 | 1观察 12 天止损的保证金%分布2若 -5%-15% 仍多,适当放宽 ATR 或入场过滤,减少假突破被扫 |
| 中 | 提高“自动锁定利润”占比 | 1适当放宽移动止损激活/保护参数2保持或微调 TP1让更多盈利由系统自动止盈/移动止损完成 |
| 中 | 入场质量 | 在可接受交易频率下,略提高 MIN_SIGNAL_STRENGTH 或趋势过滤,减少短时被止损的笔数 |
| 低 | 监控与运维 | 定期看“止盈/止损/同步”的盈亏分布,发现异常及时查 exit_reason 与成交价 |
---
## 五、简要结论
- **策略能盈利**(当日 +5.96 USDT主要来自**手动平仓**和**移动止损**,说明方向和择时整体可行。
- **主要拖累**:① 自动止损笔数多、总亏大;② “自动止盈”里多笔实际亏损,暴露出**平仓原因标记或分步止盈记库**问题。
- **建议优先**:修正止盈/止损的标记与记库逻辑,再根据新数据微调止损宽度与入场过滤,让自动止盈与移动止损在统计和实盘上更一致、可解释。
(数据来源:`交易记录_2026-02-14T09-24-06.json`,清理后本系统订单。)

View File

@ -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:
成交记录列表
"""
retries = 3
read_timeout = getattr(config, 'READ_ONLY_REQUEST_TIMEOUT', 45)
for attempt in range(retries):
try:
return await self.client.futures_account_trades(symbol=symbol, limit=limit)
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]:
"""

View File

@ -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'))

View File

@ -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"):