修复审计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
+72 -15
View File
@@ -2922,6 +2922,39 @@ def get_active_position_count(conn):
return int(conn.execute("SELECT COUNT(*) FROM order_monitors WHERE status='active'").fetchone()[0])
def count_nonzero_live_exchange_positions():
"""交易所非零持仓条数;API 不可用时返回 None(不阻断,仅 DB 判定)."""
if not exchange_private_api_configured():
return None
try:
ensure_markets_loaded()
rows = exchange.fetch_positions(None, {"settle": "usdt"}) or []
except Exception:
try:
rows = exchange.fetch_positions() or []
except Exception:
return None
n = 0
for p in rows or []:
try:
if _position_row_effective_contracts(p) > 0:
n += 1
except Exception:
continue
return n
def full_margin_flat_check(conn):
"""全仓复利开仓前:本地监控仓 + 交易所实盘仓均须为空."""
ok, msg = full_margin_requires_flat_position(get_active_position_count(conn))
if not ok:
return ok, msg
ex_n = count_nonzero_live_exchange_positions()
if ex_n is not None and ex_n > 0:
return False, "交易所仍有持仓,全仓杠杆模式请先平仓后再开"
return True, ""
def clear_key_sizing_snapshot_if_flat(conn, session_date):
if get_active_position_count(conn) > 0:
return
@@ -3770,17 +3803,19 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
return None
p = position
info = p.get("info", {}) or {}
# Gate 全仓:ccxt 的 initialMargin 常为空;collateral 来自 API 的 margin,与 App「保证金」一致
initial = _coerce_float(p.get("collateral"), p.get("initialMargin"), p.get("margin"))
# 与 Binance 对齐:优先 initialMargin;collateral 常含未实现盈亏,仅作末位兜底
initial = _coerce_float(p.get("initialMargin"), p.get("margin"))
if initial is None or initial <= 0:
initial = _coerce_float(
info.get("margin"),
info.get("initial_margin"),
info.get("cross_margin"),
info.get("iso_margin"),
info.get("initial_margin"),
info.get("position_margin"),
info.get("initialMargin"),
info.get("margin"),
)
if initial is None or initial <= 0:
initial = _coerce_float(p.get("collateral"))
notional = _coerce_float(p.get("notional"), p.get("notionalValue"))
if notional is None or notional <= 0:
notional = _coerce_float(info.get("value"))
@@ -5354,7 +5389,7 @@ def _add_trigger_entry_key_monitor(
if not ok_intent:
return False, intent_msg
if is_full_margin_mode(POSITION_SIZING_MODE):
ok_flat, flat_msg = full_margin_requires_flat_position(get_active_position_count(conn))
ok_flat, flat_msg = full_margin_flat_check(conn)
if not ok_flat:
return False, flat_msg
if count_pending_trigger_entries(conn, trading_day) > 0:
@@ -5503,7 +5538,7 @@ def _market_open_for_trigger_entry(
risk_percent = max(0.01, float(RISK_PERCENT))
if is_full_margin_mode(POSITION_SIZING_MODE):
ok_flat, flat_msg = full_margin_requires_flat_position(get_active_position_count(conn))
ok_flat, flat_msg = full_margin_flat_check(conn)
if not ok_flat:
return False, flat_msg, None
leverage = leverage_for_full_margin(symbol, BTC_LEVERAGE, ALT_LEVERAGE)
@@ -8662,7 +8697,7 @@ def add_order():
risk_percent = max(0.01, float(RISK_PERCENT))
risk_amount = round(capital_base * risk_percent / 100.0, 2)
if is_full_margin_mode(POSITION_SIZING_MODE):
ok_flat, flat_msg = full_margin_requires_flat_position(get_active_position_count(conn))
ok_flat, flat_msg = full_margin_flat_check(conn)
if not ok_flat:
conn.close()
flash(flat_msg)
@@ -9152,21 +9187,43 @@ 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)
opened_at_ms = _to_ms_with_fallback(
row["opened_at_ms"] if "opened_at_ms" in row.keys() else None, opened_at
)
margin_capital = row["margin_capital"] or DAILY_START_CAPITAL
leverage = row["leverage"] or infer_leverage(row["symbol"])
close_resp = close_exchange_order(row)
close_order_id = close_resp.get("id", "")
cancel_gate_swap_trigger_orders(row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"]))
exit_p = extract_trade_price_from_order(close_resp)
closed_at = app_now_str()
hold_seconds = calc_hold_seconds(opened_at, app_now())
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))
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)
pnl_amount = calc_pnl(
row["direction"],
row["trigger_price"],
p,
row["margin_capital"] or DAILY_START_CAPITAL,
row["leverage"] or infer_leverage(row["symbol"])
margin_capital,
leverage,
)
close_resp = close_exchange_order(row)
close_order_id = close_resp.get("id", "")
cancel_gate_swap_trigger_orders(row["exchange_symbol"] or normalize_exchange_symbol(row["symbol"]))
session_date = row["session_date"] or get_trading_day()
session_date = row["session_date"] or get_trading_day(closed_at_dt)
session_capital = update_session_capital(conn, session_date, pnl_amount)
row_snap = conn.execute("SELECT * FROM order_monitors WHERE id=?", (id,)).fetchone() or row
insert_trade_record(