修复 Gate 全仓持仓保证金误读为浮盈亏:全仓用 value/杠杆+平仓费估算,API margin 若等于 unrealised_pnl 则弃用

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-24 15:50:34 +08:00
parent 5e2f332bdd
commit 5f9901db0f
2 changed files with 157 additions and 22 deletions
+75 -22
View File
@@ -3761,46 +3761,99 @@ def _coerce_float(*values):
return None
def _gate_is_cross_margin(position, info):
mode = str(position.get("marginMode") or info.get("pos_margin_mode") or "").lower()
if "cross" in mode:
return True
lev = _coerce_float(info.get("leverage"), position.get("leverage"))
return lev is not None and lev == 0
def _gate_effective_leverage(position, info, order_leverage=None):
lev = _coerce_float(position.get("leverage"), info.get("leverage"))
if lev is not None and lev > 0:
return lev
cross_lev = _coerce_float(info.get("cross_leverage_limit"))
if cross_lev is not None and cross_lev > 0:
return cross_lev
if order_leverage is not None:
try:
ol = float(order_leverage)
if ol > 0:
return ol
except (TypeError, ValueError):
pass
return None
def _gate_estimated_initial_margin(notional, leverage):
"""Gate App 口径:仓位价值/杠杆 + 预估平仓 taker 费(0.075%)."""
if notional is None or notional <= 0 or leverage is None or leverage <= 0:
return None
return notional / float(leverage) + notional * 0.00075
def _gate_margin_matches_unrealized(margin, unrealized):
if margin is None or unrealized is None:
return False
return abs(float(margin) - float(unrealized)) <= max(0.02, abs(float(unrealized)) * 0.05)
def _gate_resolve_initial_margin(position, info, *, notional, unrealized, order_leverage=None):
"""全仓下 API margin 偶发等于 unrealised_pnl;优先 value/杠杆,逐仓仍信 API."""
api_margin = _coerce_float(
info.get("initial_margin"),
position.get("initialMargin"),
position.get("collateral"),
position.get("margin"),
info.get("margin"),
info.get("iso_margin"),
info.get("position_margin"),
info.get("initialMargin"),
)
eff_lev = _gate_effective_leverage(position, info, order_leverage)
estimated = _gate_estimated_initial_margin(notional, eff_lev) if eff_lev else None
if _gate_is_cross_margin(position, info):
if estimated and estimated > 0:
if (
api_margin is None
or api_margin <= 0
or _gate_margin_matches_unrealized(api_margin, unrealized)
or api_margin < estimated * 0.6
):
return estimated
if api_margin is not None and api_margin > 0 and not _gate_margin_matches_unrealized(
api_margin, unrealized
):
return api_margin
return estimated
if api_margin is not None and api_margin > 0:
return api_margin
return estimated
def parse_ccxt_position_metrics(position, order_leverage=None):
"""
ccxt 统一持仓结构解析保证金/名义/未实现盈亏(Gate 等所字段略有差异,做多键兜底).
App仓位保证金对齐时优先用 initialMargin;缺失时再尝试 info 内字段.
全仓优先 value/cross_leverage_limit(+平仓费);API margin unrealised_pnl 则弃用.
"""
if not position:
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"))
if initial is None or initial <= 0:
initial = _coerce_float(
info.get("margin"),
info.get("cross_margin"),
info.get("iso_margin"),
info.get("initial_margin"),
info.get("position_margin"),
info.get("initialMargin"),
)
notional = _coerce_float(p.get("notional"), p.get("notionalValue"))
if notional is None or notional <= 0:
notional = _coerce_float(info.get("value"))
if notional is not None:
notional = abs(notional)
# 全仓且 API margin 为 0 时:用名义/杠杆粗算展示(与交易所「约占用」接近)
if (initial is None or initial <= 0) and notional and notional > 0 and order_leverage:
try:
lev = float(order_leverage)
if lev > 0:
approx = notional / lev
if approx > 0:
initial = approx
except (TypeError, ValueError):
pass
unrealized = _coerce_float(
p.get("unrealizedPnl"),
info.get("unrealised_pnl"),
info.get("unrealized_pnl"),
)
initial = _gate_resolve_initial_margin(
p, info, notional=notional, unrealized=unrealized, order_leverage=order_leverage
)
mark = _coerce_float(p.get("markPrice"), p.get("mark_price"), info.get("mark_price"), info.get("markPrice"))
out = {}
if initial is not None and initial > 0:
+82
View File
@@ -0,0 +1,82 @@
"""Gate 持仓指标:全仓保证金不得误用 unrealised_pnl."""
from __future__ import annotations
import unittest
class TestGatePositionMetrics(unittest.TestCase):
def test_cross_margin_not_equal_unrealised_pnl(self):
from crypto_monitor_gate.app import parse_ccxt_position_metrics
pos = {
"side": "long",
"contracts": 400,
"collateral": 21.19,
"initialMargin": None,
"notional": 3098.54,
"unrealizedPnl": 21.19,
"markPrice": 77463.5,
"leverage": 0,
"marginMode": "cross",
"symbol": "BTC/USDT:USDT",
"info": {
"value": "3098.54",
"leverage": "0",
"cross_leverage_limit": "20",
"margin": "21.19",
"unrealised_pnl": "21.19",
"mark_price": "77463.5",
},
}
out = parse_ccxt_position_metrics(pos, order_leverage=20)
self.assertIsNotNone(out)
self.assertAlmostEqual(out["unrealized_pnl"], 21.19)
self.assertGreater(out["initial_margin"], 150)
self.assertLess(out["initial_margin"], 160)
pct = out["unrealized_pnl"] / out["initial_margin"] * 100
self.assertGreater(pct, 12)
self.assertLess(pct, 16)
def test_cross_margin_trusts_api_when_sane(self):
from crypto_monitor_gate.app import parse_ccxt_position_metrics
pos = {
"side": "long",
"contracts": 1,
"collateral": 157.03,
"notional": 3098.54,
"unrealizedPnl": 21.19,
"leverage": 0,
"marginMode": "cross",
"info": {
"value": "3098.54",
"leverage": "0",
"cross_leverage_limit": "20",
"margin": "157.03",
"unrealised_pnl": "21.19",
},
}
out = parse_ccxt_position_metrics(pos, order_leverage=20)
self.assertIsNotNone(out)
self.assertAlmostEqual(out["initial_margin"], 157.03)
def test_isolated_uses_api_margin(self):
from crypto_monitor_gate.app import parse_ccxt_position_metrics
pos = {
"side": "long",
"contracts": 10,
"collateral": 88.5,
"notional": 885.0,
"unrealizedPnl": 3.2,
"leverage": 10,
"marginMode": "isolated",
"info": {"value": "885", "leverage": "10", "margin": "88.5", "unrealised_pnl": "3.2"},
}
out = parse_ccxt_position_metrics(pos, order_leverage=10)
self.assertIsNotNone(out)
self.assertAlmostEqual(out["initial_margin"], 88.5)
if __name__ == "__main__":
unittest.main()