feat(position_manager): 添加落库失败独立日志功能以便于排查

在 `position_manager` 中新增日志记录功能,当币安订单已成交但未成功写入数据库时,将相关信息记录到独立的 `trade_db_failures.log` 文件中。此功能有助于排查与对账,确保交易记录的准确性和完整性。
This commit is contained in:
薇薇安 2026-02-16 14:23:01 +08:00
parent b9392e096c
commit aa073099f2
2 changed files with 97 additions and 0 deletions

View File

@ -130,3 +130,43 @@ Authorization: Bearer <token>
- **希望 DB 与币安尽量一致**:可先执行 `POST /api/trades/sync-binance`(按「最近 N 天」从币安拉订单并更新/补全平仓与 `exit_order_id`);再跑一次 `GET /api/trades/verify-binance` 看是否仍存在缺失或不一致,再针对单条记录排查或人工修正。 - **希望 DB 与币安尽量一致**:可先执行 `POST /api/trades/sync-binance`(按「最近 N 天」从币安拉订单并更新/补全平仓与 `exit_order_id`);再跑一次 `GET /api/trades/verify-binance` 看是否仍存在缺失或不一致,再针对单条记录排查或人工修正。
总结:**日常用「仅可对账」统计 + 定期或按需调 `verify-binance` 校验**,即可把订单记录的准确性把握住,策略执行分析所依赖的数据与币安一致。 总结:**日常用「仅可对账」统计 + 定期或按需调 `verify-binance` 校验**,即可把订单记录的准确性把握住,策略执行分析所依赖的数据与币安一致。
---
## 七、持仓里「未知来源」订单是哪里来的?
持仓单里出现「未知」或「未知来源」,通常来自下面几类情况。
### 1. 仪表板「入场类型:未知」
- **数据来源**`GET /api/account/positions` 用**币安实时持仓**列表,再按 symbol 匹配本账号 DB 里 `status=open` 的记录,取 `entry_order_id`;若有订单号再调币安 `futures_get_order` 取订单类型 → 得到「限价/市价/未知」。
- **出现「未知」的两种情况**
- **币安有仓、DB 没有对应 open 记录**(尚未补建或未匹配上):没有 `entry_order_id`,自然没有订单类型 → 显示「未知」。
- **DB 有记录但 `entry_order_id` 为空**(例如补建时没查到开仓订单、或历史单无订单号):同样无法查订单类型 → 显示「未知」。
即:**凡是「币安有仓但系统没有可关联的开仓订单号」的持仓,在仪表板上都会显示为入场类型「未知」。**
### 2. 交易记录里「入场原因」= sync_recovered_unknown_origin来历不明
- **产生位置****trading_system 的持仓状态同步**`position_manager.sync_positions_with_binance`在「币安有仓、DB 无 open 记录」时会对该仓**补建**一条交易记录。
- **何时标成「未知来源」**`entry_reason = sync_recovered_unknown_origin`
- 配置了 **SYSTEM_ORDER_ID_PREFIX** 且能查到开仓订单时:若该订单的 **clientOrderId 不以系统前缀开头**(例如手动在币安下单、或其它系统下的单),则视为「来历不明」,仍会补建、挂止损止盈并纳入监控,但 `entry_reason` 记为 `sync_recovered_unknown_origin`
- **未配置** SYSTEM_ORDER_ID_PREFIX 且开启了 **SYNC_RECOVER_ONLY_WHEN_HAS_SLTP** 时:若该仓**没有**止损/止盈单,也记为「来历不明」并补建。
- **后端 POST /api/account/positions/sync**:逻辑不同——会**跳过**「明确非系统前缀」的仓(不补建),所以不会在「同步接口」里产生 unknown_origin只有**交易进程内**的定时同步会按上面规则补建并标记 `sync_recovered_unknown_origin`
因此:**持仓里「未知来源」的订单 = 币安上存在、但开仓不是本系统下的单(或无法用系统前缀/止损止盈判断为本系统单),由交易系统状态同步补建并标记为「来历不明」的那部分。** 系统仍会为它们挂止损止盈并监控,只是统计/分析时可通过「仅可对账」过滤掉无 `entry_order_id` 的这类记录。
### 3. 本系统开的仓为什么会出现「没正确落库」或「没记订单号」?
正常流程是:**开仓订单在币安成交 → 等待 FILLED → 用实际成交价/数量调用 `Trade.create(..., entry_order_id=..., client_order_id=...)`**。下面情况会导致「开了不落库」或补建时「没订单号」。
| 原因 | 说明 |
|------|------|
| **数据库不可用** | `position_manager` 启动时尝试导入 `backend.database.models`,若 `backend` 目录不存在、或导入失败缺依赖、Python 路径错误、DB 连不上等),会设 `DB_AVAILABLE = False`,成交后直接跳过保存并打日志「数据库不可用,跳过保存」。 |
| **Trade.create 抛异常** | 落库时发生异常(如连接超时、唯一约束冲突、磁盘满、字段错误等),代码会打错并 `return None`,不会重试,币安已有仓但 DB 无记录。 |
| **进程在落库前退出** | 订单已在币安成交,但在执行到 `Trade.create` 之前进程被 kill、崩溃或重启也会出现币安有仓、DB 无记录。 |
| **补建时拿不到订单号** | 同步补建时用 `get_recent_trades(symbol, limit=30)` 取最近同向成交并取第一条的 `orderId` 作为 `entry_order_id`。若接口失败、返回空、或该笔开仓已不在最近 30 条成交里,则 `entry_order_id` 为空,补建记录就会「没记订单号」。 |
**建议**:保证交易进程与 backend 同机或能访问同一 DB、且 `backend` 路径正确,避免 `DB_AVAILABLE = False`;若曾出现「开了不落库」,可查交易进程日志中的「保存交易记录到数据库失败」或「数据库不可用」;补建后仍无订单号的记录可用「仅可对账」过滤,不影响基于可对账记录的统计。
**落库失败独立日志**:当币安订单已成交但未写入 DB 时(如 `Trade.create` 抛异常、或 `DB_AVAILABLE=False`、或 `Trade` 未导入),会额外写入项目目录下 **`logs/trade_db_failures.log`**,每行一条 JSON`ts`、`symbol`、`entry_order_id`、`side`、`quantity`、`entry_price`、`account_id`、`reason`、以及失败时的 `error_type`/`error_message`)。不依赖 DB/Redis便于单独 grep 或脚本统计「成交未落库」笔数,便于与币安对账。

