fix: PnL计算改为美元归一化,加仓不再虚增手续费 (calcArbPnL)

This commit is contained in:
jackyu66git
2026-05-04 05:03:16 +08:00
parent c44d763ab0
commit ec725ed11a
+24 -5
View File
@@ -520,8 +520,7 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
longPnl := (longCurrent - longAvg) / longAvg * 100 longPnl := (longCurrent - longAvg) / longAvg * 100
shortPnl := (shortAvg - shortCurrent) / shortAvg * 100 shortPnl := (shortAvg - shortCurrent) / shortAvg * 100
totalFees := float64(2+pos.ScaleLevels) * (takerFees[ExBitget] + takerFees[ExHyperLiquid]) // 开仓(含加仓) + 平仓手续费 netPnl, totalFees := calcArbPnL(longPnl, shortPnl, pos.ScaleLevels, t.cfg.TradeAmountUSD) // 净利为总资本的百分比
netPnl := longPnl + shortPnl - totalFees
elapsed := time.Since(pos.StartedAt) elapsed := time.Since(pos.StartedAt)
@@ -602,7 +601,7 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
EntrySpread: pos.EntrySpread, EntrySpread: pos.EntrySpread,
ExitSpread: diffPct, ExitSpread: diffPct,
PnlPct: netPnl, PnlPct: netPnl,
PnlUSD: pos.AmountUSD * netPnl / 100, PnlUSD: 2 * pos.AmountUSD * netPnl / 100,
Convergence: convergenceLabel, Convergence: convergenceLabel,
Reason: exitReason, Reason: exitReason,
Duration: elapsed.Round(time.Second).String(), Duration: elapsed.Round(time.Second).String(),
@@ -854,6 +853,27 @@ func (t *Trader) GetOpenPositions() []*ArbPosition {
return r return r
} }
// calcArbPnL computes net PnL and total fees in USD, then normalizes to % of total deployed capital.
// This correctly handles scale-ins where the old formula (longPnl+shortPnl - (2+N)*0.105)
// double-counted fees because it didn't divide by (1+N) batches.
func calcArbPnL(longPnl, shortPnl float64, scaleLevels int, tradeAmountUSD float64) (netPnlPct, feePct float64) {
numBatches := 1 + scaleLevels
legCapital := tradeAmountUSD
totalCapital := float64(numBatches) * 2 * legCapital
// Gross PnL in USD
longPnlUSD := longPnl / 100 * float64(numBatches) * legCapital
shortPnlUSD := shortPnl / 100 * float64(numBatches) * legCapital
grossPnLUSD := longPnlUSD + shortPnlUSD
// Fee in USD (entry+exit per order-pair)
feeUSD := float64(2+scaleLevels) * legCapital * (takerFees[ExBitget] + takerFees[ExHyperLiquid]) / 100
netPnlPct = (grossPnLUSD - feeUSD) / totalCapital * 100
feePct = feeUSD / totalCapital * 100
return
}
// weightedAvgPrice computes the weighted average entry price across multiple scale levels. // weightedAvgPrice computes the weighted average entry price across multiple scale levels.
// Each level trades the same USD amount, so the result is the harmonic mean of prices. // Each level trades the same USD amount, so the result is the harmonic mean of prices.
func weightedAvgPrice(prices []float64, amountPerTrade float64) float64 { func weightedAvgPrice(prices []float64, amountPerTrade float64) float64 {
@@ -1161,8 +1181,7 @@ func (t *Trader) blacklistCoin(pos *ArbPosition, bgP, hlP, diffPct float64, noti
shortAvg := weightedAvgPrice(pos.ShortEntryPrices, t.cfg.TradeAmountUSD) shortAvg := weightedAvgPrice(pos.ShortEntryPrices, t.cfg.TradeAmountUSD)
longPnl := (longCurrent - longAvg) / longAvg * 100 longPnl := (longCurrent - longAvg) / longAvg * 100
shortPnl := (shortAvg - shortCurrent) / shortAvg * 100 shortPnl := (shortAvg - shortCurrent) / shortAvg * 100
totalFees := float64(2+pos.ScaleLevels) * (takerFees[ExBitget] + takerFees[ExHyperLiquid]) netPnl, totalFees := calcArbPnL(longPnl, shortPnl, pos.ScaleLevels, t.cfg.TradeAmountUSD)
netPnl := longPnl + shortPnl - totalFees
pos.ExitDiffPct = diffPct pos.ExitDiffPct = diffPct
pos.ExitNetPnl = netPnl pos.ExitNetPnl = netPnl