delete: 移除过时的文档与代码文件
删除了多个不再使用的文档和代码文件,包括交易更新推送、条件订单推送、REST API 文档、WebSocket API 文档及相关的策略分析文档。这些文件的移除有助于清理代码库,确保项目的整洁性与可维护性。
This commit is contained in:
parent
f3089fdf7f
commit
13a0e7d580
|
|
@ -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:
|
||||
|
|
|
|||
11
backend/database/add_order_type_fields.sql
Normal file
11
backend/database/add_order_type_fields.sql
Normal 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;
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
132
docs/bian/订单记录简化流程(支付式闭环).md
Normal file
132
docs/bian/订单记录简化流程(支付式闭环).md
Normal 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. 写 DB:INSERT 一条 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_UPDATE,o.c=client_order_id,o.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_UPDATE,o.R=true,o.X=FILLED,o.s=symbol
|
||||
- → 按 symbol(+ 可选 orderId/clientOrderId) 匹配那条 open
|
||||
- → UPDATE:status=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_UPDATE:o.X=TRIGGERED/FINISHED,o.ai=触发后订单 id
|
||||
→ UPDATE 该 open:exit_order_id=o.ai(先占位)
|
||||
- 再收到 ORDER_TRADE_UPDATE:o.i=o.ai,o.X=FILLED,o.R=true
|
||||
→ 同一条记录:exit_price=o.ap,pnl/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`。
|
||||
158
docs/common/订单记录与币安对账流程.md
Normal file
158
docs/common/订单记录与币安对账流程.md
Normal 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 Stream(WS 推送)补全订单号
|
||||
|
||||
**位置**:`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 从哪来? | 本系统平仓:平仓接口返回的 orderId;WS:ORDER_TRADE_UPDATE / ALGO_UPDATE 回写;同步币安订单:按 reduceOnly 订单匹配并 update_exit。 |
|
||||
| 如何与币安一致? | 1)保证写库路径都尽量写入订单号;2)缺订单号时用「同步订单」补全;3)前端与统计只用「仅可对账」口径(reconciled_only=true);4)需要时用 verify-binance 接口校验。 |
|
||||
|
|
@ -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:可能是 -2022(ReduceOnly 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(
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
124
诊断日志问题.md
124
诊断日志问题.md
|
|
@ -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.1310,ROE 将达到 -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"
|
||||
```
|
||||
Loading…
Reference in New Issue
Block a user