From a250c28ceb2c33eca5e7c01c9452633862266c6d Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 17 May 2026 17:12:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_gate/app.py | 113 ++++++++++++++++------- crypto_monitor_gate/templates/index.html | 28 ++++++ 2 files changed, 106 insertions(+), 35 deletions(-) diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index b025dbd..38f17be 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -4742,59 +4742,83 @@ def fetch_gate_positions_close_history(): return [] ensure_markets_loaded() since_ms = exchange_position_sync_since_ms() - try: - rows = exchange.fetch_positions_history( - None, - since=int(since_ms), - limit=int(EXCHANGE_POSITION_HISTORY_LIMIT), - params={"settle": "usdt"}, - ) - except Exception: - try: - rows = exchange.fetch_positions_history( - None, - since=int(since_ms), - limit=int(EXCHANGE_POSITION_HISTORY_LIMIT), - params={}, - ) - except Exception: - return [] + until_ms = int(time.time() * 1000) out = [] - for p in rows or []: - h = _normalize_gate_position_history_entry(p) - if h and h["close_ms"] and h["side"] in ("long", "short") and h["symbol_u"]: - out.append(h) - return out + offset = 0 + page_limit = min(100, int(EXCHANGE_POSITION_HISTORY_LIMIT)) + max_total = int(EXCHANGE_POSITION_HISTORY_LIMIT) + + def _pull(params_extra): + nonlocal offset + offset = 0 + while len(out) < max_total: + params = dict(params_extra) + params["offset"] = offset + params["until"] = until_ms + try: + rows = exchange.fetch_positions_history( + None, + since=int(since_ms), + limit=page_limit, + params=params, + ) + except Exception: + return False + if not rows: + break + for p in rows: + h = _normalize_gate_position_history_entry(p) + if h and h["close_ms"] and h["side"] in ("long", "short") and h["symbol_u"]: + out.append(h) + offset += len(rows) + if len(rows) < page_limit: + break + return True + + if not _pull({"settle": "usdt"}): + _pull({}) + return out[:max_total] -def sync_trade_records_from_exchange(conn): - """为未同步的 trade_records 回填 Gate 平仓历史中的已实现盈亏。""" +def sync_trade_records_from_exchange(conn, force=False): + """为未同步的 trade_records 回填 Gate 平仓历史中的已实现盈亏。返回统计 dict。""" global _LAST_EXCHANGE_PNL_SYNC_AT + stats = {"ok": False, "hist_count": 0, "matched": 0, "pending": 0, "skipped": False} if not exchange_private_api_configured(): - return + stats["reason"] = "未配置 GATE_API_KEY / GATE_API_SECRET" + return stats now = time.time() - if now - _LAST_EXCHANGE_PNL_SYNC_AT < 25.0: - return + if not force and now - _LAST_EXCHANGE_PNL_SYNC_AT < 25.0: + stats["ok"] = True + stats["skipped"] = True + return stats try: hist = fetch_gate_positions_close_history() - except Exception: - return + except Exception as e: + stats["reason"] = str(e) + return stats + stats["hist_count"] = len(hist) if not hist: - _LAST_EXCHANGE_PNL_SYNC_AT = now - return + stats["ok"] = True + stats["reason"] = "交易所平仓历史为空(请检查 API 权限或 EXCHANGE_POSITION_SYNC_FROM_BJ)" + return stats candidates = conn.execute( """ SELECT id, symbol, direction, closed_at, closed_at_ms, opened_at, opened_at_ms FROM trade_records WHERE (exchange_sync_key IS NULL OR TRIM(exchange_sync_key) = '') + OR exchange_realized_pnl IS NULL ORDER BY id DESC LIMIT 200 """ ).fetchall() + stats["pending"] = len(candidates) if not candidates: + stats["ok"] = True _LAST_EXCHANGE_PNL_SYNC_AT = now - return + return stats used = set() + matched = 0 for tr in candidates: close_ms_trade = _to_ms_with_fallback( tr["closed_at_ms"] if "closed_at_ms" in tr.keys() else None, tr["closed_at"] @@ -4848,11 +4872,15 @@ def sync_trade_records_from_exchange(conn): (float(pnl_val), eo, ec, sk, int(tr["id"])), ) used.add(sk) + matched += 1 + stats["matched"] = matched + stats["ok"] = True _LAST_EXCHANGE_PNL_SYNC_AT = now try: conn.commit() except Exception: pass + return stats # ====================== 主页面 ====================== @@ -4874,11 +4902,12 @@ def render_main_page(page="trade"): order_list = [] for o in raw_order_list: order_list.append(enrich_order_item(row_to_dict(o), current_capital)) + exchange_pnl_sync = {} if exchange_private_api_configured(): try: - sync_trade_records_from_exchange(conn) - except Exception: - pass + exchange_pnl_sync = sync_trade_records_from_exchange(conn) or {} + except Exception as e: + exchange_pnl_sync = {"ok": False, "reason": str(e)} raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall() records = [to_effective_trade_dict(r) for r in raw_records] total = len(records) @@ -4947,9 +4976,23 @@ def render_main_page(page="trade"): key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR, key_gate_rule_text=key_gate_rule_text, kline_timeframe=KLINE_TIMEFRAME, + exchange_pnl_sync=exchange_pnl_sync, ) +@app.route("/api/sync_exchange_pnl") +@login_required +def api_sync_exchange_pnl(): + conn = get_db() + stats = sync_trade_records_from_exchange(conn, force=True) + try: + conn.commit() + except Exception: + pass + conn.close() + return jsonify(stats) + + @app.route("/") @login_required def index(): diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index 2d32612..d484473 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -493,6 +493,16 @@ {% if page == 'records' %}

交易记录 & 错过机会

+
+ 盈亏U:=交易所平仓历史, + =本地估算。 + {% if exchange_pnl_sync %} + {% if exchange_pnl_sync.skipped %}(25秒内已同步,可点右侧按钮强制){% else %} + 本轮:平仓历史 {{ exchange_pnl_sync.hist_count or 0 }} 条,对齐 {{ exchange_pnl_sync.matched or 0 }} 笔{% if exchange_pnl_sync.reason %} — {{ exchange_pnl_sync.reason }}{% endif %} + {% endif %} + {% endif %} + +