fix: align unrealized PnL across four exchange instances via hub_position_metrics
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3764,6 +3764,17 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
||||
out["mark_price"] = round(mark, 8)
|
||||
except Exception:
|
||||
out["mark_price"] = round(mark, 8)
|
||||
if out:
|
||||
sym = (p.get("symbol") or "").strip()
|
||||
try:
|
||||
cs = float(get_contract_size(sym)) if sym else 1.0
|
||||
except Exception:
|
||||
cs = 1.0
|
||||
from hub_position_metrics import enrich_ccxt_position_metrics_out
|
||||
|
||||
enrich_ccxt_position_metrics_out(
|
||||
p, out, contract_size=cs, funds_decimals=FUNDS_DECIMALS
|
||||
)
|
||||
return out or None
|
||||
|
||||
|
||||
|
||||
@@ -3538,6 +3538,15 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
||||
out["unrealized_pnl"] = round(unrealized, 2)
|
||||
if mark is not None and mark > 0:
|
||||
out["mark_price"] = round(mark, 8)
|
||||
if out:
|
||||
sym = (p.get("symbol") or "").strip()
|
||||
try:
|
||||
cs = float(get_contract_size(sym)) if sym else 1.0
|
||||
except Exception:
|
||||
cs = 1.0
|
||||
from hub_position_metrics import enrich_ccxt_position_metrics_out
|
||||
|
||||
enrich_ccxt_position_metrics_out(p, out, contract_size=cs, funds_decimals=2)
|
||||
return out or None
|
||||
|
||||
|
||||
|
||||
@@ -3611,6 +3611,15 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
||||
out["unrealized_pnl"] = round(unrealized, 6)
|
||||
if mark is not None and mark > 0:
|
||||
out["mark_price"] = round(mark, 8)
|
||||
if out:
|
||||
sym = (p.get("symbol") or "").strip()
|
||||
try:
|
||||
cs = float(get_contract_size(sym)) if sym else 1.0
|
||||
except Exception:
|
||||
cs = 1.0
|
||||
from hub_position_metrics import enrich_ccxt_position_metrics_out
|
||||
|
||||
enrich_ccxt_position_metrics_out(p, out, contract_size=cs, funds_decimals=2)
|
||||
return out or None
|
||||
|
||||
|
||||
|
||||
@@ -2781,6 +2781,17 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
||||
out["unrealized_pnl"] = round(unrealized, FUNDS_DECIMALS)
|
||||
if mark is not None and mark > 0:
|
||||
out["mark_price"] = round(mark, 8)
|
||||
if out:
|
||||
sym = (p.get("symbol") or "").strip()
|
||||
try:
|
||||
cs = float(get_contract_size(sym)) if sym else 1.0
|
||||
except Exception:
|
||||
cs = 1.0
|
||||
from hub_position_metrics import enrich_ccxt_position_metrics_out
|
||||
|
||||
enrich_ccxt_position_metrics_out(
|
||||
p, out, contract_size=cs, funds_decimals=FUNDS_DECIMALS
|
||||
)
|
||||
return out or None
|
||||
|
||||
|
||||
|
||||
+53
-7
@@ -119,14 +119,25 @@ def resolve_position_display_upnl(
|
||||
return exchange_upnl
|
||||
|
||||
|
||||
def _coerce_signed(*values: Any) -> float | None:
|
||||
"""解析可正可负的数值(未实现盈亏等)。"""
|
||||
for v in values:
|
||||
if v is None or v == "":
|
||||
continue
|
||||
f = _finite_or_none(v)
|
||||
if f is not None:
|
||||
return f
|
||||
return None
|
||||
|
||||
|
||||
def parse_position_unrealized_pnl(p: dict[str, Any]) -> float | None:
|
||||
"""四所 ccxt 持仓统一解析未实现盈亏(Gate 常在 info.unrealised_pnl)。"""
|
||||
"""四所 ccxt 持仓统一解析未实现盈亏(Gate/OKX/Binance 字段名不一致)。"""
|
||||
if not isinstance(p, dict):
|
||||
return None
|
||||
info = p.get("info") or {}
|
||||
if not isinstance(info, dict):
|
||||
info = {}
|
||||
for key in (
|
||||
return _coerce_signed(
|
||||
p.get("unrealizedPnl"),
|
||||
p.get("unrealisedPnl"),
|
||||
p.get("unrealized_pnl"),
|
||||
@@ -135,11 +146,46 @@ def parse_position_unrealized_pnl(p: dict[str, Any]) -> float | None:
|
||||
info.get("unrealized_pnl"),
|
||||
info.get("unrealisedPnl"),
|
||||
info.get("unrealizedPnl"),
|
||||
):
|
||||
px = _finite_or_none(key)
|
||||
if px is not None:
|
||||
return px
|
||||
return None
|
||||
info.get("upl"),
|
||||
info.get("uplLast"),
|
||||
)
|
||||
|
||||
|
||||
def enrich_ccxt_position_metrics_out(
|
||||
position: dict[str, Any],
|
||||
out: dict[str, Any],
|
||||
*,
|
||||
contract_size: float = 1.0,
|
||||
funds_decimals: int = 2,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
四所 parse_ccxt_position_metrics 产出后统一:
|
||||
- 标记价用 hub 兜底
|
||||
- 未实现盈亏 = resolve(交易所值, entry/mark/张数/contractSize 推算)
|
||||
"""
|
||||
if not isinstance(position, dict) or not isinstance(out, dict):
|
||||
return out
|
||||
mark = _finite_or_none(out.get("mark_price"))
|
||||
if mark is None or mark <= 0:
|
||||
mp = parse_position_mark_price(position)
|
||||
if mp is not None and mp > 0:
|
||||
out["mark_price"] = round(mp, 8)
|
||||
mark = mp
|
||||
exchange_upnl = parse_position_unrealized_pnl(position)
|
||||
if exchange_upnl is None:
|
||||
exchange_upnl = _coerce_signed(out.get("unrealized_pnl"))
|
||||
c = position_contracts(position)
|
||||
if abs(c) < 1e-12:
|
||||
return out
|
||||
side = position_side_from_ccxt(position, c)
|
||||
entry = parse_position_entry_price(position)
|
||||
cs = contract_size if contract_size and contract_size > 0 else 1.0
|
||||
upnl = resolve_position_display_upnl(
|
||||
side, entry, mark, abs(c), cs, exchange_upnl
|
||||
)
|
||||
if upnl is not None:
|
||||
out["unrealized_pnl"] = round(upnl, funds_decimals)
|
||||
return out
|
||||
|
||||
|
||||
def parse_position_mark_price(p: dict[str, Any]) -> float | None:
|
||||
|
||||
@@ -460,17 +460,22 @@ def enrich_trend_plan(cfg: dict, row) -> dict:
|
||||
and d.get("avg_entry_price") is not None
|
||||
):
|
||||
try:
|
||||
from hub_position_metrics import estimate_linear_swap_upnl_usdt
|
||||
|
||||
entry = float(d["avg_entry_price"])
|
||||
mark = float(met["mark_price"])
|
||||
margin = float(d.get("plan_margin_capital") or 0)
|
||||
leverage = int(d.get("leverage") or 1)
|
||||
calc_pnl = getattr(m, "calc_pnl", None)
|
||||
if callable(calc_pnl) and entry > 0 and margin > 0:
|
||||
d["floating_pnl"] = float(
|
||||
calc_pnl(direction, entry, mark, margin, leverage)
|
||||
)
|
||||
else:
|
||||
d["floating_pnl"] = None
|
||||
qty = None
|
||||
cs = 1.0
|
||||
get_qty = getattr(m, "get_live_position_contracts", None)
|
||||
get_cs = getattr(m, "get_contract_size", None)
|
||||
if callable(get_qty):
|
||||
qty = get_qty(ex_sym, direction)
|
||||
if callable(get_cs):
|
||||
cs = float(get_cs(ex_sym))
|
||||
upnl = estimate_linear_swap_upnl_usdt(
|
||||
direction, entry, mark, qty, cs
|
||||
)
|
||||
d["floating_pnl"] = float(upnl) if upnl is not None else None
|
||||
except (TypeError, ValueError):
|
||||
d["floating_pnl"] = None
|
||||
else:
|
||||
|
||||
@@ -12,6 +12,7 @@ from agent import _position_mark_price, _ticker_mark_price # noqa: E402
|
||||
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from hub_position_metrics import ( # noqa: E402
|
||||
enrich_ccxt_position_metrics_out,
|
||||
estimate_linear_swap_upnl_usdt,
|
||||
parse_position_unrealized_pnl,
|
||||
resolve_position_display_upnl,
|
||||
@@ -52,6 +53,24 @@ class TestHubAgentMarkPrice(unittest.TestCase):
|
||||
)
|
||||
self.assertAlmostEqual(pnl, 6.81)
|
||||
|
||||
def test_okx_upl_signed(self):
|
||||
pnl = parse_position_unrealized_pnl(
|
||||
{"info": {"upl": "-2.15"}, "unrealizedPnl": None}
|
||||
)
|
||||
self.assertAlmostEqual(pnl, -2.15)
|
||||
|
||||
def test_enrich_aligns_short_gate_metrics(self):
|
||||
pos = {
|
||||
"side": "short",
|
||||
"contracts": 11,
|
||||
"entryPrice": 73.187,
|
||||
"markPrice": 66.038,
|
||||
"info": {"unrealised_pnl": "7.86"},
|
||||
}
|
||||
out = {"unrealized_pnl": 7.86, "mark_price": 66.038}
|
||||
enrich_ccxt_position_metrics_out(pos, out, contract_size=1.0, funds_decimals=2)
|
||||
self.assertGreater(out["unrealized_pnl"], 70.0)
|
||||
|
||||
def test_estimate_short_hype_contract_size(self):
|
||||
upnl = estimate_linear_swap_upnl_usdt(
|
||||
"short", 73.187, 66.038, 11, 0.1
|
||||
|
||||
Reference in New Issue
Block a user