Use exchange fees, UPL, funding for LIVE PnL; USDC 1:1 to USDT.
Option open P&L stays local (option net); exit target ignores estimated close fees. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -169,14 +169,20 @@ class OkxTradeClient:
|
||||
state = str(last.get("state") or "")
|
||||
avg = safe_float(last.get("avgPx"))
|
||||
if state in ("filled", "partially_filled") and avg and avg > 0:
|
||||
fee = abs(safe_float(last.get("fee")) or 0.0)
|
||||
sz = safe_float(last.get("accFillSz")) or safe_float(last.get("sz")) or 0.0
|
||||
fee = abs(safe_float(last.get("fee")) or 0.0)
|
||||
fee_ccy = str(last.get("feeCcy") or "USDT")
|
||||
# 订单上 fee 常为空,再查成交明细
|
||||
if fee <= 0 and ord_id:
|
||||
fee, fee_ccy = self.sum_fill_fees(inst_id, ord_id)
|
||||
from .money import abs_fee_usdt
|
||||
|
||||
return LiveFill(
|
||||
inst_id=inst_id,
|
||||
side=str(last.get("side") or ""),
|
||||
avg_px=float(avg),
|
||||
sz=float(sz),
|
||||
fee=float(fee),
|
||||
fee=abs_fee_usdt(fee, fee_ccy),
|
||||
ord_id=ord_id,
|
||||
raw=last,
|
||||
)
|
||||
@@ -184,3 +190,112 @@ class OkxTradeClient:
|
||||
raise RuntimeError(f"OKX 订单失败 state={state} {last}")
|
||||
time.sleep(0.25)
|
||||
raise RuntimeError(f"OKX 订单未成交 ordId={ord_id} last={last}")
|
||||
|
||||
def sum_fill_fees(self, inst_id: str, ord_id: str) -> tuple[float, str]:
|
||||
"""成交明细手续费合计(原币种金额, 币种)。"""
|
||||
path = f"/api/v5/trade/fills?instId={inst_id}&ordId={ord_id}"
|
||||
try:
|
||||
rows = self._request("GET", path)
|
||||
except Exception as e:
|
||||
logger.warning("okx fills fee query failed: %s", e)
|
||||
return 0.0, "USDT"
|
||||
total = 0.0
|
||||
ccy = "USDT"
|
||||
for row in rows:
|
||||
f = abs(safe_float(row.get("fee")) or 0.0)
|
||||
total += f
|
||||
if row.get("feeCcy"):
|
||||
ccy = str(row.get("feeCcy"))
|
||||
return total, ccy
|
||||
|
||||
def get_perp_upl_usdt(self, inst_id: str, *, pos_side: str | None = None) -> float | None:
|
||||
"""当前永续未实现盈亏(USDT,1:1)。"""
|
||||
from .money import to_usdt
|
||||
|
||||
try:
|
||||
rows = self._request(
|
||||
"GET", f"/api/v5/account/positions?instId={inst_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("okx positions failed: %s", e)
|
||||
return None
|
||||
want = (pos_side or "").strip().lower()
|
||||
for row in rows:
|
||||
ps = str(row.get("posSide") or "").lower()
|
||||
pos = safe_float(row.get("pos")) or 0.0
|
||||
if abs(pos) < 1e-12:
|
||||
continue
|
||||
if want and want not in ("net", "") and ps and ps != want and ps != "net":
|
||||
continue
|
||||
upl = safe_float(row.get("upl"))
|
||||
if upl is None:
|
||||
continue
|
||||
ccy = str(row.get("ccy") or row.get("settleCcy") or "USDT")
|
||||
return to_usdt(float(upl), ccy)
|
||||
return 0.0
|
||||
|
||||
def get_funding_usdt(
|
||||
self, inst_id: str, *, begin_ms: int, end_ms: int | None = None
|
||||
) -> float:
|
||||
"""资金费合计(已计入账户的 signed 金额,USDT 1:1)。付费为负。"""
|
||||
from .money import to_usdt
|
||||
|
||||
end = int(end_ms or int(time.time() * 1000))
|
||||
# type=8 funding fee
|
||||
path = (
|
||||
f"/api/v5/account/bills?instType=SWAP&instId={inst_id}"
|
||||
f"&type=8&begin={int(begin_ms)}&end={end}"
|
||||
)
|
||||
total = 0.0
|
||||
try:
|
||||
rows = self._request("GET", path)
|
||||
except Exception as e:
|
||||
logger.warning("okx funding bills failed: %s", e)
|
||||
return 0.0
|
||||
for row in rows:
|
||||
# balChg / pnl 视接口;资金费常用 pnl 或 balChg
|
||||
raw = safe_float(row.get("pnl"))
|
||||
if raw is None:
|
||||
raw = safe_float(row.get("balChg"))
|
||||
if raw is None:
|
||||
continue
|
||||
ccy = str(row.get("ccy") or "USDT")
|
||||
total += to_usdt(float(raw), ccy)
|
||||
return total
|
||||
|
||||
def get_closed_perp_pnl_usdt(
|
||||
self, inst_id: str, *, begin_ms: int, end_ms: int | None = None
|
||||
) -> float | None:
|
||||
"""平仓后从历史仓位取已实现盈亏(不含手续费;含部分仓位盈亏)。"""
|
||||
from .money import to_usdt
|
||||
|
||||
end = int(end_ms or int(time.time() * 1000))
|
||||
path = (
|
||||
f"/api/v5/account/positions-history?instType=SWAP&instId={inst_id}"
|
||||
f"&before={end}&after={int(begin_ms)}"
|
||||
)
|
||||
try:
|
||||
# positions-history 用 GET query;部分环境用 before/after 语义相反,失败则返回 None
|
||||
rows = self._request(
|
||||
"GET",
|
||||
f"/api/v5/account/positions-history?instType=SWAP&instId={inst_id}",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("okx positions-history failed: %s", e)
|
||||
return None
|
||||
total = 0.0
|
||||
hit = False
|
||||
for row in rows:
|
||||
u_time = int(safe_float(row.get("uTime")) or safe_float(row.get("cTime")) or 0)
|
||||
if u_time and (u_time < int(begin_ms) - 60_000 or u_time > end + 60_000):
|
||||
continue
|
||||
rpnl = safe_float(row.get("realizedPnl"))
|
||||
if rpnl is None:
|
||||
rpnl = safe_float(row.get("pnl"))
|
||||
if rpnl is None:
|
||||
continue
|
||||
hit = True
|
||||
ccy = str(row.get("ccy") or "USDT")
|
||||
total += to_usdt(float(rpnl), ccy)
|
||||
return total if hit else None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user