diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 07bfadf..e97ccb4 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -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 diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 30cf68f..bd8a2e9 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -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 diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 2f458d4..7e2795d 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -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 diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 253f2aa..0b70e23 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -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 diff --git a/hub_position_metrics.py b/hub_position_metrics.py index 9d9837d..3fcc4b9 100644 --- a/hub_position_metrics.py +++ b/hub_position_metrics.py @@ -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: diff --git a/strategy_trend_register.py b/strategy_trend_register.py index c387a3f..92d9043 100644 --- a/strategy_trend_register.py +++ b/strategy_trend_register.py @@ -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: diff --git a/tests/test_hub_agent_mark_price.py b/tests/test_hub_agent_mark_price.py index 6a24b04..e80d922 100644 --- a/tests/test_hub_agent_mark_price.py +++ b/tests/test_hub_agent_mark_price.py @@ -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