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` 看是否仍存在缺失或不一致,再针对单条记录排查或人工修正。
总结:**日常用「仅可对账」统计 + 定期或按需调 `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 aiohttp
import time
from pathlib import Path
from typing import Dict, List, Optional
from datetime import datetime
try:
@ -53,6 +54,47 @@ if get_beijing_time is None:
"""获取当前北京时间UTC+8"""
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:
"""仓位管理类"""
@ -717,11 +759,26 @@ class PositionManager:
logger.error(f" 错误类型: {type(e).__name__}")
import traceback
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
elif not DB_AVAILABLE:
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:
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