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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user