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):
|
def get_contract_size(exchange_symbol):
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
market = exchange.market(exchange_symbol)
|
market = exchange.market(normalize_exchange_symbol(exchange_symbol))
|
||||||
return float(market.get("contractSize") or 1)
|
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):
|
def get_contract_size(exchange_symbol):
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
market = exchange.market(exchange_symbol)
|
market = exchange.market(normalize_exchange_symbol(exchange_symbol))
|
||||||
return float(market.get("contractSize") or 1)
|
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):
|
def get_contract_size(exchange_symbol):
|
||||||
try:
|
try:
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
market = exchange.market(exchange_symbol)
|
market = exchange.market(normalize_okx_symbol(exchange_symbol))
|
||||||
return float(market.get("contractSize") or 1)
|
return float(market.get("contractSize") or 1)
|
||||||
except Exception:
|
except Exception:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|||||||
@@ -358,18 +358,52 @@ def collect_options_items(
|
|||||||
return out
|
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(
|
def _resolve_contract_size(
|
||||||
sym: str,
|
row_or_sym: Any,
|
||||||
*,
|
*,
|
||||||
get_contract_size: Optional[Callable[[str], Any]] = None,
|
get_contract_size: Optional[Callable[[str], Any]] = None,
|
||||||
) -> float:
|
) -> float:
|
||||||
if not callable(get_contract_size) or not sym:
|
if not callable(get_contract_size):
|
||||||
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
|
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:
|
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]] = []
|
out: list[dict[str, Any]] = []
|
||||||
for it in items:
|
for it in items:
|
||||||
row = dict(it)
|
row = dict(it)
|
||||||
sym = str(row.get("price_symbol") or row.get("symbol") or "").strip()
|
# 标记价:先试 price_symbol,再试永续候选
|
||||||
mark = _safe_float(row.get("mark_price"))
|
mark = _safe_float(row.get("mark_price"))
|
||||||
if callable(get_price) and sym:
|
if callable(get_price):
|
||||||
try:
|
ordered: list[str] = []
|
||||||
px = get_price(sym)
|
for s in [str(row.get("price_symbol") or "").strip()] + _swap_symbol_candidates(row):
|
||||||
except Exception:
|
if s and s not in ordered:
|
||||||
px = None
|
ordered.append(s)
|
||||||
mark = _safe_float(px)
|
for sym in ordered:
|
||||||
if mark is None and ":" in sym:
|
|
||||||
try:
|
try:
|
||||||
px = get_price(sym.split(":", 1)[0])
|
px = get_price(sym)
|
||||||
except Exception:
|
except Exception:
|
||||||
px = None
|
px = None
|
||||||
mark = _safe_float(px)
|
mark = _safe_float(px)
|
||||||
if mark is not None:
|
if mark is not None:
|
||||||
row["mark_price"] = mark
|
row["mark_price"] = mark
|
||||||
cs = _resolve_contract_size(sym, get_contract_size=get_contract_size)
|
break
|
||||||
|
cs = _resolve_contract_size(row, get_contract_size=get_contract_size)
|
||||||
_fill_order_pnl_fields(row, mark=mark, contract_size=cs)
|
_fill_order_pnl_fields(row, mark=mark, contract_size=cs)
|
||||||
out.append(row)
|
out.append(row)
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -201,6 +201,41 @@ class TestInstanceDashboardLib(unittest.TestCase):
|
|||||||
self.assertIsNotNone(out[0]["tp_profit"])
|
self.assertIsNotNone(out[0]["tp_profit"])
|
||||||
self.assertGreater(out[0]["tp_profit"], 0)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user