Fix ghost open positions: dedupe tickets and reconcile against MT5.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-21 18:03:48 +08:00
parent e2847c90df
commit 391851a2b0
4 changed files with 71 additions and 33 deletions
+32 -12
View File
@@ -780,37 +780,58 @@ def register_routes(app: Flask) -> None:
database.now(),
),
)
tid = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
tid = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0])
# 同一账户+单号只保留一条「持仓中」,其余重复未平仓直接关掉
if ticket and close_time in (None, ""):
dup_snap = json.dumps(
{"event": "dedupe_closed", "kept_id": tid}, ensure_ascii=False
)
conn.execute(
"""
UPDATE trades
SET close_time=?, snapshot_json=?
WHERE account_id=? AND ticket=? AND id!=?
AND (close_time IS NULL OR close_time='')
""",
(database.now(), dup_snap, act["account_id"], ticket, tid),
)
return jsonify({"ok": True, "id": tid})
@app.post("/api/v1/trades/reconcile")
def api_trades_reconcile():
"""客户端上报当前仍持仓的 ticket;库中其它未平仓记录结束「持仓中」。"""
"""客户端上报当前仍持仓的 ticket;库中其它未平仓记录结束「持仓中」。
按账户对账(不按品种过滤),避免 XAUUSD/XAUUSDm 不一致导致幽灵持仓。
同一 ticket 多条未平仓时只保留 id 最大的一条。
"""
token = request.headers.get("X-Device-Token") or ""
data = request.get_json(silent=True) or {}
open_tickets = {str(t).strip() for t in (data.get("open_tickets") or []) if str(t).strip()}
symbol = (data.get("symbol") or "").strip().upper()
with database.get_db() as conn:
act = database.auth_token(conn, token)
if not act:
return jsonify({"ok": False, "error": "未激活"}), 401
if not act.get("account_id"):
return jsonify({"ok": False, "error": "激活码未绑定账户"}), 400
sql = """
rows = conn.execute(
"""
SELECT id, ticket, profit, snapshot_json
FROM trades
WHERE account_id=?
AND (close_time IS NULL OR close_time='')
"""
params: list = [act["account_id"]]
if symbol:
sql += " AND UPPER(symbol)=?"
params.append(symbol)
rows = conn.execute(sql, tuple(params)).fetchall()
ORDER BY id DESC
""",
(act["account_id"],),
).fetchall()
closed_n = 0
now_ts = database.now()
seen_open: set[str] = set()
for r in rows:
if str(r["ticket"] or "") in open_tickets:
tid = str(r["ticket"] or "")
keep = tid in open_tickets and tid not in seen_open
if keep:
seen_open.add(tid)
continue
try:
snap = json.loads(r["snapshot_json"] or "{}")
@@ -818,7 +839,6 @@ def register_routes(app: Flask) -> None:
snap = {}
snap["event"] = "reconcile_closed"
snap["reconciled_at"] = now_ts
# 若无盈亏则置 0,避免一直「—」;有历史补报时会再 upsert 覆盖
profit = r["profit"]
if profit is None:
profit = 0.0