在持仓同步功能中,增加了对系统订单前缀的默认处理,确保在无配置时使用默认前缀 "ats_"。同时,调整了日志记录逻辑,明确区分系统单与手动单的补建条件,提升了日志的可读性与准确性。这一改动旨在增强系统的可用性与用户友好性,确保持仓与数据库记录的一致性。
11 KiB
订单记录与币安对账流程
本文梳理:订单是怎么记的、何时写入 entry_order_id / exit_order_id、如何与币安保持一致。
〇、常见误解:为什么「持仓 5 单、订单记录只有 1 单」?
- 持仓列表:数据来自币安 API(
get_open_positions()),是交易所上真实存在的持仓,与 DB 无关。 - 订单记录:数据来自本系统数据库
trades表,只有「本系统开仓并成功写库」或「同步/补建」时才会有一条记录。
因此不是「持仓存在就一定先在 DB 有了」——恰恰相反:币安上可以有 5 个持仓(本系统开的、手动在 App 开的、其它工具开的,或本系统开了但当时写库失败),而 DB 里只有我们成功写入或补建的那几条,所以订单记录会少于或等于持仓数。要对齐做法:使用持仓同步(见下文「补建」)或同步币安订单,把「币安有仓、DB 无记录」的持仓补建到 DB,订单记录就会和持仓一致。
一、数据模型(与币安对应的关键字段)
| 字段 | 含义 | 与币安对应 |
|---|---|---|
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 接口校验。 |