diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 46941bf..79133a6 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -3416,7 +3416,7 @@ def resolve_order_entry_price(order_resp, exchange_symbol, fallback_price): def get_contract_size(exchange_symbol): ensure_markets_loaded() - market = exchange.market(exchange_symbol) + market = exchange.market(normalize_exchange_symbol(exchange_symbol)) return float(market.get("contractSize") or 1) diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 5a7863b..fba3dd0 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -3080,7 +3080,7 @@ def resolve_order_entry_price(order_resp, exchange_symbol, fallback_price): def get_contract_size(exchange_symbol): ensure_markets_loaded() - market = exchange.market(exchange_symbol) + market = exchange.market(normalize_exchange_symbol(exchange_symbol)) return float(market.get("contractSize") or 1) diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 8417f5b..e367a31 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -2839,7 +2839,7 @@ def resolve_order_entry_price(order_resp, exchange_symbol, fallback_price): def get_contract_size(exchange_symbol): try: ensure_markets_loaded() - market = exchange.market(exchange_symbol) + market = exchange.market(normalize_okx_symbol(exchange_symbol)) return float(market.get("contractSize") or 1) except Exception: return 1.0 diff --git a/lib/instance/instance_dashboard_lib.py b/lib/instance/instance_dashboard_lib.py index 7c08d35..7d7ff1d 100644 --- a/lib/instance/instance_dashboard_lib.py +++ b/lib/instance/instance_dashboard_lib.py @@ -358,18 +358,52 @@ def collect_options_items( return out +def _swap_symbol_candidates(row: dict[str, Any]) -> list[str]: + """优先永续 symbol(含 settle),避免用现货 BTC/USDT 查到 contractSize=1.""" + raw: list[str] = [] + for key in ("symbol", "exchange_symbol", "price_symbol"): + s = str(row.get(key) or "").strip() + if s and s not in raw: + raw.append(s) + swapish: list[str] = [] + others: list[str] = [] + for s in raw: + if ":" in s: + swapish.append(s) + continue + others.append(s) + if "/" in s: + base, quote = s.split("/", 1) + q = quote.split(":")[0].strip() + if base and q: + swapish.append(f"{base}/{q}:{q}") + out: list[str] = [] + for s in swapish + others: + if s and s not in out: + out.append(s) + return out + + def _resolve_contract_size( - sym: str, + row_or_sym: Any, *, 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: + if not callable(get_contract_size): return 1.0 + if isinstance(row_or_sym, dict): + candidates = _swap_symbol_candidates(row_or_sym) + else: + sym = str(row_or_sym or "").strip() + candidates = _swap_symbol_candidates({"symbol": sym}) if sym else [] + for sym in candidates: + try: + cs = float(get_contract_size(sym) or 0) + if cs > 0: + return cs + except Exception: + continue + return 1.0 def _fill_order_pnl_fields(row: dict[str, Any], *, mark: Optional[float], contract_size: float) -> None: @@ -415,23 +449,23 @@ def enrich_order_items_with_marks( out: list[dict[str, Any]] = [] for it in items: row = dict(it) - sym = str(row.get("price_symbol") or row.get("symbol") or "").strip() + # 标记价:先试 price_symbol,再试永续候选 mark = _safe_float(row.get("mark_price")) - if callable(get_price) and sym: - try: - px = get_price(sym) - except Exception: - px = None - mark = _safe_float(px) - if mark is None and ":" in sym: + if callable(get_price): + ordered: list[str] = [] + for s in [str(row.get("price_symbol") or "").strip()] + _swap_symbol_candidates(row): + if s and s not in ordered: + ordered.append(s) + for sym in ordered: 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 - cs = _resolve_contract_size(sym, get_contract_size=get_contract_size) + if mark is not None: + row["mark_price"] = mark + break + cs = _resolve_contract_size(row, 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 1335833..926ad07 100644 --- a/tests/test_instance_dashboard_lib.py +++ b/tests/test_instance_dashboard_lib.py @@ -201,6 +201,41 @@ class TestInstanceDashboardLib(unittest.TestCase): self.assertIsNotNone(out[0]["tp_profit"]) self.assertGreater(out[0]["tp_profit"], 0) + def test_enrich_prefers_swap_contract_size_over_spot(self): + """看板 price_symbol 常为 BTC/USDT,现货面会落到 1,必须用永续面值.""" + from lib.instance.instance_dashboard_lib import enrich_order_items_with_marks + + items = [ + { + "id": 1, + "symbol": "BTC/USDT:USDT", + "price_symbol": "BTC/USDT", + "direction": "long", + "entry": 64693.6, + "contracts": 132, + "take_profit": 65135.0, + "mark_price": None, + "tp_profit": None, + "float_pnl": None, + } + ] + + def get_price(sym): + return 64727.2 + + def get_cs(sym): + # 模拟未 normalize 的旧行为:现货 1,永续 0.0001 + if ":" in (sym or ""): + return 0.0001 + return 1.0 + + out = enrich_order_items_with_marks( + items, get_price=get_price, get_contract_size=get_cs + ) + # (64727.2 - 64693.6) * 132 * 0.0001 ≈ 0.44 + self.assertAlmostEqual(out[0]["float_pnl"], 0.44, places=2) + self.assertLess(out[0]["tp_profit"], 10) + if __name__ == "__main__": unittest.main()