From 13a0e7d580fe2950a7717acedd6ef531b4a4e3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Fri, 20 Feb 2026 17:49:00 +0800 Subject: [PATCH] =?UTF-8?q?delete:=20=E7=A7=BB=E9=99=A4=E8=BF=87=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E6=96=87=E6=A1=A3=E4=B8=8E=E4=BB=A3=E7=A0=81=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除了多个不再使用的文档和代码文件,包括交易更新推送、条件订单推送、REST API 文档、WebSocket API 文档及相关的策略分析文档。这些文件的移除有助于清理代码库,确保项目的整洁性与可维护性。 --- backend/api/routes/trades.py | 23 +- backend/database/add_order_type_fields.sql | 11 + backend/database/models.py | 58 +++-- .../bian/listenkey过期推送.txt | 0 .../bian/ws交易接口.txt | 0 .../bian/ws行情推送.txt | 0 .../bian/条件订单交易更新推送.txt | 0 .../bian/用户余额.txt | 0 .../bian/行情ws接口.txt | 0 .../bian/行情接口REST.txt | 0 .../bian/订单交易更新推送.txt | 0 ...记录简化流程(支付式闭环).md | 132 +++++++++++ .../bian/账户信息流连接.txt | 0 docs/{ => common}/API_KEY_SETUP.md | 0 docs/{ => common}/ATR.md | 0 .../ATR_STRATEGY_IMPLEMENTATION.md | 0 docs/{ => common}/CONFIG_GUIDE.md | 0 docs/{ => common}/CURRENT_STRATEGY.md | 0 docs/{ => common}/DEPLOYMENT.md | 0 .../ENTRY_CONTEXT_入场思路记录.md | 0 docs/{ => common}/INDEX.md | 0 docs/{ => common}/INSTALL.md | 0 docs/{ => common}/MULTI_USER_ARCHITECTURE.md | 0 docs/{ => common}/PROJECT_SUMMARY.md | 0 docs/{ => common}/QUICK_START.md | 0 docs/{ => common}/README.md | 0 docs/{ => common}/README_ARCHITECTURE.md | 0 .../STOP_LOSS_TAKE_PROFIT_EXPLANATION.md | 0 docs/{ => common}/STRUCTURE.md | 0 .../SUPERVISOR_TROUBLESHOOTING.md | 0 .../WS交易接口实现说明.md | 0 .../WebSocket共用与限频评估.md | 0 docs/{ => common}/apply_altcoin_strategy.sh | 0 docs/{ => common}/check_accounts_no_trades.sh | 0 docs/{ => common}/fix_cursor_path.sh | 0 docs/{ => common}/risk_profile_2026-02-15.md | 0 ..._2026-02-14_策略执行与优化建议.md | 0 ...分析_2026-02-14_盈利期vs亏损期.md | 0 .../交易表现分析_20260214.md | 0 .../全局配置与数据库同步.md | 0 .../内存优化_修复K线缓存.md | 0 .../内存问题排查与优化.md | 0 .../单账号内存评估与Redis减压.md | 0 docs/{ => common}/后续方向实施说明.md | 0 .../多账号与低配服务器优化.md | 0 ...山寨币策略快速应用完整指南.md | 0 .../当前策略方案总结_2026-02-15.md | 0 .../快速使用_盈利期对齐配置.md | 0 ...持仓与成交量低迷期优化分析.md | 0 ...持仓分析_2026-02-15_与参数建议.md | 0 .../止损止盈双通道说明.md | 0 docs/{ => common}/止损止盈计算说明.md | 0 .../止盈止损与盈利优化_2026-02-15.md | 0 docs/{ => common}/盈利提升方案.md | 0 .../策略与信号评估及改进建议.md | 0 .../{ => common}/缓存优化_写入Valkey.md | 0 .../行情接口可用性说明.md | 0 .../订单与统计一致性说明.md | 0 docs/{ => common}/订单入库演变说明.md | 0 .../订单记录与币安对账流程.md | 158 +++++++++++++ .../负载问题排查与降负载.md | 0 trading_system/position_manager.py | 211 ++---------------- trading_system/user_data_stream.py | 67 ++++-- 诊断日志问题.md | 124 ---------- 64 files changed, 414 insertions(+), 370 deletions(-) create mode 100644 backend/database/add_order_type_fields.sql rename listenkey过期推送.txt => docs/bian/listenkey过期推送.txt (100%) rename ws交易接口.txt => docs/bian/ws交易接口.txt (100%) rename ws行情推送.txt => docs/bian/ws行情推送.txt (100%) rename 条件订单交易更新推送.txt => docs/bian/条件订单交易更新推送.txt (100%) rename 用户余额.txt => docs/bian/用户余额.txt (100%) rename 行情ws接口.txt => docs/bian/行情ws接口.txt (100%) rename 行情接口REST.txt => docs/bian/行情接口REST.txt (100%) rename 订单交易更新推送.txt => docs/bian/订单交易更新推送.txt (100%) create mode 100644 docs/bian/订单记录简化流程(支付式闭环).md rename 账户信息流连接.txt => docs/bian/账户信息流连接.txt (100%) rename docs/{ => common}/API_KEY_SETUP.md (100%) rename docs/{ => common}/ATR.md (100%) rename docs/{ => common}/ATR_STRATEGY_IMPLEMENTATION.md (100%) rename docs/{ => common}/CONFIG_GUIDE.md (100%) rename docs/{ => common}/CURRENT_STRATEGY.md (100%) rename docs/{ => common}/DEPLOYMENT.md (100%) rename docs/{ => common}/ENTRY_CONTEXT_入场思路记录.md (100%) rename docs/{ => common}/INDEX.md (100%) rename docs/{ => common}/INSTALL.md (100%) rename docs/{ => common}/MULTI_USER_ARCHITECTURE.md (100%) rename docs/{ => common}/PROJECT_SUMMARY.md (100%) rename docs/{ => common}/QUICK_START.md (100%) rename docs/{ => common}/README.md (100%) rename docs/{ => common}/README_ARCHITECTURE.md (100%) rename docs/{ => common}/STOP_LOSS_TAKE_PROFIT_EXPLANATION.md (100%) rename docs/{ => common}/STRUCTURE.md (100%) rename docs/{ => common}/SUPERVISOR_TROUBLESHOOTING.md (100%) rename docs/{ => common}/WS交易接口实现说明.md (100%) rename docs/{ => common}/WebSocket共用与限频评估.md (100%) rename docs/{ => common}/apply_altcoin_strategy.sh (100%) rename docs/{ => common}/check_accounts_no_trades.sh (100%) rename docs/{ => common}/fix_cursor_path.sh (100%) rename docs/{ => common}/risk_profile_2026-02-15.md (100%) rename docs/{ => common}/交易分析_2026-02-14_策略执行与优化建议.md (100%) rename docs/{ => common}/交易对比分析_2026-02-14_盈利期vs亏损期.md (100%) rename docs/{ => common}/交易表现分析_20260214.md (100%) rename docs/{ => common}/全局配置与数据库同步.md (100%) rename docs/{ => common}/内存优化_修复K线缓存.md (100%) rename docs/{ => common}/内存问题排查与优化.md (100%) rename docs/{ => common}/单账号内存评估与Redis减压.md (100%) rename docs/{ => common}/后续方向实施说明.md (100%) rename docs/{ => common}/多账号与低配服务器优化.md (100%) rename docs/{ => common}/山寨币策略快速应用完整指南.md (100%) rename docs/{ => common}/当前策略方案总结_2026-02-15.md (100%) rename docs/{ => common}/快速使用_盈利期对齐配置.md (100%) rename docs/{ => common}/持仓与成交量低迷期优化分析.md (100%) rename docs/{ => common}/持仓分析_2026-02-15_与参数建议.md (100%) rename docs/{ => common}/止损止盈双通道说明.md (100%) rename docs/{ => common}/止损止盈计算说明.md (100%) rename docs/{ => common}/止盈止损与盈利优化_2026-02-15.md (100%) rename docs/{ => common}/盈利提升方案.md (100%) rename docs/{ => common}/策略与信号评估及改进建议.md (100%) rename docs/{ => common}/缓存优化_写入Valkey.md (100%) rename docs/{ => common}/行情接口可用性说明.md (100%) rename docs/{ => common}/订单与统计一致性说明.md (100%) rename docs/{ => common}/订单入库演变说明.md (100%) create mode 100644 docs/common/订单记录与币安对账流程.md rename docs/{ => common}/负载问题排查与降负载.md (100%) delete mode 100644 诊断日志问题.md diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index 5caee2c..e154d13 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -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: diff --git a/backend/database/add_order_type_fields.sql b/backend/database/add_order_type_fields.sql new file mode 100644 index 0000000..82024e2 --- /dev/null +++ b/backend/database/add_order_type_fields.sql @@ -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; diff --git a/backend/database/models.py b/backend/database/models.py index 2455b22..7adf5d0 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -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: diff --git a/listenkey过期推送.txt b/docs/bian/listenkey过期推送.txt similarity index 100% rename from listenkey过期推送.txt rename to docs/bian/listenkey过期推送.txt diff --git a/ws交易接口.txt b/docs/bian/ws交易接口.txt similarity index 100% rename from ws交易接口.txt rename to docs/bian/ws交易接口.txt diff --git a/ws行情推送.txt b/docs/bian/ws行情推送.txt similarity index 100% rename from ws行情推送.txt rename to docs/bian/ws行情推送.txt diff --git a/条件订单交易更新推送.txt b/docs/bian/条件订单交易更新推送.txt similarity index 100% rename from 条件订单交易更新推送.txt rename to docs/bian/条件订单交易更新推送.txt diff --git a/用户余额.txt b/docs/bian/用户余额.txt similarity index 100% rename from 用户余额.txt rename to docs/bian/用户余额.txt diff --git a/行情ws接口.txt b/docs/bian/行情ws接口.txt similarity index 100% rename from 行情ws接口.txt rename to docs/bian/行情ws接口.txt diff --git a/行情接口REST.txt b/docs/bian/行情接口REST.txt similarity index 100% rename from 行情接口REST.txt rename to docs/bian/行情接口REST.txt diff --git a/订单交易更新推送.txt b/docs/bian/订单交易更新推送.txt similarity index 100% rename from 订单交易更新推送.txt rename to docs/bian/订单交易更新推送.txt diff --git a/docs/bian/订单记录简化流程(支付式闭环).md b/docs/bian/订单记录简化流程(支付式闭环).md new file mode 100644 index 0000000..3cef838 --- /dev/null +++ b/docs/bian/订单记录简化流程(支付式闭环).md @@ -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__ +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`。 diff --git a/账户信息流连接.txt b/docs/bian/账户信息流连接.txt similarity index 100% rename from 账户信息流连接.txt rename to docs/bian/账户信息流连接.txt diff --git a/docs/API_KEY_SETUP.md b/docs/common/API_KEY_SETUP.md similarity index 100% rename from docs/API_KEY_SETUP.md rename to docs/common/API_KEY_SETUP.md diff --git a/docs/ATR.md b/docs/common/ATR.md similarity index 100% rename from docs/ATR.md rename to docs/common/ATR.md diff --git a/docs/ATR_STRATEGY_IMPLEMENTATION.md b/docs/common/ATR_STRATEGY_IMPLEMENTATION.md similarity index 100% rename from docs/ATR_STRATEGY_IMPLEMENTATION.md rename to docs/common/ATR_STRATEGY_IMPLEMENTATION.md diff --git a/docs/CONFIG_GUIDE.md b/docs/common/CONFIG_GUIDE.md similarity index 100% rename from docs/CONFIG_GUIDE.md rename to docs/common/CONFIG_GUIDE.md diff --git a/docs/CURRENT_STRATEGY.md b/docs/common/CURRENT_STRATEGY.md similarity index 100% rename from docs/CURRENT_STRATEGY.md rename to docs/common/CURRENT_STRATEGY.md diff --git a/docs/DEPLOYMENT.md b/docs/common/DEPLOYMENT.md similarity index 100% rename from docs/DEPLOYMENT.md rename to docs/common/DEPLOYMENT.md diff --git a/docs/ENTRY_CONTEXT_入场思路记录.md b/docs/common/ENTRY_CONTEXT_入场思路记录.md similarity index 100% rename from docs/ENTRY_CONTEXT_入场思路记录.md rename to docs/common/ENTRY_CONTEXT_入场思路记录.md diff --git a/docs/INDEX.md b/docs/common/INDEX.md similarity index 100% rename from docs/INDEX.md rename to docs/common/INDEX.md diff --git a/docs/INSTALL.md b/docs/common/INSTALL.md similarity index 100% rename from docs/INSTALL.md rename to docs/common/INSTALL.md diff --git a/docs/MULTI_USER_ARCHITECTURE.md b/docs/common/MULTI_USER_ARCHITECTURE.md similarity index 100% rename from docs/MULTI_USER_ARCHITECTURE.md rename to docs/common/MULTI_USER_ARCHITECTURE.md diff --git a/docs/PROJECT_SUMMARY.md b/docs/common/PROJECT_SUMMARY.md similarity index 100% rename from docs/PROJECT_SUMMARY.md rename to docs/common/PROJECT_SUMMARY.md diff --git a/docs/QUICK_START.md b/docs/common/QUICK_START.md similarity index 100% rename from docs/QUICK_START.md rename to docs/common/QUICK_START.md diff --git a/docs/README.md b/docs/common/README.md similarity index 100% rename from docs/README.md rename to docs/common/README.md diff --git a/docs/README_ARCHITECTURE.md b/docs/common/README_ARCHITECTURE.md similarity index 100% rename from docs/README_ARCHITECTURE.md rename to docs/common/README_ARCHITECTURE.md diff --git a/docs/STOP_LOSS_TAKE_PROFIT_EXPLANATION.md b/docs/common/STOP_LOSS_TAKE_PROFIT_EXPLANATION.md similarity index 100% rename from docs/STOP_LOSS_TAKE_PROFIT_EXPLANATION.md rename to docs/common/STOP_LOSS_TAKE_PROFIT_EXPLANATION.md diff --git a/docs/STRUCTURE.md b/docs/common/STRUCTURE.md similarity index 100% rename from docs/STRUCTURE.md rename to docs/common/STRUCTURE.md diff --git a/docs/SUPERVISOR_TROUBLESHOOTING.md b/docs/common/SUPERVISOR_TROUBLESHOOTING.md similarity index 100% rename from docs/SUPERVISOR_TROUBLESHOOTING.md rename to docs/common/SUPERVISOR_TROUBLESHOOTING.md diff --git a/docs/WS交易接口实现说明.md b/docs/common/WS交易接口实现说明.md similarity index 100% rename from docs/WS交易接口实现说明.md rename to docs/common/WS交易接口实现说明.md diff --git a/docs/WebSocket共用与限频评估.md b/docs/common/WebSocket共用与限频评估.md similarity index 100% rename from docs/WebSocket共用与限频评估.md rename to docs/common/WebSocket共用与限频评估.md diff --git a/docs/apply_altcoin_strategy.sh b/docs/common/apply_altcoin_strategy.sh similarity index 100% rename from docs/apply_altcoin_strategy.sh rename to docs/common/apply_altcoin_strategy.sh diff --git a/docs/check_accounts_no_trades.sh b/docs/common/check_accounts_no_trades.sh similarity index 100% rename from docs/check_accounts_no_trades.sh rename to docs/common/check_accounts_no_trades.sh diff --git a/docs/fix_cursor_path.sh b/docs/common/fix_cursor_path.sh similarity index 100% rename from docs/fix_cursor_path.sh rename to docs/common/fix_cursor_path.sh diff --git a/docs/risk_profile_2026-02-15.md b/docs/common/risk_profile_2026-02-15.md similarity index 100% rename from docs/risk_profile_2026-02-15.md rename to docs/common/risk_profile_2026-02-15.md diff --git a/docs/交易分析_2026-02-14_策略执行与优化建议.md b/docs/common/交易分析_2026-02-14_策略执行与优化建议.md similarity index 100% rename from docs/交易分析_2026-02-14_策略执行与优化建议.md rename to docs/common/交易分析_2026-02-14_策略执行与优化建议.md diff --git a/docs/交易对比分析_2026-02-14_盈利期vs亏损期.md b/docs/common/交易对比分析_2026-02-14_盈利期vs亏损期.md similarity index 100% rename from docs/交易对比分析_2026-02-14_盈利期vs亏损期.md rename to docs/common/交易对比分析_2026-02-14_盈利期vs亏损期.md diff --git a/docs/交易表现分析_20260214.md b/docs/common/交易表现分析_20260214.md similarity index 100% rename from docs/交易表现分析_20260214.md rename to docs/common/交易表现分析_20260214.md diff --git a/docs/全局配置与数据库同步.md b/docs/common/全局配置与数据库同步.md similarity index 100% rename from docs/全局配置与数据库同步.md rename to docs/common/全局配置与数据库同步.md diff --git a/docs/内存优化_修复K线缓存.md b/docs/common/内存优化_修复K线缓存.md similarity index 100% rename from docs/内存优化_修复K线缓存.md rename to docs/common/内存优化_修复K线缓存.md diff --git a/docs/内存问题排查与优化.md b/docs/common/内存问题排查与优化.md similarity index 100% rename from docs/内存问题排查与优化.md rename to docs/common/内存问题排查与优化.md diff --git a/docs/单账号内存评估与Redis减压.md b/docs/common/单账号内存评估与Redis减压.md similarity index 100% rename from docs/单账号内存评估与Redis减压.md rename to docs/common/单账号内存评估与Redis减压.md diff --git a/docs/后续方向实施说明.md b/docs/common/后续方向实施说明.md similarity index 100% rename from docs/后续方向实施说明.md rename to docs/common/后续方向实施说明.md diff --git a/docs/多账号与低配服务器优化.md b/docs/common/多账号与低配服务器优化.md similarity index 100% rename from docs/多账号与低配服务器优化.md rename to docs/common/多账号与低配服务器优化.md diff --git a/docs/山寨币策略快速应用完整指南.md b/docs/common/山寨币策略快速应用完整指南.md similarity index 100% rename from docs/山寨币策略快速应用完整指南.md rename to docs/common/山寨币策略快速应用完整指南.md diff --git a/docs/当前策略方案总结_2026-02-15.md b/docs/common/当前策略方案总结_2026-02-15.md similarity index 100% rename from docs/当前策略方案总结_2026-02-15.md rename to docs/common/当前策略方案总结_2026-02-15.md diff --git a/docs/快速使用_盈利期对齐配置.md b/docs/common/快速使用_盈利期对齐配置.md similarity index 100% rename from docs/快速使用_盈利期对齐配置.md rename to docs/common/快速使用_盈利期对齐配置.md diff --git a/docs/持仓与成交量低迷期优化分析.md b/docs/common/持仓与成交量低迷期优化分析.md similarity index 100% rename from docs/持仓与成交量低迷期优化分析.md rename to docs/common/持仓与成交量低迷期优化分析.md diff --git a/docs/持仓分析_2026-02-15_与参数建议.md b/docs/common/持仓分析_2026-02-15_与参数建议.md similarity index 100% rename from docs/持仓分析_2026-02-15_与参数建议.md rename to docs/common/持仓分析_2026-02-15_与参数建议.md diff --git a/docs/止损止盈双通道说明.md b/docs/common/止损止盈双通道说明.md similarity index 100% rename from docs/止损止盈双通道说明.md rename to docs/common/止损止盈双通道说明.md diff --git a/docs/止损止盈计算说明.md b/docs/common/止损止盈计算说明.md similarity index 100% rename from docs/止损止盈计算说明.md rename to docs/common/止损止盈计算说明.md diff --git a/docs/止盈止损与盈利优化_2026-02-15.md b/docs/common/止盈止损与盈利优化_2026-02-15.md similarity index 100% rename from docs/止盈止损与盈利优化_2026-02-15.md rename to docs/common/止盈止损与盈利优化_2026-02-15.md diff --git a/docs/盈利提升方案.md b/docs/common/盈利提升方案.md similarity index 100% rename from docs/盈利提升方案.md rename to docs/common/盈利提升方案.md diff --git a/docs/策略与信号评估及改进建议.md b/docs/common/策略与信号评估及改进建议.md similarity index 100% rename from docs/策略与信号评估及改进建议.md rename to docs/common/策略与信号评估及改进建议.md diff --git a/docs/缓存优化_写入Valkey.md b/docs/common/缓存优化_写入Valkey.md similarity index 100% rename from docs/缓存优化_写入Valkey.md rename to docs/common/缓存优化_写入Valkey.md diff --git a/docs/行情接口可用性说明.md b/docs/common/行情接口可用性说明.md similarity index 100% rename from docs/行情接口可用性说明.md rename to docs/common/行情接口可用性说明.md diff --git a/docs/订单与统计一致性说明.md b/docs/common/订单与统计一致性说明.md similarity index 100% rename from docs/订单与统计一致性说明.md rename to docs/common/订单与统计一致性说明.md diff --git a/docs/订单入库演变说明.md b/docs/common/订单入库演变说明.md similarity index 100% rename from docs/订单入库演变说明.md rename to docs/common/订单入库演变说明.md diff --git a/docs/common/订单记录与币安对账流程.md b/docs/common/订单记录与币安对账流程.md new file mode 100644 index 0000000..f052201 --- /dev/null +++ b/docs/common/订单记录与币安对账流程.md @@ -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 接口校验。 | diff --git a/docs/负载问题排查与降负载.md b/docs/common/负载问题排查与降负载.md similarity index 100% rename from docs/负载问题排查与降负载.md rename to docs/common/负载问题排查与降负载.md diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index b390318..99a9496 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -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( diff --git a/trading_system/user_data_stream.py b/trading_system/user_data_stream.py index 695a9ff..c4d01b1 100644 --- a/trading_system/user_data_stream.py +++ b/trading_system/user_data_stream.py @@ -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}") diff --git a/诊断日志问题.md b/诊断日志问题.md deleted file mode 100644 index 8a4324f..0000000 --- a/诊断日志问题.md +++ /dev/null @@ -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" -```