修复 Gate 全仓持仓保证金误读为浮盈亏:全仓用 value/杠杆+平仓费估算,API margin 若等于 unrealised_pnl 则弃用
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+75
-22
@@ -3761,46 +3761,99 @@ def _coerce_float(*values):
|
|||||||
return None
|
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):
|
def parse_ccxt_position_metrics(position, order_leverage=None):
|
||||||
"""
|
"""
|
||||||
从 ccxt 统一持仓结构解析保证金/名义/未实现盈亏(Gate 等所字段略有差异,做多键兜底).
|
从 ccxt 统一持仓结构解析保证金/名义/未实现盈亏(Gate 等所字段略有差异,做多键兜底).
|
||||||
与 App「仓位保证金」对齐时优先用 initialMargin;缺失时再尝试 info 内字段.
|
全仓优先 value/cross_leverage_limit(+平仓费);API margin 若≈unrealised_pnl 则弃用.
|
||||||
"""
|
"""
|
||||||
if not position:
|
if not position:
|
||||||
return None
|
return None
|
||||||
p = position
|
p = position
|
||||||
info = p.get("info", {}) or {}
|
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"))
|
notional = _coerce_float(p.get("notional"), p.get("notionalValue"))
|
||||||
if notional is None or notional <= 0:
|
if notional is None or notional <= 0:
|
||||||
notional = _coerce_float(info.get("value"))
|
notional = _coerce_float(info.get("value"))
|
||||||
if notional is not None:
|
if notional is not None:
|
||||||
notional = abs(notional)
|
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(
|
unrealized = _coerce_float(
|
||||||
p.get("unrealizedPnl"),
|
p.get("unrealizedPnl"),
|
||||||
info.get("unrealised_pnl"),
|
info.get("unrealised_pnl"),
|
||||||
info.get("unrealized_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"))
|
mark = _coerce_float(p.get("markPrice"), p.get("mark_price"), info.get("mark_price"), info.get("markPrice"))
|
||||||
out = {}
|
out = {}
|
||||||
if initial is not None and initial > 0:
|
if initial is not None and initial > 0:
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user