delete: 移除过时的文档与代码文件

删除了多个不再使用的文档和代码文件,包括交易更新推送、条件订单推送、REST API 文档、WebSocket API 文档及相关的策略分析文档。这些文件的移除有助于清理代码库,确保项目的整洁性与可维护性。
This commit is contained in:
薇薇安 2026-02-20 17:49:00 +08:00
parent f3089fdf7f
commit 13a0e7d580
64 changed files with 414 additions and 370 deletions

View File

@ -807,28 +807,9 @@ async def sync_trades_from_binance(
logger.info(f"✓ 补全开仓订单号: {symbol} (ID: {matched_trade['id']}, orderId: {order_id}, qty={order_qty}, price={order_price:.4f})")
else:
logger.debug(f"补全开仓订单号失败(可能已有订单号): {symbol} (ID: {matched_trade['id']}, orderId: {order_id})")
elif effective_sync_all:
# 全量/自动全量:无法匹配到现有记录时创建新记录
try:
leverage = 10
trade_id = Trade.create(
symbol=symbol,
side=order_side,
quantity=order_qty,
entry_price=order_price,
leverage=leverage,
entry_reason='sync_from_binance',
entry_order_id=order_id,
client_order_id=order.get('clientOrderId'),
account_id=aid,
status='open', # 先标记为 open如果后续有平仓订单会更新
)
created_count += 1
logger.info(f"✓ 创建新交易记录: {symbol} (ID: {trade_id}, orderId: {order_id}, side={order_side}, qty={order_qty}, price={order_price:.4f})")
except Exception as create_err:
logger.warning(f"创建交易记录失败 {symbol} (orderId: {order_id}): {create_err}")
else:
logger.debug(f"发现新的开仓订单 {order_id} ({symbol}, qty={order_qty}, price={order_price:.4f})但无法匹配到现有记录sync_all_symbols=False跳过创建")
# 订单统一由自动下单入 DB同步仅补全已有记录的订单号不创建新记录
logger.debug(f"发现开仓订单 {order_id} ({symbol}) 无法匹配到现有记录已跳过仅自动下单入DB")
except Exception as e:
logger.debug(f"处理开仓订单失败 {order_id}: {e}")
except Exception as e:

View File

@ -0,0 +1,11 @@
-- 可选:订单类型字段,便于统计与策略分析(开仓/平仓方式)
-- 执行前请确认表已存在;若列已存在可跳过
-- 开仓订单类型LIMIT / MARKET 等(来自币安订单 type
ALTER TABLE trades ADD COLUMN IF NOT EXISTS entry_order_type VARCHAR(32) NULL COMMENT '开仓订单类型 LIMIT/MARKET' AFTER client_order_id;
-- 平仓订单类型MARKET / STOP_MARKET / TAKE_PROFIT_MARKET 等(便于区分市价平、止损、止盈)
ALTER TABLE trades ADD COLUMN IF NOT EXISTS exit_order_type VARCHAR(32) NULL COMMENT '平仓订单类型' AFTER exit_order_id;
-- 来源口径:仅自动下单入 DB 时可固定为 auto_trade预留便于扩展
-- ALTER TABLE trades ADD COLUMN IF NOT EXISTS source VARCHAR(32) NULL DEFAULT 'auto_trade' COMMENT '记录来源 auto_trade' AFTER entry_reason;

View File

@ -890,6 +890,18 @@ class Trade:
except Exception as e:
logger.warning(f"update_open_fields trade_id={trade_id} 失败: {e}")
@staticmethod
def update_status(trade_id, status: str) -> bool:
"""更新交易记录状态(如下单失败时将 pending 标为 cancelled"""
if not trade_id or not status:
return False
try:
db.execute_update("UPDATE trades SET status = %s WHERE id = %s", (str(status).strip(), int(trade_id)))
return True
except Exception as e:
logger.warning(f"update_status trade_id={trade_id} 失败: {e}")
return False
@staticmethod
def get_by_entry_order_id(entry_order_id):
"""根据开仓订单号获取交易记录"""
@ -1127,42 +1139,54 @@ class Trade:
)
@staticmethod
def set_exit_order_id_for_open_trade(symbol: str, account_id: int, exit_order_id, entry_order_id: int = None) -> bool:
def set_exit_order_id_for_open_trade(symbol: str, account_id: int, exit_order_id, entry_order_id: int = None):
"""
ALGO_UPDATE/条件单触发后为指定 symbol 下未填 exit_order_id open 记录补全平仓订单号
ALGO_UPDATE/ORDER_TRADE_UPDATE 平仓成交为指定 symbol 下未填 exit_order_id open 记录补全平仓订单号
优先按 entry_order_id 精确匹配若无则按 symbol 匹配最早的一条 open 记录
Returns:
(True, trade_id) 成功并返回该记录 id便于后续 update_exit(False, None) 失败或未匹配
"""
if not symbol or account_id is None or exit_order_id is None:
return False
return False, None
try:
if not _table_has_column("trades", "account_id"):
return False
return False, None
# 优先按 entry_order_id 精确匹配(如果提供了 entry_order_id
if entry_order_id:
n = db.execute_update(
"""UPDATE trades SET exit_order_id = %s
row = db.execute_one(
"""SELECT id FROM trades
WHERE account_id = %s AND symbol = %s AND status = 'open'
AND entry_order_id = %s
AND (exit_order_id IS NULL OR exit_order_id = '')
AND (exit_order_id IS NULL OR exit_order_id = '' OR exit_order_id = '0')
LIMIT 1""",
(str(exit_order_id), int(account_id), symbol.strip(), str(entry_order_id))
(int(account_id), symbol.strip(), str(entry_order_id))
)
if n and n > 0:
logger.debug(f"set_exit_order_id_for_open_trade: 按 entry_order_id={entry_order_id} 精确匹配成功")
return True
if row:
db.execute_update(
"""UPDATE trades SET exit_order_id = %s WHERE id = %s""",
(str(exit_order_id), row["id"])
)
logger.debug(f"set_exit_order_id_for_open_trade: 按 entry_order_id={entry_order_id} 精确匹配成功 id={row['id']}")
return True, row["id"]
# 否则按 symbol 匹配最早的一条 open 记录(按 entry_time 排序)
n = db.execute_update(
"""UPDATE trades SET exit_order_id = %s
row = db.execute_one(
"""SELECT id FROM trades
WHERE account_id = %s AND symbol = %s AND status = 'open'
AND (exit_order_id IS NULL OR exit_order_id = '')
AND (exit_order_id IS NULL OR exit_order_id = '' OR exit_order_id = '0')
ORDER BY entry_time ASC
LIMIT 1""",
(str(exit_order_id), int(account_id), symbol.strip())
(int(account_id), symbol.strip())
)
return n is not None and n > 0
if not row:
return False, None
db.execute_update(
"""UPDATE trades SET exit_order_id = %s WHERE id = %s""",
(str(exit_order_id), row["id"])
)
return True, row["id"]
except Exception as e:
logger.warning(f"set_exit_order_id_for_open_trade 失败 symbol={symbol!r}: {e}")
return False
return False, None
class AccountSnapshot:

View File

@ -0,0 +1,132 @@
# 订单记录简化流程(支付式闭环)
参考 `订单交易更新推送.txt`、`条件订单交易更新推送.txt`,把订单记录做成「先本地单号 → 写 DB + 下单 → 仅靠 WS 推送更新状态」的闭环,和支付系统类似。
---
## 一、目标流程(你期望的)
1. **先本地生成订单号**(如 `client_order_id = SYS_时间戳_随机`)。
2. **写 DB 与下单尽量一体**:先插入一条「待成交」记录(带 `client_order_id`),再立刻用该 id 去交易所下单REST 或 WS选更稳的方式逻辑上视为「同一事务」——下单失败则把该条记录标为失败/取消。
3. **状态只跟 WS 走**:用币安 **ORDER_TRADE_UPDATE**(和必要时 **ALGO_UPDATE**驱动所有「成交、平仓、取消」等状态与字段更新DB 只根据推送更新,不依赖 REST 轮询结果做主数据。
这样:**一条 DB 记录 = 一次「开仓意图」或「平仓意图」**,用本地 id 和交易所 id 串联WS 是唯一事实来源,逻辑简单、易对账。
---
## 二、币安推送里用到的字段docs/bian
### ORDER_TRADE_UPDATE订单交易更新推送
| 字段 | 含义 | 用途 |
|------|------|------|
| `e` | 事件类型 | ORDER_TRADE_UPDATE |
| `o.c` | 客户端自定义订单 ID | **clientOrderId**,我们下单时传的,用来唯一匹配「本地这条记录」 |
| `o.i` | 订单 ID | **orderId**,交易所订单号,写 entry_order_id / exit_order_id |
| `o.x` | 本次事件类型 | NEW / TRADE / CANCELED / EXPIRED 等 |
| `o.X` | 订单当前状态 | NEW / PARTIALLY_FILLED / **FILLED** / CANCELED 等 |
| `o.ap` | 订单平均成交价 | 成交后更新 entry_price 或 exit_price |
| `o.z` | 订单累计成交量 | 成交数量 |
| `o.R` | 是否只减仓 | true = 平仓单 |
| `o.rp` | 该笔实现盈亏 | 平仓时写 pnl/realized_pnl |
**开仓**`o.R != true` 且 `o.X == FILLED` → 用 `o.c` 找到 pending 记录,更新为 open写入 `entry_order_id=o.i`、`entry_price=o.ap`、`quantity=o.z`。
**平仓**`o.R == true` 且 `o.X == FILLED` → 用 `o.s`(symbol) + 当前 open 记录匹配,写入 `exit_order_id=o.i`、`exit_price=o.ap`、`pnl` 等(可用 `o.rp`)。
### ALGO_UPDATE条件订单交易更新推送
| 字段 | 含义 | 用途 |
|------|------|------|
| `o.X` | 条件单状态 | TRIGGERED / FINISHED 表示已触发 |
| `o.ai` | 触发后普通订单 id | 平仓单触发后,用 **ai** 作为 exit_order_id并等 ORDER_TRADE_UPDATE(ai) 拿成交价、rp |
止损/止盈是「条件单」:先下 Algo 单,触发后生成一笔普通订单,推送里给 `ai`。我们应:
1在 ALGO_UPDATE 里用 `ai` 回写 `exit_order_id`
2在随后收到的 ORDER_TRADE_UPDATE`o.i == ai`)里用 `ap/z/rp` 回写 exit_price、pnl 等,这样平仓数据也闭环。
---
## 三、闭环流程(按事件串起来)
### 开仓
```
1. 生成 client_order_id = SYS_<ts>_<rand>
2. 写 DBINSERT 一条 status=pending, client_order_id=client_order_id, symbol/side/quantity/...
3. 下单REST 或 WS order.place带 newClientOrderId=client_order_id
- 若下单失败UPDATE 该条为 status=canceled 或 failed保证「有记录」且状态明确
4. 之后只依赖 WS
- 收到 ORDER_TRADE_UPDATEo.c=client_order_ido.X=FILLED非 R
- → UPDATE 该条status=open, entry_order_id=o.i, entry_price=o.ap, quantity=o.z
```
这样:**开仓是否成交、成交价/量、交易所 orderId** 全部由 WS 一次更新完成,不依赖 REST 轮询。
### 平仓(市价/限价主动平)
```
1. 不新建 DB 记录,只「选一条当前 open 记录」准备关仓
2. 下单reduceOnly 市价/限价单(可带 newClientOrderId 便于对账)
3. 只依赖 WS
- 收到 ORDER_TRADE_UPDATEo.R=trueo.X=FILLEDo.s=symbol
- → 按 symbol(+ 可选 orderId/clientOrderId) 匹配那条 open
- → UPDATEstatus=closed, exit_order_id=o.i, exit_price=o.ap, pnl/realized_pnl 等(可用 o.rp
```
### 平仓(条件单触发:止损/止盈)
```
1. 不新建 DB 记录,只为当前 open 挂 Algo 单STOP_MARKET/TAKE_PROFIT_MARKET 等)
2. 触发后只依赖 WS
- 先收到 ALGO_UPDATEo.X=TRIGGERED/FINISHEDo.ai=触发后订单 id
→ UPDATE 该 openexit_order_id=o.ai先占位
- 再收到 ORDER_TRADE_UPDATEo.i=o.aio.X=FILLEDo.R=true
→ 同一条记录exit_price=o.appnl/realized_pnl 用 o.rp必要时补 duration/exit_time
```
这样:**条件单触发的平仓**也完全由 WS 闭环,不依赖 REST 或定时同步做主数据。
---
## 四、和当前实现的对应关系
| 目标步骤 | 当前实现 | 说明 |
|----------|----------|------|
| 本地先生成 client_order_id | ✅ 已有 | position_manager 里 `SYS_ts_rand`,并传 `newClientOrderId` |
| 先写 DB 再下单 | ✅ 已有 | 先 `Trade.create(..., status='pending', client_order_id=...)`,再下单 |
| 下单失败把记录标失败 | ⚠️ 部分 | 有失败路径,但不一定统一 UPDATE 为 canceled/failed |
| 开仓成交只靠 WS 更新 | ✅ 已有 | User Data Stream 里 ORDER_TRADE_UPDATE FILLED + 非 R → `update_pending_to_filled(client_order_id, ..., order_id, ap, z)` |
| 平仓成交只靠 WS 更新 exit_order_id | ✅ 已有 | ORDER_TRADE_UPDATE FILLED + R → `set_exit_order_id_for_open_trade(symbol, account_id, order_id)` |
| 平仓成交用 WS 更新 exit_price / pnl | ❌ 缺口 | 目前只写了 exit_order_id**没有**在 WS 里用 `ap/rp``update_exit(..., exit_price, pnl, ...)`,条件单触发的平仓尤其缺 |
| 条件单触发用 ai 写 exit_order_id | ✅ 已有 | ALGO_UPDATE 里 `set_exit_order_id_for_open_trade(symbol, account_id, ai)` |
| 「DB + 下单」原子性 | ⚠️ 语义上的 | DB 与交易所是两套系统,无法真 2PC只能「先写 DB 再下单,失败则把该条标为失败」 |
所以:**主流程已经接近「支付式」**,真正缺的闭环是:**WS 收到平仓成交(含条件单触发的 ai 那笔)时,不仅要写 exit_order_id还要用推送里的 ap/rp 等把 exit_price、pnl、exit_time 等一次更新掉**。
---
## 五、建议补的一步(闭环平仓数据)
**User Data Stream** 里,当 `ORDER_TRADE_UPDATE``o.R == true``o.X == FILLED` 时,除现有 `set_exit_order_id_for_open_trade(symbol, account_id, order_id)` 外,建议:
- 用同一推送里的 `o.ap`、`o.z`、`o.rp`(及可选 `o.N`/`o.n` 手续费)和当前时间(或 `o.T` 成交时间),对**同一条 open 记录**再调一次 **update_exit**(或等价的「按 exit_order_id 补全 exit_price / pnl / commission」接口
- exit_price
- pnl / pnl_percent或 realized_pnl
- commission若有
- exit_time
都从 WS 一次写库。这样:
- 市价/限价平仓ORDER_TRADE_UPDATE 一次即可完成「exit_order_id + 价格 + 盈亏」闭环。
- 条件单平仓ALGO_UPDATE 写 exit_order_id → 等 ORDER_TRADE_UPDATE(ai) 再按上面补全价格和盈亏,也闭环。
---
## 六、小结
- **你的理解**:先本地单号 → 写 DB + 下单(尽量一体)→ 只靠 WS 通知更新订单状态与成交数据,是正确且更清晰的设计。
- **当前实现**:开仓侧已经是「先生成 client_order_id、先写 pending、再下单、WS 完善」;平仓侧 **exit_order_id** 已由 WS 回写,但 **exit_price / pnl 等还未在 WS 路径里统一写库**,算一个遗漏。
- **补上这一块**WS 平仓成交时顺带 update_exit 价格与盈亏),就能在「不依赖 REST 轮询、不依赖同步接口做主数据」的前提下,做到订单记录与币安完全以 WS 为源的闭环。
- **事务**DB 与交易所无法真事务,只能「先 DB 再下单,失败则把该条记录标为失败/取消」,当前逻辑已接近,可再统一失败态标记。
文档参考:`docs/bian/订单交易更新推送.txt`、`docs/bian/条件订单交易更新推送.txt`。

View File

@ -0,0 +1,158 @@
# 订单记录与币安对账流程
本文梳理:**订单是怎么记的**、**何时写入 entry_order_id / exit_order_id**、**如何与币安保持一致**。
---
## 一、数据模型(与币安对应的关键字段)
| 字段 | 含义 | 与币安对应 |
|------|------|------------|
| `entry_order_id` | 币安开仓订单号 (orderId) | 开仓订单唯一标识,用于与币安「开仓订单」一一对应 |
| `exit_order_id` | 币安平仓订单号 (orderId) | 平仓订单唯一标识,用于与币安「平仓订单」一一对应 |
| `client_order_id` | 币安 clientOrderId | 系统下单时用 `SYSTEM_ORDER_ID_PREFIX_时间戳_随机`,便于区分本系统单 |
| `entry_time` | 开仓时间 | 建议从币安订单/成交取真实时间,便于统计与早止盈 |
| `status` | open / closed / pending | open=持仓中closed=已平仓pending=已下单待成交 |
**「可对账」定义**(与币安一致的口径):
- 有 `entry_order_id`(非空且不为 0
- 若 `status = closed`,还须有 `exit_order_id`(非空且不为 0
接口默认 `reconciled_only=true` 时,只返回/统计满足上述条件的记录。
---
## 二、订单记录是在哪里写的?
### 1. 本系统开仓(策略/限价开仓)
**位置**`trading_system/position_manager.py`(开仓成功、成交确认后)
- **流程**:下单 → 等待成交REST 轮询或 WS→ 取到 `orderId`、成交价、数量 → 写库。
- **写库方式**
- 若之前已建 `status=pending`(限价先挂单):用 `Trade.update_pending_to_filled(client_order_id, account_id, entry_order_id, entry_price, quantity)``Trade.update_pending_by_entry_order_id(symbol, account_id, entry_order_id, entry_price, quantity)` 完善为 open并写入 `entry_order_id`
- 否则直接 `Trade.create(..., entry_order_id=orderId, client_order_id=..., status='open', entry_time=...)`
- **entry_order_id 来源**REST 下单返回的 `order.get("orderId")`,或成交确认时从订单查询得到。
- **entry_time**:支持传入;不传则用当前北京时间;补建/手动同步路径会从币安订单或成交取真实开仓时间。
**结论**:本系统开的仓,在正常落库且无异常时,**开仓时就会带上 entry_order_id**,与币安开仓订单一一对应。
---
### 2. User Data StreamWS 推送)补全订单号
**位置**`trading_system/user_data_stream.py`
- **开仓成交 (ORDER_TRADE_UPDATE, X=FILLED, 非 reduceOnly)**
- 若有 `clientOrderId``Trade.update_pending_to_filled(client_order_id, account_id, order_id, price, quantity)`,把对应 pending 记录完善为 open 并写入 `entry_order_id`
- 若无 clientOrderId`Trade.update_pending_by_entry_order_id(symbol, account_id, order_id, price, quantity)`,用 symbol+account 下「唯一一条 pending 且无 entry_order_id」的记录做兜底补全。
- **平仓成交 (ORDER_TRADE_UPDATE, X=FILLED, reduceOnly)**
- `Trade.set_exit_order_id_for_open_trade(symbol, account_id, order_id, entry_order_id_hint)`:给该 symbol 下当前 open 且无 exit_order_id 的记录写入平仓订单号(有 entry_order_id 时优先按 entry_order_id 精确匹配)。
- **条件单触发 (ALGO_UPDATE, X=TRIGGERED/FINISHED)**
- `ai` = 触发后的普通订单 orderId同样调用 `Trade.set_exit_order_id_for_open_trade(symbol, account_id, ai, ...)` 回写 `exit_order_id`
**结论**WS 负责在「开仓/平仓成交或条件单触发」时,把币安订单号回写到 DB保证与币安一致。
---
### 3. 本系统平仓(止损/止盈/手动平仓)
**位置**`trading_system/position_manager.py`close_position 成功后)
- **流程**:市价平仓或条件单触发 → 拿到平仓订单号 → `Trade.update_exit(trade_id, exit_price, exit_reason, pnl, pnl_percent, exit_order_id=..., exit_time_ts=..., commission=..., realized_pnl=...)`
- **exit_order_id 来源**:平仓接口返回的 `order.get('orderId')`,或从 `get_recent_trades` 按订单号汇总。
- **commission / realized_pnl**:从币安成交 `get_recent_trades` 按该订单号汇总,写入 DB统计与币安一致。
**结论**:本系统平的仓,平仓路径会写入 `exit_order_id` 及手续费/实际盈亏,与币安平仓订单一致。
---
### 4. 补建「币安有仓、DB 无记录」(状态同步)
**位置**`trading_system/position_manager.py`sync_positions_with_binance
- **何时发生**:定时同步或调用 `POST /api/account/positions/sync` 时,发现某 symbol 在币安有持仓,但 DB 没有对应 open 记录。
- **补建逻辑**(简要):
- 若配置了 `SYSTEM_ORDER_ID_PREFIX`:会查该 symbol 的开仓订单(如 get_all_orders / get_recent_trades取**同方向、时间合理**的订单作为 `entry_order_id`;若查到 `clientOrderId` 且**以系统前缀开头**则视为系统单并补建;若**明确不以系统前缀开头**则视为手动单,按配置可跳过或仍补建(如开启 SYNC_CREATE_MANUAL_ENTRY_RECORD
- 补建时调用 `Trade.create(..., entry_reason='sync_recovered' 或 'manual_entry', entry_order_id=..., entry_time=...)`**尽量从币安订单/成交取真实 entry_order_id 和 entry_time**。
- **entry_order_id 来源**`futures_get_order(entry_order_id)` 或同方向成交中最早一笔的 orderId若拿不到则可能为空后续靠「同步币安订单」补全
**结论**:补建记录会尽量带上 `entry_order_id` 和真实开仓时间;若当时拿不到,需依赖「同步币安订单」补全。
---
### 5. 同步币安订单(后端接口)
**位置**`backend/api/routes/trades.py``POST /api/trades/sync-binance`
- **作用**:按时间范围拉取币安历史订单,与 DB 对齐:**补全缺失的 entry_order_id / exit_order_id**,必要时**新建 DB 记录**。
- **开仓订单**
- 若 `Trade.get_by_entry_order_id(order_id)` 已存在 → 跳过。
- 否则在该 symbol 下找「时间窗口内且无 entry_order_id」的记录按价格/数量匹配(允许少量误差)→ 匹配到则 `Trade.update_entry_order_id(trade_id, order_id)` 补全;匹配不到则若开启「全量同步」可 `Trade.create(..., entry_order_id=order_id, entry_reason='sync_from_binance', status='open')` 新建。
- **平仓订单 (reduceOnly)**
- 若 `Trade.get_by_exit_order_id(order_id)` 已存在 → 视情况跳过。
- 否则找该 symbol 的 open 或「已 closed 但 exit_order_id 为空」的记录,匹配后 `Trade.update_exit(..., exit_order_id=order_id, ...)`,并可从成交拉取 commission/realized_pnl 写入。
**结论****与币安订单一致的关键一步**。DB 里已有记录但缺订单号时,用此接口可把 entry_order_id / exit_order_id 补全;若 DB 完全没有记录且开了全量同步,会按币安开仓订单新建记录,保证「订单记录」与币安可对账。
---
## 三、如何与币安订单一致?(操作与口径)
### 1. 保证「有订单号」
- **本系统开/平仓**:正常落库 + WS 回写,会自动有 `entry_order_id` / `exit_order_id`
- **补建/手动同步**:补建时已尽量从币安取 `entry_order_id``entry_time`;若仍缺订单号(如历史旧数据),可:
- 在前端「交易记录」页使用 **「同步订单」**(即 `POST /api/trades/sync-binance`),选择时间范围(如今天/7 天),必要时勾选「全量同步」;
- 同步后会补全「开仓/平仓订单号」,并可能新建缺失记录;同步结果会提示补全了多少个 entry_order_id / exit_order_id。
### 2. 只用「可对账」口径看订单与统计
- 前端「交易记录」默认 **勾选「仅可对账(与币安一致)」**`reconciled_only=true`)。
- 接口行为:
- `GET /api/trades?reconciled_only=true`:只返回「有 entry_order_id且若已平仓则有 exit_order_id」的记录。
- `GET /api/trades/stats?reconciled_only=true`:只统计上述记录,日盈亏、胜率、总盈亏与币安一致。
- 这样**订单记录与统计都只基于「能和币安一一对上」的数据**,避免无订单号或脏数据干扰。
### 3. 主动校验是否一致
- **接口**`GET /api/trades/verify-binance?days=30&limit=100`(需登录与账号)。
- **作用**:对「可对账」记录逐条用币安 `futures_get_order` 校验开仓/平仓订单是否存在、symbol/side/数量是否一致;返回一致/缺失/不一致数量及明细。
- 建议定期或在对账有疑问时调用,确认 DB 与币安一致。
---
## 四、流程简图(何时有 entry_order_id / exit_order_id
```
本系统开仓
→ 下单得到 orderId
→ Trade.create(..., entry_order_id=orderId) 或 update_pending_to_filled / update_pending_by_entry_order_id
→ [WS] ORDER_TRADE_UPDATE FILLED 可再次补全 entry_order_id若之前未写入
本系统平仓
→ 平仓得到 orderId
→ Trade.update_exit(..., exit_order_id=orderId)
→ [WS] ORDER_TRADE_UPDATE FILLED / ALGO_UPDATE 可再次补全 exit_order_id若之前未写入
币安有仓、DB 无(补建)
→ Trade.create(..., entry_order_id=从币安订单/成交取, entry_time=从币安取)
→ 若当时取不到 entry_order_id后续靠「同步币安订单」补全
同步币安订单 (POST /api/trades/sync-binance)
→ 开仓订单:匹配 DB 无 entry_order_id 的记录 → update_entry_order_id或新建 Trade(..., entry_order_id=orderId)
→ 平仓订单:匹配 open 或 closed 无 exit_order_id → update_exit(..., exit_order_id=orderId)
→ 结果DB 与币安在「谁开的、谁平的」上一致
```
---
## 五、小结
| 问题 | 答案 |
|------|------|
| 订单是怎么记录的? | 开仓:`Trade.create` 或 update pending → open并写 `entry_order_id`;平仓:`Trade.update_exit` 写 `exit_order_id`。补建与同步也会创建/更新记录。 |
| entry_order_id 从哪来? | 本系统开仓:下单/成交返回的 orderId补建币安 get_all_orders / get_recent_trades / futures_get_order缺失时同步币安订单接口按时间/价格匹配补全。 |
| exit_order_id 从哪来? | 本系统平仓:平仓接口返回的 orderIdWSORDER_TRADE_UPDATE / ALGO_UPDATE 回写;同步币安订单:按 reduceOnly 订单匹配并 update_exit。 |
| 如何与币安一致? | 1保证写库路径都尽量写入订单号2缺订单号时用「同步订单」补全3前端与统计只用「仅可对账」口径reconciled_only=true4需要时用 verify-binance 接口校验。 |

View File

@ -538,6 +538,8 @@ class PositionManager:
new_client_order_id=client_order_id
)
if not order:
if pending_trade_id and DB_AVAILABLE and Trade:
Trade.update_status(pending_trade_id, "cancelled")
return None
entry_order_id = order.get("orderId")
if entry_order_id:
@ -556,6 +558,8 @@ class PositionManager:
new_client_order_id=client_order_id
)
if not order:
if pending_trade_id and DB_AVAILABLE and Trade:
Trade.update_status(pending_trade_id, "cancelled")
return None
entry_order_id = order.get("orderId")
if entry_order_id:
@ -640,6 +644,8 @@ class PositionManager:
new_client_order_id=client_order_id
)
if not order:
if pending_trade_id and DB_AVAILABLE and Trade:
Trade.update_status(pending_trade_id, "cancelled")
self._pending_entry_orders.pop(symbol, None)
return None
entry_order_id = order.get("orderId")
@ -1173,202 +1179,12 @@ class PositionManager:
if order:
order_id = order.get('orderId')
logger.info(f"{symbol} [平仓] ✓ 平仓订单已提交 (订单ID: {order_id})")
# 等待订单成交,然后从币安获取实际成交价格
exit_price = None
try:
# 等待一小段时间让订单成交
await asyncio.sleep(1)
# 从币安获取订单详情,获取实际成交价格
try:
order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=order_id, recvWindow=20000)
if order_info:
# 优先使用平均成交价格avgPrice如果没有则使用价格字段
exit_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0))
if exit_price > 0:
logger.info(f"{symbol} [平仓] 从币安订单获取实际成交价格: {exit_price:.4f} USDT")
else:
# 如果订单还没有完全成交,尝试从成交记录获取
if order_info.get('status') == 'FILLED' and order_info.get('fills'):
# 计算加权平均成交价格
total_qty = 0
total_value = 0
for fill in order_info.get('fills', []):
qty = float(fill.get('qty', 0))
price = float(fill.get('price', 0))
total_qty += qty
total_value += qty * price
if total_qty > 0:
exit_price = total_value / total_qty
logger.info(f"{symbol} [平仓] 从成交记录计算平均成交价格: {exit_price:.4f} USDT")
except Exception as order_error:
logger.warning(f"{symbol} [平仓] 获取订单详情失败: {order_error},使用备用方法")
# 如果无法从订单获取价格,使用当前价格作为备用
if not exit_price or exit_price <= 0:
ticker = await self.client.get_ticker_24h(symbol)
if ticker:
exit_price = float(ticker['price'])
logger.warning(f"{symbol} [平仓] 使用当前价格作为平仓价格: {exit_price:.4f} USDT")
else:
exit_price = float(order.get('avgPrice', 0)) or float(order.get('price', 0))
if exit_price <= 0:
logger.error(f"{symbol} [平仓] 无法获取平仓价格,使用订单价格字段")
exit_price = float(order.get('price', 0))
except Exception as price_error:
logger.warning(f"{symbol} [平仓] 获取成交价格时出错: {price_error},使用当前价格")
ticker = await self.client.get_ticker_24h(symbol)
exit_price = float(ticker['price']) if ticker else float(order.get('price', 0))
# 更新数据库记录
if DB_AVAILABLE and Trade and symbol in self.active_positions:
position_info = self.active_positions[symbol]
trade_id = position_info.get('tradeId')
if trade_id:
try:
logger.info(f"正在更新 {symbol} 平仓记录到数据库 (ID: {trade_id})...")
# 计算盈亏确保所有值都是float类型
entry_price = float(position_info['entryPrice'])
quantity_float = float(quantity)
exit_price_float = float(exit_price)
if position_info['side'] == 'BUY':
pnl = (exit_price_float - entry_price) * quantity_float
pnl_percent = ((exit_price_float - entry_price) / entry_price) * 100
else: # SELL
pnl = (entry_price - exit_price_float) * quantity_float
pnl_percent = ((entry_price - exit_price_float) / entry_price) * 100
# 获取平仓订单号
exit_order_id = order.get('orderId')
if exit_order_id:
logger.info(f"{symbol} [平仓] 币安订单号: {exit_order_id}")
# -----------------------------------------------------------
# 新增:获取实际成交详情(佣金、资金费率、实际盈亏)
# -----------------------------------------------------------
realized_pnl = None
commission = None
commission_asset = None
try:
# 等待一小段时间确保成交记录已生成
await asyncio.sleep(2)
# 获取最近的成交记录
recent_trades = await self.client.get_recent_trades(symbol, limit=10)
# 筛选属于当前平仓订单的成交记录
# 注意:一次平仓可能对应多条成交记录(分批成交)
related_trades = []
if exit_order_id:
related_trades = [t for t in recent_trades if str(t.get('orderId')) == str(exit_order_id)]
else:
# 如果没有订单号(极少见),尝试通过时间匹配
# TODO: 暂时跳过,风险较大
pass
if related_trades:
total_realized_pnl = 0.0
total_commission = 0.0
commission_assets = set()
for t in related_trades:
total_realized_pnl += float(t.get('realizedPnl', 0))
total_commission += float(t.get('commission', 0))
commission_assets.add(t.get('commissionAsset'))
realized_pnl = total_realized_pnl
commission = total_commission
commission_asset = "/".join(commission_assets) if commission_assets else None
logger.info(
f"{symbol} [平仓] 获取到实际成交详情: "
f"实际盈亏={realized_pnl} USDT, 佣金={commission} {commission_asset}"
)
else:
logger.warning(f"{symbol} [平仓] 未找到订单 {exit_order_id} 的成交记录,无法记录佣金")
except Exception as fee_error:
logger.warning(f"{symbol} [平仓] 获取成交详情失败: {fee_error}")
# 计算持仓持续时间
entry_time = position_info.get('entryTime')
duration_minutes = None
if entry_time:
try:
if isinstance(entry_time, str):
entry_dt = datetime.strptime(entry_time, '%Y-%m-%d %H:%M:%S')
else:
entry_dt = entry_time
exit_dt = get_beijing_time() # 使用北京时间计算持续时间
duration = exit_dt - entry_dt
duration_minutes = int(duration.total_seconds() / 60)
except Exception as e:
logger.debug(f"计算持仓持续时间失败: {e}")
# 获取策略类型(从开仓原因或持仓信息中获取)
strategy_type = position_info.get('strategyType', 'trend_following') # 默认趋势跟踪
# 网络/DB 超时时重试,避免 TimeoutError 导致平仓记录未更新
db_update_retries = 3
for db_attempt in range(db_update_retries):
try:
Trade.update_exit(
trade_id=trade_id,
exit_price=exit_price_float,
exit_reason=reason,
pnl=pnl,
pnl_percent=pnl_percent,
exit_order_id=exit_order_id, # 保存币安平仓订单号
strategy_type=strategy_type,
duration_minutes=duration_minutes,
realized_pnl=realized_pnl,
commission=commission,
commission_asset=commission_asset
)
logger.info(
f"{symbol} [平仓] ✓ 数据库记录已更新 "
f"(盈亏: {pnl:.2f} USDT, {pnl_percent:.2f}%, 原因: {reason})"
)
break
except Exception as e:
err_msg = str(e).strip() or f"{type(e).__name__}"
if db_attempt < db_update_retries - 1:
wait_sec = 2
logger.warning(
f"{symbol} [平仓] 更新数据库失败 (第 {db_attempt + 1}/{db_update_retries} 次): {err_msg}{wait_sec}秒后重试"
)
await asyncio.sleep(wait_sec)
else:
logger.error(f"❌ 更新平仓记录到数据库失败: {e}")
logger.error(f" 错误类型: {type(e).__name__}")
import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}")
except Exception as db_error:
logger.error(f"{symbol} [平仓] 更新平仓记录到数据库时发生异常: {db_error}")
logger.error(f" 错误类型: {type(db_error).__name__}")
import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}")
else:
logger.warning(f"{symbol} 没有关联的数据库交易ID无法更新平仓记录")
elif not DB_AVAILABLE:
logger.debug(f"数据库不可用,跳过更新 {symbol} 平仓记录")
elif not Trade:
logger.warning(f"Trade模型未导入无法更新 {symbol} 平仓记录")
# 停止WebSocket监控
logger.info(f"{symbol} [平仓] ✓ 平仓订单已提交 (订单ID: {order_id})DB 由 WS ORDER_TRADE_UPDATE 更新")
# 支付式闭环:平仓数据仅由 WS 推送更新,此处只做本地清理
await self._stop_position_monitoring(symbol)
# 移除持仓记录
if symbol in self.active_positions:
del self.active_positions[symbol]
logger.info(
f"{symbol} [平仓] ✓ 平仓完成: {side} {quantity:.4f} @ {exit_price:.4f} "
f"(原因: {reason})"
)
logger.info(f"{symbol} [平仓] ✓ 平仓完成: {side} {quantity:.4f} (原因: {reason})")
return True
else:
# place_order 返回 None可能是 -2022ReduceOnly rejected等竞态场景
@ -3250,6 +3066,12 @@ class PositionManager:
sync_create_manual = config.TRADING_CONFIG.get("SYNC_CREATE_MANUAL_ENTRY_RECORD", False)
sync_recover = config.TRADING_CONFIG.get("SYNC_RECOVER_MISSING_POSITIONS", True)
sync_recover_only_has_sltp = config.TRADING_CONFIG.get("SYNC_RECOVER_ONLY_WHEN_HAS_SLTP", True)
# 订单统一由自动下单入 DB同步/持仓 sync 不创建新记录,仅 WS 与自动开仓写库
if config.TRADING_CONFIG.get("ONLY_AUTO_TRADE_CREATES_RECORDS", True):
sync_recover = False
sync_create_manual = False
if missing_in_db:
logger.debug(f" ONLY_AUTO_TRADE_CREATES_RECORDS=True跳过补建/手动开仓创建 ({len(missing_in_db)} 个仅币安持仓)")
if sync_recover:
system_order_prefix = (config.TRADING_CONFIG.get("SYSTEM_ORDER_ID_PREFIX") or "").strip()
@ -3712,7 +3534,8 @@ class PositionManager:
'maxProfit': 0.0,
'trailingStopActivated': False
}
if not sync_create_manual and DB_AVAILABLE and Trade:
# 订单统一由自动下单入 DB此处仅做内存监控不创建 DB 记录
if not sync_create_manual and DB_AVAILABLE and Trade and not config.TRADING_CONFIG.get("ONLY_AUTO_TRADE_CREATES_RECORDS", True):
try:
notional = float(entry_price) * quantity
trade_id = Trade.create(

View File

@ -443,7 +443,7 @@ class UserDataStream:
return True
if e == "ORDER_TRADE_UPDATE":
logger.debug(f"UserDataStream: 收到 ORDER_TRADE_UPDATE 推送")
self._on_order_trade_update(data.get("o") or {})
self._on_order_trade_update(data.get("o") or {}, event_time_ms=data.get("E"))
elif e == "ACCOUNT_UPDATE":
logger.debug(f"UserDataStream: 收到 ACCOUNT_UPDATE 推送")
self._on_account_update(data.get("a") or {})
@ -454,9 +454,9 @@ class UserDataStream:
logger.debug(f"UserDataStream: 收到未知事件类型: {e}")
return False
def _on_order_trade_update(self, o: Dict):
def _on_order_trade_update(self, o: Dict, event_time_ms=None):
# 文档: x=本次事件执行类型(NEW/TRADE/CANCELED等), X=订单当前状态, c=clientOrderId, i=orderId
# ap=均价, z=累计成交量, R=只减仓, rp=该交易实现盈亏, s=交易对
# ap=均价, z=累计成交量, R=只减仓, rp=该交易实现盈亏, s=交易对, n=手续费数量, N=手续费资产
event_type = (o.get("x") or "").strip().upper()
status = (o.get("X") or "").strip().upper()
symbol = (o.get("s") or "").strip()
@ -515,9 +515,7 @@ class UserDataStream:
except Exception as ex:
logger.warning(f"UserDataStream: 开仓成交完善失败 orderId={order_id}: {ex}")
else:
# 平仓成交:按 symbol 回写 open 记录的 exit_order_id若有 rp 可记入日志
if rp is not None:
logger.debug(f"UserDataStream: 平仓订单 FILLED orderId={order_id} symbol={symbol!r} 实现盈亏 rp={rp}")
# 平仓成交(支付式闭环):回写 exit_order_id 并用推送数据更新 exit_price/pnl/commission仅 WS 驱动 DB
if symbol:
try:
import sys
@ -527,15 +525,55 @@ class UserDataStream:
if backend_path.exists():
sys.path.insert(0, str(backend_path))
from database.models import Trade
# 尝试从订单信息中获取关联的开仓订单号(如果有)
# 注意:币安平仓订单推送中可能不包含开仓订单号,这里先按 symbol 匹配
entry_order_id_hint = None # 未来可从订单关联信息中提取
if Trade.set_exit_order_id_for_open_trade(symbol, self.account_id, order_id, entry_order_id_hint):
ok, trade_id = Trade.set_exit_order_id_for_open_trade(
symbol, self.account_id, order_id, entry_order_id_hint=None
)
if ok:
logger.info(f"UserDataStream: 平仓订单已回写 exit_order_id symbol={symbol!r} orderId={order_id}")
else:
logger.debug(f"UserDataStream: 平仓订单回写失败可能已存在或无可匹配记录symbol={symbol!r} orderId={order_id}")
# 可能已由 ALGO_UPDATE 写过 exit_order_id直接按 exit_order_id 取记录
tid = trade_id
if tid is None:
row = Trade.get_by_exit_order_id(order_id)
tid = row.get("id") if row else None
if tid is not None:
try:
rp_f = float(rp) if rp is not None else None
commission_f = None
try:
n_val = o.get("n")
if n_val is not None:
commission_f = float(n_val)
except (TypeError, ValueError):
pass
exit_time_ts = None
if event_time_ms is not None:
try:
exit_time_ts = int(int(event_time_ms) / 1000)
except (TypeError, ValueError):
pass
# 用推送的 ap/rp 更新平仓信息,与币安一致
Trade.update_exit(
trade_id=tid,
exit_price=ap_f,
exit_reason="sync",
pnl=rp_f if rp_f is not None else 0.0,
pnl_percent=0.0,
exit_order_id=order_id,
exit_time_ts=exit_time_ts,
realized_pnl=rp_f,
commission=commission_f,
commission_asset=o.get("N"),
)
logger.info(
f"UserDataStream: 平仓记录已更新 symbol={symbol!r} orderId={order_id} "
f"exit_price={ap_f} realized_pnl={rp_f}"
)
except Exception as ex2:
logger.warning(f"UserDataStream: update_exit 失败 orderId={order_id}: {ex2}")
elif not ok:
logger.debug(f"UserDataStream: 平仓订单未匹配到 open 记录 symbol={symbol!r} orderId={order_id}")
except Exception as ex:
logger.warning(f"UserDataStream: set_exit_order_id_for_open_trade 失败 {ex}")
logger.warning(f"UserDataStream: 平仓回写/更新失败 orderId={order_id}: {ex}")
def _on_account_update(self, a: Dict):
# 文档: a.B = 余额数组a.P = 持仓信息数组。有 Redis 时只写 Redis、不写进程内存。
@ -620,7 +658,8 @@ class UserDataStream:
if backend_path.exists():
sys.path.insert(0, str(backend_path))
from database.models import Trade
if Trade.set_exit_order_id_for_open_trade(symbol, self.account_id, ai):
ok, _ = Trade.set_exit_order_id_for_open_trade(symbol, self.account_id, ai)
if ok:
logger.info(f"UserDataStream: 条件单触发已回写 exit_order_id symbol={symbol!r} ai={ai}")
except Exception as ex:
logger.warning(f"UserDataStream: ALGO_UPDATE set_exit_order_id 失败 {ex}")

View File

@ -1,124 +0,0 @@
# 日志问题分析
## 1. IPUSDT 持仓状态分析
**日志信息**:
```
IPUSDT [实时监控] 诊断:
• ROE(保证金盈亏): -5.83% (用户关注)
• 价格变动: -1.46% (实际币价涨跌)
• 杠杆倍数: 4.0x (放大倍数)
• 当前价: 1.1490 | 入场价: 1.1660
• 止损价: 1.1310 (目标ROE: -12.00%)
• 触发止损: NO
```
**分析**:
- ✅ **计算验证**:
- 价格变动: (1.1490 - 1.1660) / 1.1660 = -1.46% ✓
- ROE: -1.46% × 4.0 = -5.84% ≈ -5.83% ✓
- ✅ **止损状态**:
- 当前价 1.1490 > 止损价 1.1310,未触发止损 ✓
- 距离止损: (1.1490 - 1.1310) / 1.1310 = 1.59%
- 如果价格继续下跌到 1.1310ROE 将达到 -12.00%
- ⚠️ **风险提示**:
- 当前亏损 -5.83%,距离止损还有 1.59% 的价格空间
- 如果价格继续下跌,可能会触发止损
## 2. User Data Stream listenKey 创建失败
**错误信息**:
```
create_futures_listen_key 失败: 请求超时(可检查该账号网络或代理)
UserDataStream(account_id=2): 重新创建 listenKey 失败60s 后重试
```
**可能原因**:
1. **网络连接问题**:
- 到币安 API 的网络不稳定
- 代理设置问题
- 防火墙阻止连接
2. **API 权限问题**:
- API Key 未启用 "Enable Reading"
- API Key 未启用 "Enable Futures"
- API Key 已过期或被禁用
3. **IP 白名单限制**:
- 当前 IP 不在 API Key 的 IP 白名单中
- IP 白名单配置错误
4. **币安服务问题**:
- 币安 API 服务暂时不可用
- API 限流或维护中
**影响**:
- ❌ 无法实时接收订单/持仓/余额推送
- ❌ 订单号可能无法及时同步到数据库
- ⚠️ 系统会每 60 秒重试创建 listenKey
## 3. 网络超时问题
**日志显示**:
- 大量 `TimeoutError` 错误
- 获取持仓信息失败(重试 7 次后失败)
- 获取成交记录失败(重试 5 次后失败)
**建议排查步骤**:
### 步骤 1: 检查网络连接
```bash
# 测试币安 API 连接
curl -I https://fapi.binance.com/fapi/v1/ping
# 测试 listenKey 创建(需要 API Key
curl -X POST "https://fapi.binance.com/fapi/v1/listenKey" \
-H "X-MBX-APIKEY: YOUR_API_KEY"
```
### 步骤 2: 检查 API Key 权限
1. 登录币安账户
2. 进入 API 管理页面
3. 检查账号 2 的 API Key
- ✅ Enable Reading
- ✅ Enable Futures
- ✅ IP 白名单设置(如果启用了)
### 步骤 3: 运行诊断工具
```bash
cd trading_system
python3 -m trading_system.check_user_data_stream
```
### 步骤 4: 检查代理设置
如果使用了代理,检查:
- 代理服务器是否正常运行
- 代理配置是否正确
- 代理是否支持 HTTPS 连接
## 4. 建议解决方案
### 短期方案(立即执行):
1. **检查网络连接**: 确认服务器到币安的网络是否正常
2. **验证 API Key**: 确认账号 2 的 API Key 权限和 IP 白名单
3. **增加超时时间**: 如果网络较慢,可以增加超时时间(当前 10 秒)
### 长期方案(优化):
1. **增加重试机制**: 对于 listenKey 创建失败,增加指数退避重试
2. **监控告警**: 当 listenKey 创建失败时发送告警
3. **降级方案**: 当 WS 不可用时,增加 REST API 轮询频率
## 5. 查看详细日志
```bash
# 查看最近的 listenKey 相关日志
tail -100 trading_2.out.log | grep -i "listen_key\|UserDataStream"
# 查看 IPUSDT 相关日志
tail -100 trading_2.out.log | grep -i "IPUSDT"
# 查看所有超时错误
tail -200 trading_2.out.log | grep -i "TimeoutError"
```