53863559f4
Retarget git remote, install path, and deploy docs from crypto_monitor to crypto_monitor_user. Co-authored-by: Cursor <cursoragent@cursor.com>
271 lines
8.2 KiB
Python
271 lines
8.2 KiB
Python
"""ccxt 持仓标记价解析(实例 price_snapshot 与中控子代理共用)."""
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from typing import Any, Callable
|
||
|
||
|
||
def _finite_or_none(x: Any) -> float | None:
|
||
try:
|
||
f = float(x)
|
||
return f if math.isfinite(f) else None
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
def _coerce_float(*values: Any) -> float | None:
|
||
for v in values:
|
||
if v is None or v == "":
|
||
continue
|
||
px = _finite_or_none(v)
|
||
if px is not None and px > 0:
|
||
return px
|
||
return None
|
||
|
||
|
||
CONTRACTS_QTY_DECIMALS = 2
|
||
|
||
|
||
def normalize_contracts_qty(qty: Any, *, decimals: int = CONTRACTS_QTY_DECIMALS) -> float:
|
||
"""张数统一精度(OKX 等线性永续默认两位小数)."""
|
||
try:
|
||
q = float(qty)
|
||
except (TypeError, ValueError):
|
||
return 0.0
|
||
if not math.isfinite(q):
|
||
return 0.0
|
||
return round(abs(q), decimals)
|
||
|
||
|
||
def contracts_qty_is_open(qty: Any, *, decimals: int = CONTRACTS_QTY_DECIMALS) -> bool:
|
||
return normalize_contracts_qty(qty, decimals=decimals) > 0
|
||
|
||
|
||
def position_contracts(p: dict[str, Any]) -> float:
|
||
info = p.get("info") or {}
|
||
if not isinstance(info, dict):
|
||
info = {}
|
||
# OKX 等:info.pos 为交易所张数,优先于 ccxt contracts(加仓后后者可能滞后)
|
||
for k in ("pos", "positionAmt", "positionamt", "size"):
|
||
if k in info:
|
||
try:
|
||
v = float(info[k])
|
||
if v != 0:
|
||
return normalize_contracts_qty(v)
|
||
except (TypeError, ValueError):
|
||
pass
|
||
raw = p.get("contracts")
|
||
if raw is not None:
|
||
try:
|
||
v = float(raw)
|
||
if v != 0:
|
||
return normalize_contracts_qty(v)
|
||
except (TypeError, ValueError):
|
||
pass
|
||
return 0.0
|
||
|
||
|
||
def position_side_from_ccxt(p: dict[str, Any], contracts: float | None = None) -> str:
|
||
s = (p.get("side") or "").lower()
|
||
if s in ("long", "short"):
|
||
return s
|
||
c = contracts if contracts is not None else position_contracts(p)
|
||
if c > 0:
|
||
return "long"
|
||
if c < 0:
|
||
return "short"
|
||
return "long"
|
||
|
||
|
||
def parse_position_entry_price(p: dict[str, Any]) -> float | None:
|
||
"""三所 ccxt 持仓开仓均价."""
|
||
if not isinstance(p, dict):
|
||
return None
|
||
info = p.get("info") or {}
|
||
if not isinstance(info, dict):
|
||
info = {}
|
||
return _coerce_float(
|
||
p.get("entryPrice"),
|
||
p.get("entry_price"),
|
||
p.get("average"),
|
||
info.get("entryPrice"),
|
||
info.get("entry_price"),
|
||
info.get("avgPx"),
|
||
info.get("avgEntryPrice"),
|
||
info.get("avg_entry_price"),
|
||
info.get("avgPrice"),
|
||
info.get("openAvgPx"),
|
||
)
|
||
|
||
|
||
def estimate_linear_swap_upnl_usdt(
|
||
side: str,
|
||
entry: float | None,
|
||
mark: float | None,
|
||
contracts: float | None,
|
||
contract_size: float | None = None,
|
||
) -> float | None:
|
||
"""U 本位线性永续:浮盈 = (标记价 - 开仓价) × 张数 × contractSize(空头取反)."""
|
||
e = _finite_or_none(entry)
|
||
m = _finite_or_none(mark)
|
||
c = _finite_or_none(contracts)
|
||
if e is None or m is None or c is None or c <= 0:
|
||
return None
|
||
mult = _finite_or_none(contract_size)
|
||
if mult is None or mult <= 0:
|
||
mult = 1.0
|
||
diff = (m - e) if (side or "long").strip().lower() == "long" else (e - m)
|
||
return round(diff * abs(c) * mult, 2)
|
||
|
||
|
||
def resolve_position_display_upnl(
|
||
side: str,
|
||
entry: float | None,
|
||
mark: float | None,
|
||
contracts: float | None,
|
||
contract_size: float | None,
|
||
exchange_upnl: float | None,
|
||
) -> float | None:
|
||
"""展示用浮盈:优先与标记价/张数一致的推算;与交易所值偏差过大时用推算值."""
|
||
computed = estimate_linear_swap_upnl_usdt(
|
||
side, entry, mark, contracts, contract_size
|
||
)
|
||
if computed is None:
|
||
return exchange_upnl
|
||
if exchange_upnl is None:
|
||
return computed
|
||
ref = max(abs(computed), 1.0)
|
||
if abs(exchange_upnl - computed) / ref > 0.2:
|
||
return computed
|
||
return exchange_upnl
|
||
|
||
|
||
def _coerce_signed(*values: Any) -> float | None:
|
||
"""解析可正可负的数值(未实现盈亏等)."""
|
||
for v in values:
|
||
if v is None or v == "":
|
||
continue
|
||
f = _finite_or_none(v)
|
||
if f is not None:
|
||
return f
|
||
return None
|
||
|
||
|
||
def parse_position_unrealized_pnl(p: dict[str, Any]) -> float | None:
|
||
"""三所 ccxt 持仓统一解析未实现盈亏(Gate/OKX/Binance 字段名不一致)."""
|
||
if not isinstance(p, dict):
|
||
return None
|
||
info = p.get("info") or {}
|
||
if not isinstance(info, dict):
|
||
info = {}
|
||
return _coerce_signed(
|
||
p.get("unrealizedPnl"),
|
||
p.get("unrealisedPnl"),
|
||
p.get("unrealized_pnl"),
|
||
p.get("unrealised_pnl"),
|
||
info.get("unrealised_pnl"),
|
||
info.get("unrealized_pnl"),
|
||
info.get("unrealisedPnl"),
|
||
info.get("unrealizedPnl"),
|
||
info.get("upl"),
|
||
info.get("uplLast"),
|
||
)
|
||
|
||
|
||
def enrich_ccxt_position_metrics_out(
|
||
position: dict[str, Any],
|
||
out: dict[str, Any],
|
||
*,
|
||
contract_size: float = 1.0,
|
||
funds_decimals: int = 2,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
三所 parse_ccxt_position_metrics 产出后统一:
|
||
- 标记价用 hub 兜底
|
||
- 未实现盈亏 = resolve(交易所值, entry/mark/张数/contractSize 推算)
|
||
"""
|
||
if not isinstance(position, dict) or not isinstance(out, dict):
|
||
return out
|
||
mark = _finite_or_none(out.get("mark_price"))
|
||
if mark is None or mark <= 0:
|
||
mp = parse_position_mark_price(position)
|
||
if mp is not None and mp > 0:
|
||
out["mark_price"] = round(mp, 8)
|
||
mark = mp
|
||
exchange_upnl = parse_position_unrealized_pnl(position)
|
||
if exchange_upnl is None:
|
||
exchange_upnl = _coerce_signed(out.get("unrealized_pnl"))
|
||
c = position_contracts(position)
|
||
if abs(c) < 1e-12:
|
||
return out
|
||
side = position_side_from_ccxt(position, c)
|
||
entry = parse_position_entry_price(position)
|
||
if entry is not None and entry > 0:
|
||
out["entry_price"] = round(entry, 8)
|
||
cs = contract_size if contract_size and contract_size > 0 else 1.0
|
||
upnl = resolve_position_display_upnl(
|
||
side, entry, mark, abs(c), cs, exchange_upnl
|
||
)
|
||
if upnl is not None:
|
||
out["unrealized_pnl"] = round(upnl, funds_decimals)
|
||
return out
|
||
|
||
|
||
def parse_position_mark_price(p: dict[str, Any]) -> float | None:
|
||
"""三所 ccxt 持仓统一解析标记价(与 crypto_monitor_* parse_ccxt_position_metrics 口径一致)."""
|
||
if not isinstance(p, dict):
|
||
return None
|
||
info = p.get("info") or {}
|
||
if not isinstance(info, dict):
|
||
info = {}
|
||
mark = _coerce_float(
|
||
p.get("markPrice"),
|
||
p.get("mark_price"),
|
||
p.get("mark"),
|
||
info.get("markPx"),
|
||
info.get("mark_price"),
|
||
info.get("markPrice"),
|
||
)
|
||
if mark is not None:
|
||
return mark
|
||
contracts = position_contracts(p)
|
||
if abs(contracts) >= 1e-12:
|
||
notional = _finite_or_none(p.get("notional"))
|
||
if notional is not None and abs(notional) > 0:
|
||
return abs(notional) / abs(contracts)
|
||
return None
|
||
|
||
|
||
def build_position_marks_list(
|
||
positions: list,
|
||
*,
|
||
format_mark_display: Callable[[str, float], str] | None = None,
|
||
) -> list[dict[str, Any]]:
|
||
"""从 fetch_positions 结果生成 position_marks,供 price_snapshot / 中控合并."""
|
||
out: list[dict[str, Any]] = []
|
||
for p in positions or []:
|
||
if not isinstance(p, dict):
|
||
continue
|
||
c = position_contracts(p)
|
||
if abs(c) < 1e-12:
|
||
continue
|
||
mark = parse_position_mark_price(p)
|
||
if mark is None or mark <= 0:
|
||
continue
|
||
sym = (p.get("symbol") or "").strip()
|
||
side = position_side_from_ccxt(p, c)
|
||
row: dict[str, Any] = {
|
||
"symbol": sym,
|
||
"side": side,
|
||
"mark_price": mark,
|
||
}
|
||
if format_mark_display and sym:
|
||
try:
|
||
row["mark_price_display"] = format_mark_display(sym, mark)
|
||
except Exception:
|
||
row["mark_price_display"] = f"{mark:g}"
|
||
else:
|
||
row["mark_price_display"] = f"{mark:g}"
|
||
out.append(row)
|
||
return out
|