修复读取
This commit is contained in:
+75
-32
@@ -4742,59 +4742,83 @@ def fetch_gate_positions_close_history():
|
|||||||
return []
|
return []
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
since_ms = exchange_position_sync_since_ms()
|
since_ms = exchange_position_sync_since_ms()
|
||||||
try:
|
until_ms = int(time.time() * 1000)
|
||||||
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 []
|
|
||||||
out = []
|
out = []
|
||||||
for p in rows or []:
|
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)
|
h = _normalize_gate_position_history_entry(p)
|
||||||
if h and h["close_ms"] and h["side"] in ("long", "short") and h["symbol_u"]:
|
if h and h["close_ms"] and h["side"] in ("long", "short") and h["symbol_u"]:
|
||||||
out.append(h)
|
out.append(h)
|
||||||
return out
|
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):
|
def sync_trade_records_from_exchange(conn, force=False):
|
||||||
"""为未同步的 trade_records 回填 Gate 平仓历史中的已实现盈亏。"""
|
"""为未同步的 trade_records 回填 Gate 平仓历史中的已实现盈亏。返回统计 dict。"""
|
||||||
global _LAST_EXCHANGE_PNL_SYNC_AT
|
global _LAST_EXCHANGE_PNL_SYNC_AT
|
||||||
|
stats = {"ok": False, "hist_count": 0, "matched": 0, "pending": 0, "skipped": False}
|
||||||
if not exchange_private_api_configured():
|
if not exchange_private_api_configured():
|
||||||
return
|
stats["reason"] = "未配置 GATE_API_KEY / GATE_API_SECRET"
|
||||||
|
return stats
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - _LAST_EXCHANGE_PNL_SYNC_AT < 25.0:
|
if not force and now - _LAST_EXCHANGE_PNL_SYNC_AT < 25.0:
|
||||||
return
|
stats["ok"] = True
|
||||||
|
stats["skipped"] = True
|
||||||
|
return stats
|
||||||
try:
|
try:
|
||||||
hist = fetch_gate_positions_close_history()
|
hist = fetch_gate_positions_close_history()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
return
|
stats["reason"] = str(e)
|
||||||
|
return stats
|
||||||
|
stats["hist_count"] = len(hist)
|
||||||
if not hist:
|
if not hist:
|
||||||
_LAST_EXCHANGE_PNL_SYNC_AT = now
|
stats["ok"] = True
|
||||||
return
|
stats["reason"] = "交易所平仓历史为空(请检查 API 权限或 EXCHANGE_POSITION_SYNC_FROM_BJ)"
|
||||||
|
return stats
|
||||||
candidates = conn.execute(
|
candidates = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, symbol, direction, closed_at, closed_at_ms, opened_at, opened_at_ms
|
SELECT id, symbol, direction, closed_at, closed_at_ms, opened_at, opened_at_ms
|
||||||
FROM trade_records
|
FROM trade_records
|
||||||
WHERE (exchange_sync_key IS NULL OR TRIM(exchange_sync_key) = '')
|
WHERE (exchange_sync_key IS NULL OR TRIM(exchange_sync_key) = '')
|
||||||
|
OR exchange_realized_pnl IS NULL
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT 200
|
LIMIT 200
|
||||||
"""
|
"""
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
stats["pending"] = len(candidates)
|
||||||
if not candidates:
|
if not candidates:
|
||||||
|
stats["ok"] = True
|
||||||
_LAST_EXCHANGE_PNL_SYNC_AT = now
|
_LAST_EXCHANGE_PNL_SYNC_AT = now
|
||||||
return
|
return stats
|
||||||
used = set()
|
used = set()
|
||||||
|
matched = 0
|
||||||
for tr in candidates:
|
for tr in candidates:
|
||||||
close_ms_trade = _to_ms_with_fallback(
|
close_ms_trade = _to_ms_with_fallback(
|
||||||
tr["closed_at_ms"] if "closed_at_ms" in tr.keys() else None, tr["closed_at"]
|
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"])),
|
(float(pnl_val), eo, ec, sk, int(tr["id"])),
|
||||||
)
|
)
|
||||||
used.add(sk)
|
used.add(sk)
|
||||||
|
matched += 1
|
||||||
|
stats["matched"] = matched
|
||||||
|
stats["ok"] = True
|
||||||
_LAST_EXCHANGE_PNL_SYNC_AT = now
|
_LAST_EXCHANGE_PNL_SYNC_AT = now
|
||||||
try:
|
try:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
# ====================== 主页面 ======================
|
# ====================== 主页面 ======================
|
||||||
@@ -4874,11 +4902,12 @@ def render_main_page(page="trade"):
|
|||||||
order_list = []
|
order_list = []
|
||||||
for o in raw_order_list:
|
for o in raw_order_list:
|
||||||
order_list.append(enrich_order_item(row_to_dict(o), current_capital))
|
order_list.append(enrich_order_item(row_to_dict(o), current_capital))
|
||||||
|
exchange_pnl_sync = {}
|
||||||
if exchange_private_api_configured():
|
if exchange_private_api_configured():
|
||||||
try:
|
try:
|
||||||
sync_trade_records_from_exchange(conn)
|
exchange_pnl_sync = sync_trade_records_from_exchange(conn) or {}
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
exchange_pnl_sync = {"ok": False, "reason": str(e)}
|
||||||
raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall()
|
raw_records = conn.execute("SELECT * FROM trade_records ORDER BY id DESC").fetchall()
|
||||||
records = [to_effective_trade_dict(r) for r in raw_records]
|
records = [to_effective_trade_dict(r) for r in raw_records]
|
||||||
total = len(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_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR,
|
||||||
key_gate_rule_text=key_gate_rule_text,
|
key_gate_rule_text=key_gate_rule_text,
|
||||||
kline_timeframe=KLINE_TIMEFRAME,
|
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("/")
|
@app.route("/")
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
|
|||||||
@@ -493,6 +493,16 @@
|
|||||||
{% if page == 'records' %}
|
{% if page == 'records' %}
|
||||||
<div class="card full records-card">
|
<div class="card full records-card">
|
||||||
<h2>交易记录 & 错过机会</h2>
|
<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">
|
<div class="form-row" style="margin-bottom:10px;gap:8px">
|
||||||
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
|
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
|
||||||
<input id="review-mode-toggle" type="checkbox">
|
<input id="review-mode-toggle" type="checkbox">
|
||||||
@@ -1704,6 +1714,24 @@ function refreshPriceSnapshotConditional(){
|
|||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user