feat(position_manager): 添加落库失败独立日志功能以便于排查
在 `position_manager` 中新增日志记录功能,当币安订单已成交但未成功写入数据库时,将相关信息记录到独立的 `trade_db_failures.log` 文件中。此功能有助于排查与对账,确保交易记录的准确性和完整性。
This commit is contained in:
parent
b9392e096c
commit
aa073099f2
|
|
@ -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 或脚本统计「成交未落库」笔数,便于与币安对账。
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user