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:
dekun
2026-07-26 21:49:20 +08:00
parent b7e055e37b
commit ca66c494e7
12 changed files with 685 additions and 25 deletions
+150 -7
View File
@@ -163,16 +163,18 @@ class BinanceTradeClient:
data = q
if not avg or avg <= 0:
raise RuntimeError(f"币安永续无成交均价: {data}")
# 手续费:优先 cumCommission;否则用名义×费率估
from .money import abs_fee_usdt
fee = abs(safe_float(data.get("cumCommission")) or 0.0)
if fee <= 0:
fee = float(avg) * float(sz or 0) * float(self.settings.fee_rate)
fee_asset = str(data.get("commissionAsset") or "USDT")
if fee <= 0 and ord_id:
fee, fee_asset = self.sum_perp_trade_fees(symbol, ord_id)
return LiveFill(
inst_id=symbol,
side=str(data.get("side") or "").lower(),
avg_px=float(avg),
sz=float(sz or 0),
fee=float(fee),
fee=abs_fee_usdt(fee, fee_asset),
ord_id=ord_id,
raw=data if isinstance(data, dict) else {},
)
@@ -223,15 +225,156 @@ class BinanceTradeClient:
raise RuntimeError(f"币安期权订单失败 status={st} {q}")
if not avg or avg <= 0:
raise RuntimeError(f"币安期权无成交均价: {data}")
from .money import abs_fee_usdt
fee = abs(safe_float(data.get("fee")) or 0.0)
if fee <= 0:
fee = float(avg) * float(sz or 0) * float(self.settings.fee_rate)
fee_asset = "USDT"
if fee <= 0 and ord_id:
fee, fee_asset = self.sum_option_trade_fees(symbol, ord_id)
return LiveFill(
inst_id=symbol,
side=str(data.get("side") or "").lower(),
avg_px=float(avg),
sz=float(sz or 0),
fee=float(fee),
fee=abs_fee_usdt(fee, fee_asset),
ord_id=ord_id,
raw=data if isinstance(data, dict) else {},
)
def sum_perp_trade_fees(self, symbol: str, order_id: str) -> tuple[float, str]:
try:
rows = self._signed(
self._fapi,
"GET",
"/fapi/v1/userTrades",
{"symbol": symbol, "orderId": order_id},
)
except Exception as e:
logger.warning("binance perp userTrades fee failed: %s", e)
return 0.0, "USDT"
if not isinstance(rows, list):
rows = [rows] if isinstance(rows, dict) else []
total = 0.0
asset = "USDT"
for row in rows:
total += abs(safe_float(row.get("commission")) or 0.0)
if row.get("commissionAsset"):
asset = str(row.get("commissionAsset"))
return total, asset
def sum_option_trade_fees(self, symbol: str, order_id: str) -> tuple[float, str]:
try:
rows = self._signed(
self._eapi,
"GET",
"/eapi/v1/userTrades",
{"symbol": symbol, "orderId": order_id},
)
except Exception as e:
logger.warning("binance option userTrades fee failed: %s", e)
return 0.0, "USDT"
if not isinstance(rows, list):
rows = [rows] if isinstance(rows, dict) else []
total = 0.0
asset = "USDT"
for row in rows:
total += abs(safe_float(row.get("commission")) or safe_float(row.get("fee")) or 0.0)
if row.get("commissionAsset") or row.get("feeAsset"):
asset = str(row.get("commissionAsset") or row.get("feeAsset"))
return total, asset
def get_perp_upl_usdt(self, symbol: str, *, position_side: str | None = None) -> float | None:
from .money import to_usdt
try:
rows = self._signed(
self._fapi, "GET", "/fapi/v2/positionRisk", {"symbol": symbol}
)
except Exception as e:
logger.warning("binance positionRisk failed: %s", e)
return None
if isinstance(rows, dict):
rows = [rows]
want = (position_side or "").strip().upper()
for row in rows:
amt = safe_float(row.get("positionAmt")) or 0.0
if abs(amt) < 1e-12:
continue
ps = str(row.get("positionSide") or "").upper()
if want and ps and ps not in ("BOTH",) and ps != want:
continue
upl = safe_float(row.get("unRealizedProfit"))
if upl is None:
continue
return to_usdt(float(upl), "USDT")
return 0.0
def get_funding_usdt(
self, symbol: str, *, begin_ms: int, end_ms: int | None = None
) -> float:
from .money import to_usdt
end = int(end_ms or int(time.time() * 1000))
try:
rows = self._signed(
self._fapi,
"GET",
"/fapi/v1/income",
{
"symbol": symbol,
"incomeType": "FUNDING_FEE",
"startTime": int(begin_ms),
"endTime": end,
"limit": 1000,
},
)
except Exception as e:
logger.warning("binance funding income failed: %s", e)
return 0.0
if isinstance(rows, dict):
rows = [rows]
total = 0.0
for row in rows:
raw = safe_float(row.get("income"))
if raw is None:
continue
asset = str(row.get("asset") or "USDT")
total += to_usdt(float(raw), asset)
return total
def get_closed_perp_pnl_usdt(
self, symbol: str, *, begin_ms: int, end_ms: int | None = None
) -> float | None:
"""用 REALIZED_PNL income 近似已实现(含部分平仓);资金费另计。"""
from .money import to_usdt
end = int(end_ms or int(time.time() * 1000))
try:
rows = self._signed(
self._fapi,
"GET",
"/fapi/v1/income",
{
"symbol": symbol,
"incomeType": "REALIZED_PNL",
"startTime": int(begin_ms),
"endTime": end,
"limit": 1000,
},
)
except Exception as e:
logger.warning("binance realized income failed: %s", e)
return None
if isinstance(rows, dict):
rows = [rows]
if not rows:
return None
total = 0.0
for row in rows:
raw = safe_float(row.get("income"))
if raw is None:
continue
asset = str(row.get("asset") or "USDT")
total += to_usdt(float(raw), asset)
return total