Dedupe duplicate ticket closes so stats match MT5 today PnL.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -174,6 +174,45 @@ def scrub_synthetic_trade_profits(conn: sqlite3.Connection) -> int:
|
|||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def scrub_duplicate_ticket_profits(conn: sqlite3.Connection) -> int:
|
||||||
|
"""同一账户+单号多条真实平仓时,只保留 id 最大的一条盈亏,其余标为去重。"""
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, account_id, ticket, profit, snapshot_json, close_time
|
||||||
|
FROM trades
|
||||||
|
WHERE ticket IS NOT NULL AND ticket != ''
|
||||||
|
AND close_time IS NOT NULL AND close_time != ''
|
||||||
|
ORDER BY account_id, ticket, id DESC
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
keep: set[tuple[Any, str]] = set()
|
||||||
|
n = 0
|
||||||
|
now_ts = now()
|
||||||
|
for r in rows:
|
||||||
|
if is_synthetic_close(r["snapshot_json"]):
|
||||||
|
continue
|
||||||
|
key = (r["account_id"], str(r["ticket"]))
|
||||||
|
if key not in keep:
|
||||||
|
keep.add(key)
|
||||||
|
continue
|
||||||
|
# 重复平仓副本:清空盈亏并标记
|
||||||
|
try:
|
||||||
|
snap = json.loads(r["snapshot_json"] or "{}")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
snap = {}
|
||||||
|
if not isinstance(snap, dict):
|
||||||
|
snap = {}
|
||||||
|
snap["event"] = "dedupe_closed"
|
||||||
|
snap["deduped_at"] = now_ts
|
||||||
|
snap["reason"] = "duplicate_ticket_close"
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE trades SET profit=NULL, snapshot_json=? WHERE id=?",
|
||||||
|
(json.dumps(snap, ensure_ascii=False), r["id"]),
|
||||||
|
)
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
def row_to_dict(row: Optional[sqlite3.Row]) -> Optional[dict]:
|
def row_to_dict(row: Optional[sqlite3.Row]) -> Optional[dict]:
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
@@ -537,6 +576,16 @@ def trade_stats(
|
|||||||
|
|
||||||
rows: list[dict] = []
|
rows: list[dict] = []
|
||||||
skipped_synthetic = 0
|
skipped_synthetic = 0
|
||||||
|
skipped_dup_ticket = 0
|
||||||
|
seen_closed_ticket: set[str] = set()
|
||||||
|
# raw 已按时间 DESC;再按 id 大的优先,便于同 ticket 去重留最新
|
||||||
|
raw_rows.sort(
|
||||||
|
key=lambda t: (
|
||||||
|
float(t.get("close_time") or t.get("open_time") or t.get("created_at") or 0),
|
||||||
|
int(t.get("id") or 0),
|
||||||
|
),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
for t in raw_rows:
|
for t in raw_rows:
|
||||||
closed_flag = t.get("close_time") not in (None, "")
|
closed_flag = t.get("close_time") not in (None, "")
|
||||||
if closed_flag and is_synthetic_close(t.get("snapshot_json")):
|
if closed_flag and is_synthetic_close(t.get("snapshot_json")):
|
||||||
@@ -560,6 +609,13 @@ def trade_stats(
|
|||||||
continue
|
continue
|
||||||
if ots < start_ts or ots >= end_ts:
|
if ots < start_ts or ots >= end_ts:
|
||||||
continue
|
continue
|
||||||
|
if closed_flag:
|
||||||
|
tk = str(t.get("ticket") or "").strip()
|
||||||
|
if tk:
|
||||||
|
if tk in seen_closed_ticket:
|
||||||
|
skipped_dup_ticket += 1
|
||||||
|
continue
|
||||||
|
seen_closed_ticket.add(tk)
|
||||||
rows.append(t)
|
rows.append(t)
|
||||||
|
|
||||||
total = len(rows)
|
total = len(rows)
|
||||||
@@ -656,6 +712,7 @@ def trade_stats(
|
|||||||
"annotated": annotated,
|
"annotated": annotated,
|
||||||
"unannotated": total - annotated,
|
"unannotated": total - annotated,
|
||||||
"skipped_synthetic": skipped_synthetic,
|
"skipped_synthetic": skipped_synthetic,
|
||||||
|
"skipped_dup_ticket": skipped_dup_ticket,
|
||||||
"period": period,
|
"period": period,
|
||||||
"day": day_label,
|
"day": day_label,
|
||||||
"by_hold": [_finalize_group(by_hold[k]) for k in ("lt_5m", "m5_30", "m30_2h", "ge_2h", "unknown") if by_hold[k]["count"] or k != "unknown"],
|
"by_hold": [_finalize_group(by_hold[k]) for k in ("lt_5m", "m5_30", "m30_2h", "ge_2h", "unknown") if by_hold[k]["count"] or k != "unknown"],
|
||||||
|
|||||||
+25
-12
@@ -782,20 +782,32 @@ def register_routes(app: Flask) -> None:
|
|||||||
)
|
)
|
||||||
tid = int(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, ""):
|
# - 上报持仓:关掉其它未平仓副本
|
||||||
|
# - 上报平仓:其它同单号副本盈亏清空(防 history 重复插入双计)
|
||||||
|
if ticket:
|
||||||
dup_snap = json.dumps(
|
dup_snap = json.dumps(
|
||||||
{"event": "dedupe_closed", "kept_id": tid}, ensure_ascii=False
|
{"event": "dedupe_closed", "kept_id": tid}, ensure_ascii=False
|
||||||
)
|
)
|
||||||
conn.execute(
|
if close_time in (None, ""):
|
||||||
"""
|
conn.execute(
|
||||||
UPDATE trades
|
"""
|
||||||
SET close_time=?, profit=NULL, snapshot_json=?
|
UPDATE trades
|
||||||
WHERE account_id=? AND ticket=? AND id!=?
|
SET close_time=?, profit=NULL, snapshot_json=?
|
||||||
AND (close_time IS NULL OR close_time='')
|
WHERE account_id=? AND ticket=? AND id!=?
|
||||||
""",
|
AND (close_time IS NULL OR close_time='')
|
||||||
(database.now(), dup_snap, act["account_id"], ticket, tid),
|
""",
|
||||||
)
|
(database.now(), dup_snap, act["account_id"], ticket, tid),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE trades
|
||||||
|
SET profit=NULL, snapshot_json=?
|
||||||
|
WHERE account_id=? AND ticket=? AND id!=?
|
||||||
|
""",
|
||||||
|
(dup_snap, act["account_id"], ticket, tid),
|
||||||
|
)
|
||||||
return jsonify({"ok": True, "id": tid})
|
return jsonify({"ok": True, "id": tid})
|
||||||
|
|
||||||
@app.post("/api/v1/trades/reconcile")
|
@app.post("/api/v1/trades/reconcile")
|
||||||
@@ -932,8 +944,9 @@ def register_routes(app: Flask) -> None:
|
|||||||
account_id = None if u["role"] == "admin" else u.get("account_id")
|
account_id = None if u["role"] == "admin" else u.get("account_id")
|
||||||
machines, machine_id = _resolve_machine_filter(u, machine_raw)
|
machines, machine_id = _resolve_machine_filter(u, machine_raw)
|
||||||
with database.get_db() as conn:
|
with database.get_db() as conn:
|
||||||
# 清掉历史假平仓上误写的盈亏
|
# 清掉假平仓盈亏 + 同单号重复平仓双计
|
||||||
database.scrub_synthetic_trade_profits(conn)
|
database.scrub_synthetic_trade_profits(conn)
|
||||||
|
database.scrub_duplicate_ticket_profits(conn)
|
||||||
stats = database.trade_stats(
|
stats = database.trade_stats(
|
||||||
conn,
|
conn,
|
||||||
account_id=account_id,
|
account_id=account_id,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
默认按北京时间<strong>今日真实平仓</strong>统计;对账/去重产生的假平仓已排除。
|
默认按北京时间<strong>今日真实平仓</strong>统计;对账/去重产生的假平仓已排除。
|
||||||
{% if selected_period == 'today' and stats.day %}当前:{{ stats.day }}{% endif %}
|
{% if selected_period == 'today' and stats.day %}当前:{{ stats.day }}{% endif %}
|
||||||
{% if stats.skipped_synthetic %} · 已排除假平仓 {{ stats.skipped_synthetic }} 笔{% endif %}
|
{% if stats.skipped_synthetic %} · 已排除假平仓 {{ stats.skipped_synthetic }} 笔{% endif %}
|
||||||
|
{% if stats.skipped_dup_ticket %} · 已排除重复单号 {{ stats.skipped_dup_ticket }} 笔{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap">
|
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap">
|
||||||
|
|||||||
Reference in New Issue
Block a user