Fix dashboard PnL using spot contract size of 1.

Prefer perpetual symbols and normalize before market.contractSize lookup so Gate BTC float matches ~0.4U not thousands.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-06 14:20:43 +08:00
parent 75a4175522
commit d41028b766
5 changed files with 91 additions and 22 deletions
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+53 -19
View File
@@ -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
+35
View File
@@ -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()