diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 6c611ed..246acd4 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -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: diff --git a/tests/test_gate_position_metrics.py b/tests/test_gate_position_metrics.py new file mode 100644 index 0000000..0b22a8d --- /dev/null +++ b/tests/test_gate_position_metrics.py @@ -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()