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)
|
out["mark_price"] = round(mark, 8)
|
||||||
except Exception:
|
except Exception:
|
||||||
out["mark_price"] = round(mark, 8)
|
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
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3538,6 +3538,15 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
|||||||
out["unrealized_pnl"] = round(unrealized, 2)
|
out["unrealized_pnl"] = round(unrealized, 2)
|
||||||
if mark is not None and mark > 0:
|
if mark is not None and mark > 0:
|
||||||
out["mark_price"] = round(mark, 8)
|
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
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3611,6 +3611,15 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
|||||||
out["unrealized_pnl"] = round(unrealized, 6)
|
out["unrealized_pnl"] = round(unrealized, 6)
|
||||||
if mark is not None and mark > 0:
|
if mark is not None and mark > 0:
|
||||||
out["mark_price"] = round(mark, 8)
|
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
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2781,6 +2781,17 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
|||||||
out["unrealized_pnl"] = round(unrealized, FUNDS_DECIMALS)
|
out["unrealized_pnl"] = round(unrealized, FUNDS_DECIMALS)
|
||||||
if mark is not None and mark > 0:
|
if mark is not None and mark > 0:
|
||||||
out["mark_price"] = round(mark, 8)
|
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
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+53
-7
@@ -119,14 +119,25 @@ def resolve_position_display_upnl(
|
|||||||
return exchange_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:
|
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):
|
if not isinstance(p, dict):
|
||||||
return None
|
return None
|
||||||
info = p.get("info") or {}
|
info = p.get("info") or {}
|
||||||
if not isinstance(info, dict):
|
if not isinstance(info, dict):
|
||||||
info = {}
|
info = {}
|
||||||
for key in (
|
return _coerce_signed(
|
||||||
p.get("unrealizedPnl"),
|
p.get("unrealizedPnl"),
|
||||||
p.get("unrealisedPnl"),
|
p.get("unrealisedPnl"),
|
||||||
p.get("unrealized_pnl"),
|
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("unrealized_pnl"),
|
||||||
info.get("unrealisedPnl"),
|
info.get("unrealisedPnl"),
|
||||||
info.get("unrealizedPnl"),
|
info.get("unrealizedPnl"),
|
||||||
):
|
info.get("upl"),
|
||||||
px = _finite_or_none(key)
|
info.get("uplLast"),
|
||||||
if px is not None:
|
)
|
||||||
return px
|
|
||||||
return None
|
|
||||||
|
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:
|
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
|
and d.get("avg_entry_price") is not None
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
from hub_position_metrics import estimate_linear_swap_upnl_usdt
|
||||||
|
|
||||||
entry = float(d["avg_entry_price"])
|
entry = float(d["avg_entry_price"])
|
||||||
mark = float(met["mark_price"])
|
mark = float(met["mark_price"])
|
||||||
margin = float(d.get("plan_margin_capital") or 0)
|
qty = None
|
||||||
leverage = int(d.get("leverage") or 1)
|
cs = 1.0
|
||||||
calc_pnl = getattr(m, "calc_pnl", None)
|
get_qty = getattr(m, "get_live_position_contracts", None)
|
||||||
if callable(calc_pnl) and entry > 0 and margin > 0:
|
get_cs = getattr(m, "get_contract_size", None)
|
||||||
d["floating_pnl"] = float(
|
if callable(get_qty):
|
||||||
calc_pnl(direction, entry, mark, margin, leverage)
|
qty = get_qty(ex_sym, direction)
|
||||||
)
|
if callable(get_cs):
|
||||||
else:
|
cs = float(get_cs(ex_sym))
|
||||||
d["floating_pnl"] = None
|
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):
|
except (TypeError, ValueError):
|
||||||
d["floating_pnl"] = None
|
d["floating_pnl"] = None
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from agent import _position_mark_price, _ticker_mark_price # noqa: E402
|
|||||||
|
|
||||||
sys.path.insert(0, str(ROOT))
|
sys.path.insert(0, str(ROOT))
|
||||||
from hub_position_metrics import ( # noqa: E402
|
from hub_position_metrics import ( # noqa: E402
|
||||||
|
enrich_ccxt_position_metrics_out,
|
||||||
estimate_linear_swap_upnl_usdt,
|
estimate_linear_swap_upnl_usdt,
|
||||||
parse_position_unrealized_pnl,
|
parse_position_unrealized_pnl,
|
||||||
resolve_position_display_upnl,
|
resolve_position_display_upnl,
|
||||||
@@ -52,6 +53,24 @@ class TestHubAgentMarkPrice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertAlmostEqual(pnl, 6.81)
|
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):
|
def test_estimate_short_hype_contract_size(self):
|
||||||
upnl = estimate_linear_swap_upnl_usdt(
|
upnl = estimate_linear_swap_upnl_usdt(
|
||||||
"short", 73.187, 66.038, 11, 0.1
|
"short", 73.187, 66.038, 11, 0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user