修复读取
This commit is contained in:
+78
-35
@@ -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():
|
||||
|
||||
@@ -493,6 +493,16 @@
|
||||
{% if page == 'records' %}
|
||||
<div class="card full records-card">
|
||||
<h2>交易记录 & 错过机会</h2>
|
||||
<div class="rule-tip" style="margin-bottom:8px;font-size:.78rem">
|
||||
盈亏U:<span style="color:#6ab88a">所</span>=交易所平仓历史,
|
||||
<span style="color:#8892b0">估</span>=本地估算。
|
||||
{% 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 %}
|
||||
<button type="button" id="sync-exchange-pnl-btn" style="margin-left:8px;padding:4px 10px;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:6px;cursor:pointer">立即同步</button>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:10px;gap:8px">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
|
||||
<input id="review-mode-toggle" type="checkbox">
|
||||
@@ -1704,6 +1714,24 @@ function refreshPriceSnapshotConditional(){
|
||||
}).catch(()=>{});
|
||||
}
|
||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||
|
||||
const syncExchangePnlBtn = document.getElementById("sync-exchange-pnl-btn");
|
||||
if(syncExchangePnlBtn){
|
||||
syncExchangePnlBtn.addEventListener("click", ()=>{
|
||||
syncExchangePnlBtn.disabled = true;
|
||||
syncExchangePnlBtn.innerText = "同步中…";
|
||||
fetch("/api/sync_exchange_pnl").then(r=>r.json()).then(data=>{
|
||||
const msg = data.ok
|
||||
? `平仓历史 ${data.hist_count||0} 条,对齐 ${data.matched||0} 笔${data.reason?("\n"+data.reason):""}`
|
||||
: (data.reason || "同步失败");
|
||||
alert(msg);
|
||||
if((data.matched||0) > 0) location.reload();
|
||||
}).catch(()=>alert("同步请求失败")).finally(()=>{
|
||||
syncExchangePnlBtn.disabled = false;
|
||||
syncExchangePnlBtn.innerText = "立即同步";
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user