修复审计P1:基数纠偏用开仓止损并清空币数量、Gate保证金/手动平仓成交价/全仓实盘空仓校验、OKX·Binance占位函数、快照超时。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-13 00:29:55 +08:00
parent 35b70777b8
commit 39878de7fc
8 changed files with 232 additions and 43 deletions
+3 -5
View File
@@ -13,10 +13,10 @@ def enrich_trade_price_displays(
if not isinstance(item, dict):
return item
try:
from lib.trade.trade_margin_record_lib import repair_stored_margin_capital
from lib.trade.trade_margin_record_lib import apply_repaired_margin_capital
fixed = repair_stored_margin_capital(
item.get("margin_capital"),
apply_repaired_margin_capital(
item,
trigger_price=item.get("trigger_price"),
leverage=item.get("leverage"),
symbol=item.get("symbol"),
@@ -24,8 +24,6 @@ def enrich_trade_price_displays(
initial_stop_loss=item.get("initial_stop_loss"),
risk_amount=item.get("risk_amount"),
)
if fixed is not None:
item["margin_capital"] = fixed
except Exception:
pass
if not format_price_fn:
+19 -4
View File
@@ -1173,6 +1173,17 @@ function accountSnapshotFundingMissing(data){
return !hasFunding && !hasTotal && !hasTrading;
}
let accountSnapshotRetryCount = 0;
let accountSnapshotInflight = false;
let priceSnapshotInflight = false;
function fetchJsonWithTimeout(url, timeoutMs) {
const ms = timeoutMs != null ? timeoutMs : 25000;
const ac = new AbortController();
const timer = setTimeout(function () { ac.abort(); }, ms);
return fetch(url, { signal: ac.signal, credentials: "same-origin" })
.then(function (r) { return r.json(); })
.finally(function () { clearTimeout(timer); });
}
function applyAccountSnapshot(data){
if(!data || typeof data !== "object") return;
if(typeof data.show_perp_funds !== "undefined"){
@@ -1263,8 +1274,10 @@ function applyAccountSnapshot(data){
}
function refreshAccountSnapshot(opts){
const options = opts || {};
if(accountSnapshotInflight && !options.force) return;
accountSnapshotInflight = true;
const qs = options.force ? "?force=1" : "";
fetch("/api/account_snapshot" + qs).then(r=>r.json()).then(data=>{
fetchJsonWithTimeout("/api/account_snapshot" + qs, 25000).then(data=>{
applyAccountSnapshot(data);
if(accountSnapshotFundingMissing(data) && !options.force && accountSnapshotRetryCount < 3){
accountSnapshotRetryCount += 1;
@@ -1277,7 +1290,7 @@ function refreshAccountSnapshot(opts){
accountSnapshotRetryCount += 1;
setTimeout(() => refreshAccountSnapshot({ silent: true }), 1200 * accountSnapshotRetryCount);
}
});
}).finally(()=>{ accountSnapshotInflight = false; });
}
const orderSymbolEl = document.getElementById("order-symbol");
@@ -1419,8 +1432,10 @@ refreshOrderDefaults();
if(typeof initOrderEntryModelSelect === "function") initOrderEntryModelSelect();
refreshPriceSnapshotConditional();
function refreshPriceSnapshotConditional(){
if(priceSnapshotInflight) return;
priceSnapshotInflight = true;
const page = document.body.getAttribute("data-page") || "";
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
fetchJsonWithTimeout("/api/price_snapshot", 25000).then(data=>{
const updatedEl = document.getElementById("price-last-updated");
if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at;
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
@@ -1474,7 +1489,7 @@ function refreshPriceSnapshotConditional(){
} else if (typeof data.options_unrealized_pnl !== "undefined") {
paintRealtimePnlFromSnapshot(data);
}
}).catch(()=>{});
}).catch(()=>{}).finally(()=>{ priceSnapshotInflight = false; });
}
function formatLiveHoldDurationFromMs(openedMs, nowMs){
if(openedMs == null || openedMs === "" || !Number.isFinite(Number(openedMs))) return "—";
+19 -4
View File
@@ -1654,6 +1654,17 @@ function accountSnapshotFundingMissing(data){
return !hasFunding && !hasTotal && !hasTrading;
}
let accountSnapshotRetryCount = 0;
let accountSnapshotInflight = false;
let priceSnapshotInflight = false;
function fetchJsonWithTimeout(url, timeoutMs) {
const ms = timeoutMs != null ? timeoutMs : 25000;
const ac = new AbortController();
const timer = setTimeout(function () { ac.abort(); }, ms);
return fetch(url, { signal: ac.signal, credentials: "same-origin" })
.then(function (r) { return r.json(); })
.finally(function () { clearTimeout(timer); });
}
function applyAccountSnapshot(data){
if(!data || typeof data !== "object") return;
if(typeof data.show_perp_funds !== "undefined"){
@@ -1753,8 +1764,10 @@ function applyAccountSnapshot(data){
}
function refreshAccountSnapshot(opts){
const options = opts || {};
if(accountSnapshotInflight && !options.force) return;
accountSnapshotInflight = true;
const qs = options.force ? "?force=1" : "";
fetch("/api/account_snapshot" + qs).then(r=>r.json()).then(data=>{
fetchJsonWithTimeout("/api/account_snapshot" + qs, 25000).then(data=>{
applyAccountSnapshot(data);
if(accountSnapshotFundingMissing(data) && !options.force && accountSnapshotRetryCount < 3){
accountSnapshotRetryCount += 1;
@@ -1767,7 +1780,7 @@ function refreshAccountSnapshot(opts){
accountSnapshotRetryCount += 1;
setTimeout(() => refreshAccountSnapshot({ silent: true }), 1200 * accountSnapshotRetryCount);
}
});
}).finally(()=>{ accountSnapshotInflight = false; });
}
{% if ui_open_guard_enabled %}
@@ -1925,8 +1938,10 @@ refreshOrderDefaults();
refreshPriceSnapshotConditional();
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});
function refreshPriceSnapshotConditional(){
if(priceSnapshotInflight) return;
priceSnapshotInflight = true;
const page = document.body.getAttribute("data-page") || "";
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
fetchJsonWithTimeout("/api/price_snapshot", 25000).then(data=>{
const updatedEl = document.getElementById("price-last-updated");
if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at;
if(data.force_close && window.TimeCloseUI && TimeCloseUI.paintForceCloseHeader){
@@ -2000,7 +2015,7 @@ function refreshPriceSnapshotConditional(){
} else if (typeof data.options_unrealized_pnl !== "undefined") {
paintRealtimePnlFromSnapshot(data);
}
}).catch(()=>{});
}).catch(()=>{}).finally(()=>{ priceSnapshotInflight = false; });
}
function formatLiveHoldDurationFromMs(openedMs, nowMs){
if(openedMs == null || openedMs === "" || !Number.isFinite(Number(openedMs))) return "—";
+50 -10
View File
@@ -134,6 +134,12 @@ def resolve_trade_record_margin_usdt(
"""写入 trade_records.基数:计划保证金优先于异常交易所快照;禁止币×价÷杠杆虚增."""
plan = _pos_float(plan_margin_capital)
ex = _pos_float(exchange_margin_usdt)
from_risk = margin_from_risk_amount(
risk_amount,
trigger_price=trigger_price,
stop_loss=stop_loss,
leverage=leverage,
)
if plan is not None and looks_like_coin_amount_as_margin(
plan,
@@ -143,6 +149,8 @@ def resolve_trade_record_margin_usdt(
notional_value=notional_value,
):
plan = None
if plan is not None and from_risk is not None and plan > from_risk * 2.5:
plan = None
if ex is not None and looks_like_coin_amount_as_margin(
ex,
@@ -155,18 +163,13 @@ def resolve_trade_record_margin_usdt(
ex = None
if ex is not None and plan is not None and ex > plan * 2.5:
ex = None
if ex is not None and from_risk is not None and ex > from_risk * 2.5:
ex = None
if plan is not None:
return round(plan, 2)
if ex is not None:
return round(ex, 2)
from_risk = margin_from_risk_amount(
risk_amount,
trigger_price=trigger_price,
stop_loss=stop_loss,
leverage=leverage,
)
if from_risk is not None:
return from_risk
@@ -187,10 +190,16 @@ def repair_stored_margin_capital(
risk_amount: Any = None,
initial_stop_loss: Any = None,
) -> Optional[float]:
"""展示/列表:修复已入库的异常基数;禁止把币数量换算成虚高保证金."""
"""展示/列表:修复已入库的异常基数.
返回值语义:
- 正数:应用该保证金
- None:若原值像币数量,调用方应清空显示;否则保留原值(见 apply_repaired_margin_capital)
"""
del symbol # 预留按币种阈值
m = _pos_float(margin_capital)
sl = stop_loss if stop_loss not in (None, "") else initial_stop_loss
# 与写入路径一致:优先开仓止损,避免保本后距离变小导致反推虚高
sl = initial_stop_loss if initial_stop_loss not in (None, "") else stop_loss
from_risk = margin_from_risk_amount(
risk_amount,
trigger_price=trigger_price,
@@ -207,7 +216,38 @@ def repair_stored_margin_capital(
return from_risk
if from_risk is not None and m > from_risk * 2.5:
# 已入库虚高(如曾用币×价÷杠杆「纠偏」成 384)
return from_risk
return round(m, 2)
def apply_repaired_margin_capital(
item: dict[str, Any],
*,
trigger_price: Any = None,
leverage: Any = None,
stop_loss: Any = None,
initial_stop_loss: Any = None,
risk_amount: Any = None,
symbol: Any = None,
) -> None:
"""就地更新 item['margin_capital'];币数量无法修复时清空为 None."""
if not isinstance(item, dict):
return
raw = item.get("margin_capital")
fixed = repair_stored_margin_capital(
raw,
trigger_price=trigger_price,
leverage=leverage,
symbol=symbol,
stop_loss=stop_loss,
initial_stop_loss=initial_stop_loss,
risk_amount=risk_amount,
)
if fixed is not None:
item["margin_capital"] = fixed
return
if looks_like_coin_amount_as_margin(
raw, trigger_price=trigger_price, leverage=leverage
):
item["margin_capital"] = None