diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 961f582..46941bf 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -9604,7 +9604,9 @@ register_trade_records_api( def _dashboard_enrich_orders(items): from lib.instance.instance_dashboard_lib import enrich_order_items_with_marks - return enrich_order_items_with_marks(items, get_price=get_price) + return enrich_order_items_with_marks( + items, get_price=get_price, get_contract_size=get_contract_size + ) from lib.instance.instance_dashboard_register import register_instance_dashboard_routes diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 6f18812..5a7863b 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -9446,7 +9446,9 @@ register_trade_records_api( def _dashboard_enrich_orders(items): from lib.instance.instance_dashboard_lib import enrich_order_items_with_marks - return enrich_order_items_with_marks(items, get_price=get_price) + return enrich_order_items_with_marks( + items, get_price=get_price, get_contract_size=get_contract_size + ) from lib.instance.instance_dashboard_register import register_instance_dashboard_routes diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 5270451..8417f5b 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -9181,7 +9181,9 @@ def _dashboard_fetch_options_positions(): def _dashboard_enrich_orders(items): from lib.instance.instance_dashboard_lib import enrich_order_items_with_marks - return enrich_order_items_with_marks(items, get_price=get_price) + return enrich_order_items_with_marks( + items, get_price=get_price, get_contract_size=get_contract_size + ) from lib.hedge_plan.okx_trade_mode_lib import hedge_module_enabled diff --git a/lib/instance/instance_dashboard_lib.py b/lib/instance/instance_dashboard_lib.py index 390e442..7c08d35 100644 --- a/lib/instance/instance_dashboard_lib.py +++ b/lib/instance/instance_dashboard_lib.py @@ -358,34 +358,81 @@ def collect_options_items( return out +def _resolve_contract_size( + sym: str, + *, + get_contract_size: Optional[Callable[[str], Any]] = None, +) -> float: + if not callable(get_contract_size) or not sym: + return 1.0 + try: + cs = float(get_contract_size(sym) or 1.0) + return cs if cs > 0 else 1.0 + except Exception: + return 1.0 + + +def _fill_order_pnl_fields(row: dict[str, Any], *, mark: Optional[float], contract_size: float) -> None: + """按线性 U 本位补看板「盈利金额 / 浮盈」.""" + direction = str(row.get("direction") or "long").lower() + entry = _safe_float(row.get("entry")) + contracts = _safe_float(row.get("contracts")) + tp = _safe_float(row.get("take_profit")) + if entry is None or contracts is None or contracts <= 0: + return + cs = float(contract_size) if contract_size and contract_size > 0 else 1.0 + if mark is not None: + try: + from lib.hub.hub_position_metrics import estimate_linear_swap_upnl_usdt + + upnl = estimate_linear_swap_upnl_usdt(direction, entry, mark, contracts, cs) + if upnl is not None: + row["float_pnl"] = upnl + except Exception: + pass + if tp is not None and tp > 0: + try: + from lib.strategy.strategy_trend_lib import calc_tp_profit_usdt + + profit = calc_tp_profit_usdt(direction, entry, tp, contracts, cs) + if profit is not None: + row["tp_profit"] = round(float(profit), 2) + except Exception: + pass + + def enrich_order_items_with_marks( items: list[dict[str, Any]], *, get_price: Optional[Callable[[str], Any]] = None, + get_contract_size: Optional[Callable[[str], Any]] = None, ) -> list[dict[str, Any]]: - """后台聚合时补标记价(不打全量 fetch_positions;浮盈仍由实盘页口径负责).""" - if not items or not callable(get_price): + """后台聚合时补标记价,并按张数×合约面值估算盈利金额/浮盈.""" + if not items: + return items + if not callable(get_price) and not callable(get_contract_size): return items out: list[dict[str, Any]] = [] for it in items: row = dict(it) sym = str(row.get("price_symbol") or row.get("symbol") or "").strip() - if not sym: - out.append(row) - continue - try: - px = get_price(sym) - except Exception: - px = None - mark = _safe_float(px) - if mark is None and ":" in sym: + mark = _safe_float(row.get("mark_price")) + if callable(get_price) and sym: try: - px = get_price(sym.split(":", 1)[0]) + px = get_price(sym) except Exception: px = None mark = _safe_float(px) - if mark is not None: - row["mark_price"] = mark + if mark is None and ":" in sym: + try: + px = get_price(sym.split(":", 1)[0]) + except Exception: + px = None + mark = _safe_float(px) + if mark is not None: + row["mark_price"] = mark + cs = _resolve_contract_size(sym, get_contract_size=get_contract_size) + _fill_order_pnl_fields(row, mark=mark, contract_size=cs) out.append(row) return out diff --git a/tests/test_instance_dashboard_lib.py b/tests/test_instance_dashboard_lib.py index 15cdb1a..1335833 100644 --- a/tests/test_instance_dashboard_lib.py +++ b/tests/test_instance_dashboard_lib.py @@ -167,6 +167,40 @@ class TestInstanceDashboardLib(unittest.TestCase): self.assertIn("对冲#2", opt["target_monitor"]) conn.close() + def test_enrich_order_items_fills_float_pnl_and_tp_profit(self): + from lib.instance.instance_dashboard_lib import enrich_order_items_with_marks + + items = [ + { + "id": 1, + "symbol": "BTC/USDT:USDT", + "price_symbol": "BTC/USDT:USDT", + "direction": "long", + "entry": 64693.6, + "contracts": 132, + "take_profit": 66000.0, + "mark_price": None, + "tp_profit": None, + "float_pnl": None, + } + ] + + def get_price(sym): + return 64809.5 + + def get_cs(sym): + return 0.0001 + + out = enrich_order_items_with_marks( + items, get_price=get_price, get_contract_size=get_cs + ) + self.assertEqual(len(out), 1) + self.assertEqual(out[0]["mark_price"], 64809.5) + # (64809.5 - 64693.6) * 132 * 0.0001 ≈ 1.53 + self.assertAlmostEqual(out[0]["float_pnl"], 1.53, places=2) + self.assertIsNotNone(out[0]["tp_profit"]) + self.assertGreater(out[0]["tp_profit"], 0) + if __name__ == "__main__": unittest.main()