fix: align unrealized PnL across four exchange instances via hub_position_metrics

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 20:25:06 +08:00
parent 806350231e
commit 673bcbdc70
7 changed files with 126 additions and 16 deletions
+11
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+11
View File
@@ -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
View File
@@ -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:
+14 -9
View File
@@ -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:
+19
View File
@@ -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