Deduct round-trip taker fees from estimated perp PnL.
Unify hub/instance TP profit, calc_pnl, and push/accounting to net of 0.05% per side; leave exchange floating PnL unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -315,10 +315,18 @@ def _roll_leg_preview(
|
||||
cs = float(contract_size) if contract_size else 1.0
|
||||
if direction == "long":
|
||||
loss_at_sl = (new_avg - sl) * new_qty * cs
|
||||
reward_at_tp = (tp - new_avg) * new_qty * cs
|
||||
reward_gross = (tp - new_avg) * new_qty * cs
|
||||
else:
|
||||
loss_at_sl = (sl - new_avg) * new_qty * cs
|
||||
reward_at_tp = (new_avg - tp) * new_qty * cs
|
||||
reward_gross = (new_avg - tp) * new_qty * cs
|
||||
try:
|
||||
from lib.trade.trade_fee_lib import net_pnl_after_fee
|
||||
|
||||
reward_at_tp = net_pnl_after_fee(reward_gross, new_avg, tp, new_qty, cs)
|
||||
if reward_at_tp is None:
|
||||
reward_at_tp = reward_gross
|
||||
except Exception:
|
||||
reward_at_tp = reward_gross
|
||||
return {
|
||||
"add_amount_raw": q2,
|
||||
"qty_after": new_qty,
|
||||
@@ -412,10 +420,18 @@ def calc_roll_calculator(
|
||||
|
||||
if direction == "long":
|
||||
first_loss = (avg - initial_sl) * qty_f * cs
|
||||
first_profit = (tp - avg) * qty_f * cs
|
||||
first_profit_gross = (tp - avg) * qty_f * cs
|
||||
else:
|
||||
first_loss = (initial_sl - avg) * qty_f * cs
|
||||
first_profit = (avg - tp) * qty_f * cs
|
||||
first_profit_gross = (avg - tp) * qty_f * cs
|
||||
try:
|
||||
from lib.trade.trade_fee_lib import net_pnl_after_fee
|
||||
|
||||
first_profit = net_pnl_after_fee(first_profit_gross, avg, tp, qty_f, cs)
|
||||
if first_profit is None:
|
||||
first_profit = first_profit_gross
|
||||
except Exception:
|
||||
first_profit = first_profit_gross
|
||||
|
||||
rows.append(
|
||||
{
|
||||
|
||||
@@ -143,8 +143,16 @@ def reward_at_tp_usdt(
|
||||
cs = float(contract_size or 1.0)
|
||||
direction = (direction or "long").strip().lower()
|
||||
if direction == "short":
|
||||
return (float(avg) - float(take_profit)) * float(qty) * cs
|
||||
return (float(take_profit) - float(avg)) * float(qty) * cs
|
||||
gross = (float(avg) - float(take_profit)) * float(qty) * cs
|
||||
else:
|
||||
gross = (float(take_profit) - float(avg)) * float(qty) * cs
|
||||
try:
|
||||
from lib.trade.trade_fee_lib import net_pnl_after_fee
|
||||
|
||||
net = net_pnl_after_fee(gross, avg, take_profit, qty, cs)
|
||||
return float(net) if net is not None else gross
|
||||
except Exception:
|
||||
return gross
|
||||
|
||||
|
||||
def roll_fib_trigger_crossed(
|
||||
|
||||
@@ -16,7 +16,7 @@ def reward_at_tp_usdt(
|
||||
*,
|
||||
contract_size: float = 1.0,
|
||||
) -> Optional[float]:
|
||||
"""与 strategy_roll_lib.preview_roll 一致:线性合约 U 本位盈利."""
|
||||
"""与 strategy_roll_lib.preview_roll 一致:线性合约 U 本位净盈利(扣双边 taker 费)."""
|
||||
try:
|
||||
avg = float(avg_entry)
|
||||
tp = float(take_profit)
|
||||
@@ -28,8 +28,16 @@ def reward_at_tp_usdt(
|
||||
return None
|
||||
direction = (direction or "long").strip().lower()
|
||||
if direction == "short":
|
||||
return (avg - tp) * q * cs
|
||||
return (tp - avg) * q * cs
|
||||
gross = (avg - tp) * q * cs
|
||||
else:
|
||||
gross = (tp - avg) * q * cs
|
||||
try:
|
||||
from lib.trade.trade_fee_lib import net_pnl_after_fee
|
||||
|
||||
net = net_pnl_after_fee(gross, avg, tp, q, cs)
|
||||
return net
|
||||
except Exception:
|
||||
return gross
|
||||
|
||||
|
||||
def leg_fill_price(leg: dict) -> Optional[float]:
|
||||
|
||||
@@ -306,13 +306,19 @@ def calc_tp_profit_usdt(
|
||||
contracts: float,
|
||||
contract_size: float = 1.0,
|
||||
) -> Optional[float]:
|
||||
"""到达止盈价时,按累计张数与加仓后均价的盈利 U."""
|
||||
"""到达止盈价时,按累计张数与加仓后均价的净盈利 U(扣双边 taker 费)."""
|
||||
try:
|
||||
from lib.hub.hub_position_metrics import estimate_linear_swap_upnl_usdt
|
||||
from lib.trade.trade_fee_lib import net_pnl_after_fee
|
||||
|
||||
return estimate_linear_swap_upnl_usdt(
|
||||
gross = estimate_linear_swap_upnl_usdt(
|
||||
direction, float(avg_entry), float(take_profit_price), float(contracts), float(contract_size)
|
||||
)
|
||||
if gross is None:
|
||||
return None
|
||||
return net_pnl_after_fee(
|
||||
gross, avg_entry, take_profit_price, contracts, contract_size
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""永续估算盈亏:固定 taker 手续费(默认单边 0.05%,开+平双边).
|
||||
|
||||
浮盈亏仍读交易所;本模块只服务「盈利金额 / 止盈盈利 / 推送 / 记账 pnl_amount」等估算口径.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def _finite(v) -> Optional[float]:
|
||||
try:
|
||||
f = float(v)
|
||||
return f if math.isfinite(f) else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def taker_fee_rate() -> float:
|
||||
"""单边 taker 费率,默认 0.0005(=0.05%)."""
|
||||
raw = os.getenv("PERP_TAKER_FEE_RATE", "0.0005")
|
||||
rate = _finite(raw)
|
||||
if rate is None or rate < 0:
|
||||
return 0.0005
|
||||
return rate
|
||||
|
||||
|
||||
def notional_usdt(price, qty, contract_size: float = 1.0) -> Optional[float]:
|
||||
"""名义价值 U = 价格 × 张数 × 合约面值."""
|
||||
p = _finite(price)
|
||||
q = _finite(qty)
|
||||
cs = _finite(contract_size)
|
||||
if p is None or q is None or p <= 0 or q <= 0:
|
||||
return None
|
||||
if cs is None or cs <= 0:
|
||||
cs = 1.0
|
||||
return abs(q) * p * cs
|
||||
|
||||
|
||||
def estimate_roundtrip_fee_usdt(
|
||||
entry_price,
|
||||
exit_price,
|
||||
qty=None,
|
||||
contract_size: float = 1.0,
|
||||
*,
|
||||
open_notional: float | None = None,
|
||||
rate: float | None = None,
|
||||
) -> float:
|
||||
"""开+平双边手续费(各单边 rate).
|
||||
|
||||
优先用 价×张×面值;若无张数则用 open_notional 估开仓名义,
|
||||
平仓名义按 exit/entry 缩放.
|
||||
"""
|
||||
fee_rate = taker_fee_rate() if rate is None else float(rate)
|
||||
if fee_rate <= 0:
|
||||
return 0.0
|
||||
entry = _finite(entry_price)
|
||||
exit_p = _finite(exit_price)
|
||||
open_n = notional_usdt(entry, qty, contract_size) if qty is not None else None
|
||||
if open_n is None:
|
||||
open_n = _finite(open_notional)
|
||||
if open_n is None or open_n <= 0:
|
||||
return 0.0
|
||||
if entry is not None and entry > 0 and exit_p is not None and exit_p > 0:
|
||||
close_n = open_n * (exit_p / entry)
|
||||
else:
|
||||
close_n = open_n
|
||||
return round(open_n * fee_rate + close_n * fee_rate, 8)
|
||||
|
||||
|
||||
def net_pnl_after_fee(
|
||||
gross_pnl,
|
||||
entry_price,
|
||||
exit_price,
|
||||
qty=None,
|
||||
contract_size: float = 1.0,
|
||||
*,
|
||||
open_notional: float | None = None,
|
||||
rate: float | None = None,
|
||||
) -> Optional[float]:
|
||||
"""毛利扣双边手续费后的净盈亏;gross 无效则返回 None."""
|
||||
gross = _finite(gross_pnl)
|
||||
if gross is None:
|
||||
return None
|
||||
fee = estimate_roundtrip_fee_usdt(
|
||||
entry_price,
|
||||
exit_price,
|
||||
qty,
|
||||
contract_size,
|
||||
open_notional=open_notional,
|
||||
rate=rate,
|
||||
)
|
||||
return round(gross - fee, 4)
|
||||
Reference in New Issue
Block a user