修复币安交易记录

This commit is contained in:
dekun
2026-05-19 17:49:33 +08:00
parent 44432a0688
commit 5a2e66c038
2 changed files with 442 additions and 59 deletions
+418 -59
View File
@@ -2002,23 +2002,196 @@ def format_hold_minutes(minutes):
return f"{mins}分钟"
def calc_pnl(direction, trigger_price, exit_price, margin_capital, leverage):
def calc_pnl(direction, trigger_price, exit_price, margin_capital, leverage, notional_usdt=None):
"""估算盈亏(USDT)。优先用名义价值 notional_usdt,否则 margin×leverage。"""
try:
trigger = float(trigger_price)
exit_p = float(exit_price)
margin = float(margin_capital)
lev = float(leverage)
if trigger <= 0:
return 0.0
if notional_usdt is not None:
notional = float(notional_usdt)
else:
margin = float(margin_capital)
lev = float(leverage)
notional = margin * lev
if notional <= 0:
return 0.0
if direction == "short":
pnl_ratio = (trigger - exit_p) / trigger
else:
pnl_ratio = (exit_p - trigger) / trigger
return round(margin * lev * pnl_ratio, FUNDS_DECIMALS)
return round(notional * pnl_ratio, FUNDS_DECIMALS)
except Exception:
return 0.0
def get_plan_notional_usdt(row_or_dict):
"""计划名义价值(USDT),与开仓 sizing 口径一致。"""
if row_or_dict is None:
return None
try:
if hasattr(row_or_dict, "keys"):
nv = row_or_dict["notional_value"] if "notional_value" in row_or_dict.keys() else None
margin = row_or_dict["margin_capital"] if "margin_capital" in row_or_dict.keys() else None
lev = row_or_dict["leverage"] if "leverage" in row_or_dict.keys() else None
sym = row_or_dict["symbol"] if "symbol" in row_or_dict.keys() else ""
else:
nv = row_or_dict.get("notional_value")
margin = row_or_dict.get("margin_capital")
lev = row_or_dict.get("leverage")
sym = row_or_dict.get("symbol") or ""
except Exception:
return None
try:
if nv is not None and str(nv).strip() != "":
v = float(nv)
if v > 0:
return round(v, FUNDS_DECIMALS)
except (TypeError, ValueError):
pass
try:
margin = float(margin or 0)
lev = float(lev or infer_leverage(sym) or 0)
if margin > 0 and lev > 0:
return round(margin * lev, FUNDS_DECIMALS)
except (TypeError, ValueError):
pass
return None
def _trade_ids_from_fills(trades):
ids = set()
for t in trades or []:
info = t.get("info") if isinstance(t.get("info"), dict) else {}
for src in (t, info):
if not isinstance(src, dict):
continue
for k in ("tradeId", "trade_id", "id"):
v = src.get(k)
if v is not None and str(v).strip() != "":
ids.add(str(v).strip())
return ids
def _income_entry_trade_id(entry):
if not isinstance(entry, dict):
return ""
info = entry.get("info") if isinstance(entry.get("info"), dict) else {}
for src in (entry, info):
for k in ("tradeId", "trade_id"):
v = src.get(k)
if v is not None and str(v).strip() != "":
return str(v).strip()
return ""
def calc_pnl_from_closing_trades(direction, entry_price, trades, exchange_symbol=None):
"""按减仓成交数量×价差汇总盈亏(不含资金费;比单点标记价更接近交易所)。"""
try:
entry = float(entry_price)
except (TypeError, ValueError):
return None
if entry <= 0 or not trades:
return None
contract_size = 1.0
if exchange_symbol and BINANCE_API_KEY and BINANCE_API_SECRET:
try:
ensure_markets_loaded()
contract_size = float(exchange.market(exchange_symbol).get("contractSize") or 1)
except Exception:
contract_size = 1.0
pnl = 0.0
qty = 0.0
for t in trades:
try:
price = float(t.get("price") or 0)
amount = float(t.get("amount") or 0) * contract_size
except (TypeError, ValueError):
continue
if price <= 0 or amount <= 0:
continue
qty += amount
if direction == "short":
pnl += amount * (entry - price)
else:
pnl += amount * (price - entry)
if qty <= 0:
return None
return round(pnl, FUNDS_DECIMALS)
def resolve_trade_pnl_amount(
row,
entry_price,
exit_price=None,
opened_at_str=None,
opened_at_ms=None,
closed_at_str=None,
closed_at_ms=None,
):
"""
平仓盈亏优先 Binance income 净额含手续费其次按减仓成交汇总最后用计划名义×涨跌
返回 (pnl, exit_price, exchange_opened_at, exchange_closed_at, exchange_sync_key)
"""
direction = (row["direction"] if hasattr(row, "keys") else row.get("direction") or "long").strip().lower()
sym = row["symbol"] if hasattr(row, "keys") else row.get("symbol")
ex_sym = (
row["exchange_symbol"]
if hasattr(row, "keys") and "exchange_symbol" in row.keys()
else row.get("exchange_symbol")
) or normalize_exchange_symbol(sym)
open_ms = _to_ms_with_fallback(
opened_at_ms if opened_at_ms is not None else (row["opened_at_ms"] if hasattr(row, "keys") and "opened_at_ms" in row.keys() else None),
opened_at_str or (row["opened_at"] if hasattr(row, "keys") else row.get("opened_at")),
)
close_ms = _to_ms_with_fallback(
closed_at_ms,
closed_at_str,
)
closing_trades = []
if open_ms and (close_ms or closed_at_str):
closing_trades = fetch_closing_fills_for_record(
ex_sym,
direction,
opened_at_str or (row["opened_at"] if hasattr(row, "keys") else ""),
closed_at_str,
opened_at_ms=open_ms,
closed_at_ms=close_ms,
)
if closing_trades:
wexit = calc_weighted_exit_price(closing_trades)
if wexit and (exit_price is None or float(exit_price or 0) <= 0):
exit_price = wexit
last_ts = closing_trades[-1].get("timestamp")
if last_ts and not closed_at_str:
closed_at_str = ms_to_app_local_str(int(last_ts))
close_ms = int(last_ts)
net, sync_key, eo, ec = fetch_binance_net_pnl_for_trade(
ex_sym, direction, open_ms, close_ms, closing_trades=closing_trades
)
if net is not None:
return net, exit_price, eo, ec, sync_key
if closing_trades:
fill_pnl = calc_pnl_from_closing_trades(direction, entry_price, closing_trades, ex_sym)
if fill_pnl is not None:
return fill_pnl, exit_price, None, None, None
notional = get_plan_notional_usdt(row)
margin = row["margin_capital"] if hasattr(row, "keys") else row.get("margin_capital")
lev = row["leverage"] if hasattr(row, "keys") else row.get("leverage")
if exit_price:
pnl = calc_pnl(
direction,
entry_price,
exit_price,
margin or DAILY_START_CAPITAL,
lev or infer_leverage(sym),
notional_usdt=notional,
)
return pnl, exit_price, None, None, None
return 0.0, exit_price, None, None, None
def calc_rr_ratio(direction, entry_price, stop_loss, take_profit):
try:
entry = float(entry_price)
@@ -2149,7 +2322,7 @@ def insert_trade_record(
kst = key_signal_type_for_trade_record(key_signal_type, KEY_MONITOR_AUTO_TYPES)
snap_sl = initial_stop_loss if initial_stop_loss not in (None, "") else stop_loss
er = (entry_reason or "").strip() or entry_reason_from_key_signal(kst) or ""
conn.execute(
cur = conn.execute(
"INSERT INTO trade_records (symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id,entry_reason) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol, monitor_type, kst, direction, trigger_price, snap_sl, snap_sl, take_profit,
@@ -2158,6 +2331,13 @@ def insert_trade_record(
open_ts, open_ts_ms, close_ts, close_ts_ms, result, miss_reason, exchange_trade_id, er or None
)
)
record_id = int(cur.lastrowid or 0)
if record_id and close_ts and result != "错过" and exchange_private_api_configured():
try:
sync_trade_record_exchange_pnl(conn, record_id, commit=False)
except Exception:
pass
return record_id
def calc_duration_text(open_str, close_str):
@@ -3484,49 +3664,79 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
trigger_price = row["trigger_price"]
stop_loss = row["stop_loss"]
take_profit = row["take_profit"]
margin_capital = row["margin_capital"] or DAILY_START_CAPITAL
leverage = row["leverage"] or infer_leverage(sym)
exchange_symbol = row["exchange_symbol"] or normalize_exchange_symbol(sym)
trade = fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_at_ms=opened_at_ms)
exit_px = None
closed_at_str = app_now_str()
if trade:
try:
exit_px = float(trade.get("price") or 0) or None
except (TypeError, ValueError):
exit_px = None
ts = trade.get("timestamp")
if ts:
closed_at_str = ms_to_app_local_str(int(ts))
closed_at_ms = None
closing_trades = fetch_closing_fills_for_record(
exchange_symbol, direction, opened_at_str, None, opened_at_ms=opened_at_ms
)
exit_px = calc_weighted_exit_price(closing_trades) if closing_trades else None
if exit_px is None:
trade = fetch_latest_closing_fill(exchange_symbol, direction, opened_at_str, opened_at_ms=opened_at_ms)
if trade:
try:
exit_px = float(trade.get("price") or 0) or None
except (TypeError, ValueError):
exit_px = None
if not closing_trades:
closing_trades = [trade]
if closing_trades:
last_ts = closing_trades[-1].get("timestamp")
if last_ts:
closed_at_str = ms_to_app_local_str(int(last_ts))
closed_at_ms = int(last_ts)
open_ms = _to_ms_with_fallback(
row["opened_at_ms"] if "opened_at_ms" in row.keys() else None, opened_at_str
)
close_ms = _to_ms_with_fallback(closed_at_ms, closed_at_str)
pnl, exit_px2, _, _, _ = resolve_trade_pnl_amount(
row,
trigger_price,
exit_px,
opened_at_str=opened_at_str,
opened_at_ms=open_ms,
closed_at_str=closed_at_str,
closed_at_ms=close_ms,
)
if exit_px2:
exit_px = float(exit_px2)
if exit_px is None or exit_px <= 0:
p = get_price(sym)
if p:
guessed = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, p)
if guessed:
pnl = calc_pnl(direction, trigger_price, p, margin_capital, leverage)
pnl2, _, _, _, _ = resolve_trade_pnl_amount(
row,
trigger_price,
p,
opened_at_str=opened_at_str,
opened_at_ms=open_ms,
closed_at_str=closed_at_str,
closed_at_ms=close_ms,
)
return (
guessed,
pnl,
pnl2,
closed_at_str,
"未能拉取成交明细,按当前市价与止盈/止损位近似归类(建议核对交易所账单)",
)
return (
"外部平仓",
0.0,
pnl,
closed_at_str,
"检测到交易所仓位已关闭,且无法从成交记录还原平仓价",
)
result = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_px)
pnl = calc_pnl(direction, trigger_price, exit_px, margin_capital, leverage)
if result:
return (
result,
pnl,
closed_at_str,
"按交易所成交记录同步为止盈/止损平仓",
"按交易所成交/流水同步为止盈/止损平仓",
)
return (
"外部平仓",
@@ -4733,6 +4943,7 @@ def check_order_monitors():
else:
res = normalize_result_with_pnl(res, pnl_amount)
close_order_id = ""
exit_p = None
try:
close_resp = close_exchange_order(r)
close_order_id = close_resp.get("id", "")
@@ -4804,6 +5015,16 @@ def check_order_monitors():
hold_seconds = calc_hold_seconds(
opened_at, parse_dt_for_trading_day(closed_at) or now
)
exit_ref = exit_p if exit_p and float(exit_p) > 0 else p
pnl_amount, _, _, _, _ = resolve_trade_pnl_amount(
r,
trigger_price,
exit_ref,
opened_at_str=opened_at,
opened_at_ms=_to_ms_with_fallback(opened_at_ms, opened_at),
closed_at_str=closed_at,
closed_at_ms=_to_ms_with_fallback(None, closed_at),
)
insert_trade_record(
conn,
symbol=sym,
@@ -4859,6 +5080,16 @@ def check_order_monitors():
)
continue
cancel_binance_futures_open_orders(r["exchange_symbol"] or normalize_exchange_symbol(sym))
exit_ref = exit_p if exit_p and float(exit_p) > 0 else p
pnl_amount, _, _, _, _ = resolve_trade_pnl_amount(
r,
trigger_price,
exit_ref,
opened_at_str=opened_at,
opened_at_ms=_to_ms_with_fallback(opened_at_ms, opened_at),
closed_at_str=closed_at,
closed_at_ms=_to_ms_with_fallback(None, closed_at),
)
session_capital = update_session_capital(conn, session_date, pnl_amount)
send_wechat_msg(
build_wechat_close_message(
@@ -5077,6 +5308,47 @@ def api_sync_positions():
return jsonify({"ok": True, "days": days, "synced": int(synced)})
@app.route("/api/sync_exchange_pnl", methods=["POST"])
@login_required
def api_sync_exchange_pnl():
"""立即为近期未同步记录回填 Binance 净盈亏(含手续费)。"""
if not exchange_private_api_configured():
return jsonify({"ok": False, "msg": "未配置 Binance API,无法同步"}), 400
payload = request.get_json(silent=True) or {}
limit = 120
force_all = str(payload.get("force", "")).lower() in ("1", "true", "yes")
try:
if payload.get("limit") is not None:
limit = max(1, min(500, int(payload.get("limit"))))
except (TypeError, ValueError):
pass
global _LAST_EXCHANGE_PNL_SYNC_AT
_LAST_EXCHANGE_PNL_SYNC_AT = 0.0
conn = get_db()
if force_all:
rows = conn.execute(
"SELECT id FROM trade_records ORDER BY id DESC LIMIT ?",
(limit,),
).fetchall()
else:
rows = conn.execute(
"""
SELECT id FROM trade_records
WHERE (exchange_sync_key IS NULL OR TRIM(exchange_sync_key) = '')
ORDER BY id DESC
LIMIT ?
""",
(limit,),
).fetchall()
synced = 0
for r in rows:
if sync_trade_record_exchange_pnl(conn, int(r["id"]), commit=False, force=True):
synced += 1
conn.commit()
conn.close()
return jsonify({"ok": True, "synced": synced, "candidates": len(rows)})
def _coerce_ts_ms(val):
if val is None or val == "":
return None
@@ -5153,7 +5425,9 @@ def _fetch_binance_income_entries(exchange_symbol, start_ms, end_ms):
return out
def fetch_binance_net_pnl_for_trade(exchange_symbol, direction, open_ms, close_ms):
def fetch_binance_net_pnl_for_trade(
exchange_symbol, direction, open_ms, close_ms, closing_trades=None
):
if open_ms is None or close_ms is None or close_ms < open_ms:
return None, None, None, None
buffer_ms = 5 * 60 * 1000
@@ -5162,6 +5436,7 @@ def fetch_binance_net_pnl_for_trade(exchange_symbol, direction, open_ms, close_m
)
if not entries:
return None, None, None, None
trade_ids = _trade_ids_from_fills(closing_trades) if closing_trades else None
net = 0.0
first_t = None
last_t = None
@@ -5169,6 +5444,10 @@ def fetch_binance_net_pnl_for_trade(exchange_symbol, direction, open_ms, close_m
it = (e.get("incomeType") or e.get("income_type") or "").strip()
if it not in BINANCE_NET_INCOME_TYPES:
continue
if trade_ids and it in ("REALIZED_PNL", "COMMISSION"):
tid = _income_entry_trade_id(e)
if tid and tid not in trade_ids:
continue
try:
net += float(e.get("income") or 0)
except (TypeError, ValueError):
@@ -5189,6 +5468,76 @@ def fetch_binance_net_pnl_for_trade(exchange_symbol, direction, open_ms, close_m
return net, sync_key, eo, ec
def sync_trade_record_exchange_pnl(conn, record_id, commit=True, force=False):
"""单条 trade_records 回填 Binance 净盈亏;成功时同时更新 pnl_amount 便于统计。"""
if not exchange_private_api_configured():
return False
tr = conn.execute("SELECT * FROM trade_records WHERE id=?", (int(record_id),)).fetchone()
if not tr:
return False
sk_existing = (tr["exchange_sync_key"] if "exchange_sync_key" in tr.keys() else None) or ""
if not force and str(sk_existing).strip():
return tr["exchange_realized_pnl"] is not None
direction = (tr["direction"] or "long").strip().lower()
open_ms = _to_ms_with_fallback(
tr["opened_at_ms"] if "opened_at_ms" in tr.keys() else None, tr["opened_at"]
)
close_ms = _to_ms_with_fallback(
tr["closed_at_ms"] if "closed_at_ms" in tr.keys() else None, tr["closed_at"]
)
if open_ms is None or close_ms is None:
return False
try:
ex_sym = normalize_exchange_symbol(tr["symbol"])
except Exception:
return False
closing_trades = fetch_closing_fills_for_record(
ex_sym,
direction,
tr["opened_at"],
tr["closed_at"],
opened_at_ms=open_ms,
closed_at_ms=close_ms,
)
net, sync_key, eo, ec = None, None, None, None
for attempt in range(3):
if attempt:
time.sleep(0.7)
net, sync_key, eo, ec = fetch_binance_net_pnl_for_trade(
ex_sym, direction, open_ms, close_ms, closing_trades=closing_trades
)
if net is not None and sync_key:
break
if net is None:
net = calc_pnl_from_closing_trades(
direction, tr["trigger_price"], closing_trades, ex_sym
)
if net is not None:
try:
ensure_markets_loaded()
cid = exchange.market(ex_sym).get("id") or ex_sym
except Exception:
cid = ex_sym
sync_key = f"fills|{cid}|{direction}|{open_ms}|{close_ms}|{net}"
if net is None or not sync_key:
return False
conn.execute(
"""
UPDATE trade_records
SET exchange_realized_pnl = ?, exchange_opened_at = ?, exchange_closed_at = ?,
exchange_sync_key = ?, pnl_amount = ?
WHERE id = ?
""",
(float(net), eo, ec, sync_key, float(net), int(record_id)),
)
if commit:
try:
conn.commit()
except Exception:
pass
return True
def sync_trade_records_from_exchange(conn):
"""为未同步的 trade_records 回填交易所口径净盈亏(Binance:income 流水汇总)。"""
global _LAST_EXCHANGE_PNL_SYNC_AT
@@ -5199,8 +5548,7 @@ def sync_trade_records_from_exchange(conn):
return
candidates = conn.execute(
"""
SELECT id, symbol, direction, opened_at, opened_at_ms, closed_at, closed_at_ms
FROM trade_records
SELECT id FROM trade_records
WHERE (exchange_sync_key IS NULL OR TRIM(exchange_sync_key) = '')
ORDER BY id DESC
LIMIT 120
@@ -5210,30 +5558,7 @@ def sync_trade_records_from_exchange(conn):
_LAST_EXCHANGE_PNL_SYNC_AT = now
return
for tr in candidates:
direction = (tr["direction"] or "long").strip().lower()
open_ms = _to_ms_with_fallback(
tr["opened_at_ms"] if "opened_at_ms" in tr.keys() else None, tr["opened_at"]
)
close_ms = _to_ms_with_fallback(
tr["closed_at_ms"] if "closed_at_ms" in tr.keys() else None, tr["closed_at"]
)
if open_ms is None or close_ms is None:
continue
try:
ex_sym = normalize_exchange_symbol(tr["symbol"])
except Exception:
continue
net, sync_key, eo, ec = fetch_binance_net_pnl_for_trade(ex_sym, direction, open_ms, close_ms)
if net is None or not sync_key:
continue
conn.execute(
"""
UPDATE trade_records
SET exchange_realized_pnl = ?, exchange_opened_at = ?, exchange_closed_at = ?, exchange_sync_key = ?
WHERE id = ?
""",
(float(net), eo, ec, sync_key, int(tr["id"])),
)
sync_trade_record_exchange_pnl(conn, int(tr["id"]), commit=False)
_LAST_EXCHANGE_PNL_SYNC_AT = now
try:
conn.commit()
@@ -6548,21 +6873,55 @@ def del_order(id):
return redirect("/")
if row["status"] == "active":
try:
p = get_price(row["symbol"]) or float(row["trigger_price"])
opened_at = get_opened_at_value(row)
closed_at = app_now_str()
hold_seconds = calc_hold_seconds(opened_at, app_now())
pnl_amount = calc_pnl(
row["direction"],
row["trigger_price"],
p,
row["margin_capital"] or DAILY_START_CAPITAL,
row["leverage"] or infer_leverage(row["symbol"])
opened_at_ms = _to_ms_with_fallback(
row["opened_at_ms"] if "opened_at_ms" in row.keys() else None, opened_at
)
close_resp = close_exchange_order(row)
close_order_id = close_resp.get("id", "")
cancel_binance_futures_open_orders(row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"]))
session_date = row["session_date"] or get_trading_day()
exit_p = extract_trade_price_from_order(close_resp)
closed_at = app_now_str()
closed_at_ms = None
if not exit_p or float(exit_p) <= 0:
tr_fill = fetch_latest_closing_fill(
row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"]),
row["direction"],
opened_at,
opened_at_ms=opened_at_ms,
)
if tr_fill and tr_fill.get("price"):
try:
exit_p = float(tr_fill["price"])
except (TypeError, ValueError):
exit_p = None
ts = tr_fill.get("timestamp")
if ts:
closed_at = ms_to_app_local_str(int(ts))
closed_at_ms = int(ts)
else:
tr_fill = fetch_latest_closing_fill(
row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"]),
row["direction"],
opened_at,
opened_at_ms=opened_at_ms,
)
if tr_fill and tr_fill.get("timestamp"):
closed_at = ms_to_app_local_str(int(tr_fill["timestamp"]))
closed_at_ms = int(tr_fill["timestamp"])
pnl_amount, exit_p, _, _, _ = resolve_trade_pnl_amount(
row,
row["trigger_price"],
exit_p,
opened_at_str=opened_at,
opened_at_ms=opened_at_ms,
closed_at_str=closed_at,
closed_at_ms=closed_at_ms,
)
p = exit_p or get_price(row["symbol"]) or float(row["trigger_price"])
closed_at_dt = parse_dt_for_trading_day(closed_at) or app_now()
hold_seconds = calc_hold_seconds(opened_at, closed_at_dt)
session_date = row["session_date"] or get_trading_day(closed_at_dt)
session_capital = update_session_capital(conn, session_date, pnl_amount)
insert_trade_record(
conn,
@@ -231,6 +231,7 @@
<a href="/export/journal_entries">复盘记录</a>
<a href="/export/key_monitors">关键位(当前)</a>
<a href="/export/key_monitor_history">关键位历史</a>
<button type="button" id="btn-sync-exchange-pnl" style="padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#1f3a5a;color:#8fc8ff;cursor:pointer;font-size:.85rem">同步交易所盈亏</button>
</div>
<div class="stat-box">
<div class="stat-item"><div class="label">交易所</div><div class="value">{{ exchange_display }}</div></div>
@@ -999,6 +1000,29 @@ function attachListWindowToExports(){
});
}
function syncExchangePnl(force){
const btn = document.getElementById("btn-sync-exchange-pnl");
if(btn) btn.disabled = true;
fetch("/api/sync_exchange_pnl", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({limit: 200, force: !!force})
}).then(r=>r.json()).then(data=>{
if(data.ok){
alert("已同步 " + (data.synced||0) + " / " + (data.candidates||0) + " 条记录,页面将刷新");
window.location.reload();
} else {
alert(data.msg || "同步失败");
}
}).catch(()=>alert("同步请求失败")).finally(()=>{ if(btn) btn.disabled = false; });
}
document.getElementById("btn-sync-exchange-pnl")?.addEventListener("click", function(){
if(confirm("从 Binance 流水回填盈亏(含手续费)?将覆盖未复盘记录的展示盈亏。")){
syncExchangePnl(true);
}
});
function loadJournals(){
const qs = listWindowQueryString();
fetch("/api/journals" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{