View File

@ -6,6 +6,7 @@ import logging
import json import json
import aiohttp import aiohttp
import time import time
from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
from datetime import datetime from datetime import datetime
try: try:
@ -53,6 +54,47 @@ if get_beijing_time is None:
"""获取当前北京时间UTC+8""" """获取当前北京时间UTC+8"""
return datetime.now(BEIJING_TZ).replace(tzinfo=None) return datetime.now(BEIJING_TZ).replace(tzinfo=None)
# 落库失败独立日志:币安已成交但未写入 DB 时写入此文件,便于排查与对账(不依赖 DB/Redis
_FAILURE_LOG_DIR = None
_FAILURE_LOG_PATH = None
def _get_db_failure_log_path():
global _FAILURE_LOG_DIR, _FAILURE_LOG_PATH
if _FAILURE_LOG_PATH is not None:
return _FAILURE_LOG_PATH
try:
root = Path(__file__).parent.parent
_FAILURE_LOG_DIR = root / "logs"
_FAILURE_LOG_DIR.mkdir(parents=True, exist_ok=True)
_FAILURE_LOG_PATH = _FAILURE_LOG_DIR / "trade_db_failures.log"
return _FAILURE_LOG_PATH
except Exception:
return None
def _log_trade_db_failure(symbol, entry_order_id, side, quantity, entry_price, account_id, reason, error_type=None, error_message=None):
"""将「成交但落库失败」记录到独立日志文件(一行 JSON便于 grep/脚本统计,不依赖 DB/Redis。"""
path = _get_db_failure_log_path()
if not path:
return
try:
rec = {
"ts": datetime.utcnow().isoformat() + "Z",
"symbol": symbol,
"entry_order_id": entry_order_id,
"side": side,
"quantity": quantity,
"entry_price": entry_price,
"account_id": account_id,
"reason": reason,
}
if error_type:
rec["error_type"] = error_type
if error_message:
rec["error_message"] = (str(error_message) or "")[:500]
with open(path, "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
except Exception as e:
logger.warning(f"写入落库失败日志失败: {e}")
class PositionManager: class PositionManager:
"""仓位管理类""" """仓位管理类"""
@ -717,11 +759,26 @@ class PositionManager:
logger.error(f" 错误类型: {type(e).__name__}") logger.error(f" 错误类型: {type(e).__name__}")
import traceback import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}") logger.error(f" 错误详情:\n{traceback.format_exc()}")
_log_trade_db_failure(
symbol=symbol, entry_order_id=entry_order_id, side=side,
quantity=quantity, entry_price=entry_price, account_id=self.account_id,
reason="create_exception", error_type=type(e).__name__, error_message=str(e)
)
return None return None
elif not DB_AVAILABLE: elif not DB_AVAILABLE:
logger.debug(f"数据库不可用,跳过保存 {symbol} 交易记录") logger.debug(f"数据库不可用,跳过保存 {symbol} 交易记录")
_log_trade_db_failure(
symbol=symbol, entry_order_id=entry_order_id, side=side,
quantity=quantity, entry_price=entry_price, account_id=self.account_id,
reason="DB_AVAILABLE=False"
)
elif not Trade: elif not Trade:
logger.warning(f"Trade模型未导入无法保存 {symbol} 交易记录") logger.warning(f"Trade模型未导入无法保存 {symbol} 交易记录")
_log_trade_db_failure(
symbol=symbol, entry_order_id=entry_order_id, side=side,
quantity=quantity, entry_price=entry_price, account_id=self.account_id,
reason="Trade=None"
)
# 记录持仓信息(包含动态止损止盈和分步止盈) # 记录持仓信息(包含动态止损止盈和分步止盈)
from datetime import datetime from datetime import datetime