1
This commit is contained in:
parent
a88e114b4c
commit
1830444ef0
83
docs/交易分析_2026-02-14_策略执行与优化建议.md
Normal file
83
docs/交易分析_2026-02-14_策略执行与优化建议.md
Normal 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_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`,清理后本系统订单。)
|
||||
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user