feat(trades): 添加对账校验接口以验证交易记录准确性
新增 `GET /api/trades/verify-binance` 接口,允许用户校验与币安的交易记录一致性。该接口支持指定时间范围和校验条数,返回校验结果的汇总和详细信息,确保策略执行分析所依赖的数据与交易所一致。
This commit is contained in:
parent
225cb436d1
commit
b9392e096c
|
|
@ -17,7 +17,7 @@ project_root = Path(__file__).parent.parent.parent.parent
|
|||
sys.path.insert(0, str(project_root))
|
||||
sys.path.insert(0, str(project_root / 'backend'))
|
||||
|
||||
from database.models import Trade
|
||||
from database.models import Trade, Account
|
||||
from api.auth_deps import get_account_id
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -650,3 +650,168 @@ async def sync_trades_from_binance(
|
|||
except Exception as e:
|
||||
logger.error(f"同步币安订单失败: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"同步币安订单失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/verify-binance")
|
||||
async def verify_trades_against_binance(
|
||||
account_id: int = Depends(get_account_id),
|
||||
days: int = Query(30, ge=1, le=90, description="校验最近 N 天的记录,默认 30"),
|
||||
limit: int = Query(100, ge=1, le=500, description="最多校验条数,默认 100"),
|
||||
):
|
||||
"""
|
||||
用币安订单接口逐条校验本账号「可对账」交易记录的准确性,便于把握策略执行分析所依赖的订单数据是否与交易所一致。
|
||||
- 只校验有 entry_order_id 的记录(已平仓的还会校验 exit_order_id)。
|
||||
- 每条记录会请求币安 futures_get_order 核对订单是否存在、symbol/side/数量是否一致。
|
||||
- 返回汇总(一致/缺失/不一致数量)与明细,便于排查对不上的记录。
|
||||
"""
|
||||
beijing_tz = timezone(timedelta(hours=8))
|
||||
now = datetime.now(beijing_tz)
|
||||
end_ts = int(now.timestamp())
|
||||
start_ts = end_ts - days * 24 * 3600
|
||||
|
||||
trades = Trade.get_all(
|
||||
account_id=account_id,
|
||||
start_timestamp=start_ts,
|
||||
end_timestamp=end_ts,
|
||||
)
|
||||
# 只校验「可对账」记录:有 entry_order_id;若已平仓则还须有 exit_order_id
|
||||
def _has_entry(eid):
|
||||
return eid is not None and str(eid).strip() not in ("", "0")
|
||||
def _has_exit(xid):
|
||||
return xid is not None and str(xid).strip() not in ("", "0")
|
||||
to_verify = [
|
||||
t for t in trades
|
||||
if _has_entry(t.get("entry_order_id"))
|
||||
and (t.get("status") != "closed" or _has_exit(t.get("exit_order_id")))
|
||||
][:limit]
|
||||
|
||||
if not to_verify:
|
||||
return {
|
||||
"success": True,
|
||||
"account_id": account_id,
|
||||
"summary": {"total_verified": 0, "entry_ok": 0, "entry_missing": 0, "entry_mismatch": 0, "exit_ok": 0, "exit_missing": 0, "exit_mismatch": 0},
|
||||
"details": [],
|
||||
"message": "该时间范围内没有可对账记录(需有 entry_order_id,已平仓需有 exit_order_id)",
|
||||
}
|
||||
|
||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||
if not api_key or not api_secret:
|
||||
raise HTTPException(status_code=400, detail=f"账号 {account_id} 未配置 API 密钥,无法请求币安")
|
||||
|
||||
trading_system_path = project_root / "trading_system"
|
||||
if not trading_system_path.exists():
|
||||
trading_system_path = project_root / "backend" / "trading_system"
|
||||
sys.path.insert(0, str(project_root))
|
||||
sys.path.insert(0, str(trading_system_path))
|
||||
try:
|
||||
from binance_client import BinanceClient
|
||||
except ImportError:
|
||||
raise HTTPException(status_code=500, detail="无法导入 BinanceClient")
|
||||
|
||||
client = BinanceClient(api_key=api_key, api_secret=api_secret, testnet=use_testnet)
|
||||
await client.connect()
|
||||
try:
|
||||
summary = {"total_verified": 0, "entry_ok": 0, "entry_missing": 0, "entry_mismatch": 0, "exit_ok": 0, "exit_missing": 0, "exit_mismatch": 0}
|
||||
details = []
|
||||
|
||||
for t in to_verify:
|
||||
tid = t.get("id")
|
||||
symbol = t.get("symbol") or ""
|
||||
eid = t.get("entry_order_id")
|
||||
xid = t.get("exit_order_id")
|
||||
status_t = t.get("status") or "open"
|
||||
side_t = t.get("side") or ""
|
||||
qty_t = float(t.get("quantity") or 0)
|
||||
entry_price_t = float(t.get("entry_price") or 0)
|
||||
|
||||
row = {
|
||||
"trade_id": tid,
|
||||
"symbol": symbol,
|
||||
"side": side_t,
|
||||
"status": status_t,
|
||||
"entry_order_id": eid,
|
||||
"exit_order_id": xid,
|
||||
"entry_verified": None,
|
||||
"entry_message": None,
|
||||
"exit_verified": None,
|
||||
"exit_message": None,
|
||||
}
|
||||
|
||||
# 校验开仓订单
|
||||
if _has_entry(eid):
|
||||
summary["total_verified"] += 1
|
||||
try:
|
||||
order = await client.client.futures_get_order(symbol=symbol, orderId=int(eid))
|
||||
if not order:
|
||||
summary["entry_missing"] += 1
|
||||
row["entry_verified"] = False
|
||||
row["entry_message"] = "币安未返回订单"
|
||||
else:
|
||||
ob_side = (order.get("side") or "").upper()
|
||||
ob_qty = float(order.get("origQty") or order.get("executedQty") or 0)
|
||||
ob_price = float(order.get("avgPrice") or 0)
|
||||
if ob_side != side_t or abs(ob_qty - qty_t) > 1e-8:
|
||||
summary["entry_mismatch"] += 1
|
||||
row["entry_verified"] = False
|
||||
row["entry_message"] = f"币安 side={ob_side} qty={ob_qty},DB side={side_t} qty={qty_t}"
|
||||
else:
|
||||
summary["entry_ok"] += 1
|
||||
row["entry_verified"] = True
|
||||
row["entry_message"] = "一致"
|
||||
except Exception as ex:
|
||||
err = str(ex)
|
||||
if "Unknown order" in err or "-2011" in err or "404" in err.lower():
|
||||
summary["entry_missing"] += 1
|
||||
row["entry_verified"] = False
|
||||
row["entry_message"] = "币安无此订单"
|
||||
else:
|
||||
summary["entry_mismatch"] += 1
|
||||
row["entry_verified"] = False
|
||||
row["entry_message"] = err[:200]
|
||||
|
||||
# 校验平仓订单(仅已平仓且存在 exit_order_id)
|
||||
if status_t == "closed" and _has_exit(xid):
|
||||
try:
|
||||
order = await client.client.futures_get_order(symbol=symbol, orderId=int(xid))
|
||||
if not order:
|
||||
summary["exit_missing"] += 1
|
||||
row["exit_verified"] = False
|
||||
row["exit_message"] = "币安未返回订单"
|
||||
else:
|
||||
ob_side = (order.get("side") or "").upper()
|
||||
ob_qty = float(order.get("executedQty") or order.get("origQty") or 0)
|
||||
if ob_side != side_t or abs(ob_qty - qty_t) > 1e-8:
|
||||
summary["exit_mismatch"] += 1
|
||||
row["exit_verified"] = False
|
||||
row["exit_message"] = f"币安 side={ob_side} qty={ob_qty},DB side={side_t} qty={qty_t}"
|
||||
elif not order.get("reduceOnly"):
|
||||
summary["exit_mismatch"] += 1
|
||||
row["exit_verified"] = False
|
||||
row["exit_message"] = "币安订单非 reduceOnly,非平仓单"
|
||||
else:
|
||||
summary["exit_ok"] += 1
|
||||
row["exit_verified"] = True
|
||||
row["exit_message"] = "一致"
|
||||
except Exception as ex:
|
||||
err = str(ex)
|
||||
if "Unknown order" in err or "-2011" in err or "404" in err.lower():
|
||||
summary["exit_missing"] += 1
|
||||
row["exit_verified"] = False
|
||||
row["exit_message"] = "币安无此订单"
|
||||
else:
|
||||
summary["exit_mismatch"] += 1
|
||||
row["exit_verified"] = False
|
||||
row["exit_message"] = err[:200]
|
||||
|
||||
details.append(row)
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"account_id": account_id,
|
||||
"summary": summary,
|
||||
"details": details,
|
||||
"message": f"已校验 {summary['total_verified']} 条开仓订单,开仓一致 {summary['entry_ok']}、缺失 {summary['entry_missing']}、不一致 {summary['entry_mismatch']};平仓一致 {summary['exit_ok']}、缺失 {summary['exit_missing']}、不一致 {summary['exit_mismatch']}",
|
||||
}
|
||||
finally:
|
||||
await client.disconnect()
|
||||
|
|
|
|||
|
|
@ -92,3 +92,41 @@
|
|||
表示:仅对 **当前已在 `active_positions` 中且具备 SL/TP 价格的持仓** 在交易所侧挂或更新保护单;每次挂前会先取消同类型旧单再挂新单,不会重复堆积同一 symbol 的多组 SL/TP。
|
||||
|
||||
若希望减少「想下单」类日志的干扰,可将「跳过开仓」类日志级别调低(如改为 debug),或仅在有实际下单时再打 info。
|
||||
|
||||
---
|
||||
|
||||
## 六、如何确认订单记录的准确性
|
||||
|
||||
策略执行情况分析依赖订单记录,必须保证「能对上的」记录与币安一致。可按下面步骤把握。
|
||||
|
||||
### 1. 日常使用:只用「可对账」口径
|
||||
|
||||
- 前端「交易记录」**保持勾选「仅可对账(与币安一致)」**(默认即勾选)。
|
||||
- 看日盈亏、胜率、策略统计时,接口已默认 `reconciled_only=true`,只统计有 `entry_order_id` 且已平仓有 `exit_order_id` 的记录,与币安可一一对应。
|
||||
- 分析策略执行、复盘盈亏时,以这批记录为准,避免被无订单号或补建脏数据干扰。
|
||||
|
||||
### 2. 主动校验:调用「对账校验」接口
|
||||
|
||||
- **接口**:`GET /api/trades/verify-binance`(需登录并带 `X-Account-Id` 指定账号)。
|
||||
- **参数**:`days`(校验最近 N 天,默认 30)、`limit`(最多校验条数,默认 100)。
|
||||
- **作用**:对当前账号在时间范围内的「可对账」记录,逐条用币安 `futures_get_order` 校验:
|
||||
- 开仓订单:订单是否存在、symbol/side/数量是否与 DB 一致;
|
||||
- 平仓订单(已平仓且存在 `exit_order_id`):同上,并确认为 reduceOnly。
|
||||
- **返回**:`summary`(一致/缺失/不一致数量)+ `details`(每条记录的 `entry_verified`/`exit_verified` 及说明)。便于快速发现「对不上」的记录。
|
||||
|
||||
**示例**(校验最近 30 天、最多 100 条):
|
||||
|
||||
```http
|
||||
GET /api/trades/verify-binance?days=30&limit=100
|
||||
X-Account-Id: 1
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
若 `entry_missing` 或 `exit_missing` 不为 0,说明 DB 里有的订单号在币安查不到(可能错写、错账号或历史清理);若 `entry_mismatch`/`exit_mismatch` 不为 0,说明订单存在但 symbol/side/数量等与 DB 不一致,需排查写入或同步逻辑。
|
||||
|
||||
### 3. 发现对不上时怎么处理
|
||||
|
||||
- **仅做策略分析**:继续以「仅可对账」口径为准;有问题的记录因缺订单号或校验不通过,不会进入默认统计。
|
||||
- **希望 DB 与币安尽量一致**:可先执行 `POST /api/trades/sync-binance`(按「最近 N 天」从币安拉订单并更新/补全平仓与 `exit_order_id`);再跑一次 `GET /api/trades/verify-binance` 看是否仍存在缺失或不一致,再针对单条记录排查或人工修正。
|
||||
|
||||
总结:**日常用「仅可对账」统计 + 定期或按需调 `verify-binance` 校验**,即可把订单记录的准确性把握住,策略执行分析所依赖的数据与币安一致。
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user