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' %}