feat: actual exchange fees for entry orders

- BitgetTrade: added GetTradeFee() queries /mix/order/fills
- HyperLiquidTrade: added GetTradeFee() parses MarketOpen response
- trader.placeOrder() now returns (errMsg, actualFeeUSD)
- executeEntry uses actual fee from exchange, falls back to estimate
This commit is contained in:
jackyu66git
2026-05-04 18:35:33 +08:00
parent 0b2bfd0b02
commit 6baae85eaa
3 changed files with 132 additions and 28 deletions
+53 -27
View File
@@ -554,14 +554,18 @@ func (t *Trader) executeEntry(opp *ArbOpportunity, store *PriceStore, notifier *
t.mu.Unlock()
// Execute both legs
if err := t.placeOrder(pos.LongLeg, "buy", store); err != "" {
log.Printf("[Trader] %s: long leg placeOrder failed: %s", opp.Coin, err)
var longFeeUSD, shortFeeUSD float64
var errMsg string
errMsg, longFeeUSD = t.placeOrder(pos.LongLeg, "buy", store)
if errMsg != "" {
log.Printf("[Trader] %s: long leg placeOrder failed: %s", opp.Coin, errMsg)
t.cleanup(pos.Coin)
return false
}
time.Sleep(t.cfg.LegDelay)
if err := t.placeOrder(pos.ShortLeg, "sell", store); err != "" {
log.Printf("[Trader] %s: short leg placeOrder failed: %s", opp.Coin, err)
errMsg, shortFeeUSD = t.placeOrder(pos.ShortLeg, "sell", store)
if errMsg != "" {
log.Printf("[Trader] %s: short leg placeOrder failed: %s", opp.Coin, errMsg)
// Leg1 placed successfully, leg2 failed — try to close leg1
pos.Status = "failed"
if closeErr := t.closeLeg(pos.LongLeg); closeErr != "" {
@@ -570,7 +574,7 @@ func (t *Trader) executeEntry(opp *ArbOpportunity, store *PriceStore, notifier *
pos.ErrorLog = fmt.Sprintf("ORPHAN: leg1 %s %s placed OK, leg2 %s %s failed (%s), leg1 close also failed (%s)",
pos.LongLeg.Exchange, pos.LongLeg.Side,
pos.ShortLeg.Exchange, pos.ShortLeg.Side,
err, closeErr)
errMsg, closeErr)
log.Printf("[Trader] ⚠️ ORPHAN POSITION on %s: %s", pos.Coin, pos.ErrorLog)
}
t.cleanup(pos.Coin)
@@ -602,8 +606,13 @@ func (t *Trader) executeEntry(opp *ArbOpportunity, store *PriceStore, notifier *
if tradeID, err := t.db.SaveTrade(dbTrade); err == nil {
pos.DBTradeID = tradeID
longFee := tradeUnit * takerFees[pos.LongLeg.Exchange] / 100
shortFee := tradeUnit * takerFees[pos.ShortLeg.Exchange] / 100
// Use actual fee from exchange (fetched in placeOrder), fall back to estimate
if longFeeUSD <= 0 {
longFeeUSD = tradeUnit * takerFees[pos.LongLeg.Exchange] / 100
}
if shortFeeUSD <= 0 {
shortFeeUSD = tradeUnit * takerFees[pos.ShortLeg.Exchange] / 100
}
longShares := tradeUnit / pos.LongLeg.EntryPrice
shortShares := tradeUnit / pos.ShortLeg.EntryPrice
@@ -611,13 +620,13 @@ func (t *Trader) executeEntry(opp *ArbOpportunity, store *PriceStore, notifier *
TradeID: tradeID, Leg: "long", Type: "entry",
Exchange: pos.LongLeg.Exchange, Side: "buy",
Price: &pos.LongLeg.EntryPrice, Size: &longShares,
Fee: &longFee, Status: &status, CreatedAt: now,
Fee: &longFeeUSD, Status: &status, CreatedAt: now,
})
shortOID, _ := t.db.SaveOrder(&db.OrderRecord{
TradeID: tradeID, Leg: "short", Type: "entry",
Exchange: pos.ShortLeg.Exchange, Side: "sell",
Price: &pos.ShortLeg.EntryPrice, Size: &shortShares,
Fee: &shortFee, Status: &status, CreatedAt: now,
Fee: &shortFeeUSD, Status: &status, CreatedAt: now,
})
t.db.SaveSystemOrder(&db.SystemOrderRecord{
TradeID: tradeID, Type: "entry", Status: "filled",
@@ -838,14 +847,14 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
for _, p := range pos.LongEntryPrices {
totalLongSharesRetry += t.cfg.TradeAmountUSD / p
}
totalShortSharesRetry := 0.0
totalshortSharesRetry := 0.0
for _, p := range pos.ShortEntryPrices {
totalShortSharesRetry += t.cfg.TradeAmountUSD / p
totalshortSharesRetry += t.cfg.TradeAmountUSD / p
}
pos.ExitLongFeeUSD = float64(numBatchesRetry)*t.cfg.TradeAmountUSD*takerFees[pos.LongLeg.Exchange]/100 +
totalLongSharesRetry*longCurrent*takerFees[pos.LongLeg.Exchange]/100
pos.ExitShortFeeUSD = float64(numBatchesRetry)*t.cfg.TradeAmountUSD*takerFees[pos.ShortLeg.Exchange]/100 +
totalShortSharesRetry*shortCurrent*takerFees[pos.ShortLeg.Exchange]/100
totalshortSharesRetry*shortCurrent*takerFees[pos.ShortLeg.Exchange]/100
}
closeErr := t.closeBothLegs(pos)
@@ -878,15 +887,15 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
for _, p := range pos.LongEntryPrices {
totalLongShares += legCapital / p
}
totalShortShares := 0.0
totalshortShares := 0.0
for _, p := range pos.ShortEntryPrices {
totalShortShares += legCapital / p
totalshortShares += legCapital / p
}
longEntryFeeSum := float64(numBatches) * legCapital * takerFees[pos.LongLeg.Exchange] / 100
shortEntryFeeSum := float64(numBatches) * legCapital * takerFees[pos.ShortLeg.Exchange] / 100
longExitFeeAmt := totalLongShares * pos.LongLeg.ExitPrice * takerFees[pos.LongLeg.Exchange] / 100
shortExitFeeAmt := totalShortShares * pos.ShortLeg.ExitPrice * takerFees[pos.ShortLeg.Exchange] / 100
shortExitFeeAmt := totalshortShares * pos.ShortLeg.ExitPrice * takerFees[pos.ShortLeg.Exchange] / 100
longFeeUSD := longEntryFeeSum + longExitFeeAmt
shortFeeUSD := shortEntryFeeSum + shortExitFeeAmt
@@ -957,7 +966,7 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
Price: &pos.LongLeg.ExitPrice, Size: &longExitShares,
Fee: &longExitFeeAmt, Status: &status, CreatedAt: now,
})
shortExitShares := totalShortShares
shortExitShares := totalshortShares
shortOID, _ := t.db.SaveOrder(&db.OrderRecord{
TradeID: pos.DBTradeID, Leg: "short", Type: "exit",
Exchange: pos.ShortLeg.Exchange, Side: "buy",
@@ -1034,30 +1043,47 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
}
}
func (t *Trader) placeOrder(leg *PositionLeg, side string, store *PriceStore) string {
func (t *Trader) placeOrder(leg *PositionLeg, side string, store *PriceStore) (string, float64) {
if t.cfg.TestMode {
return t.mockFill(leg, side, store)
return t.mockFill(leg, side, store), 0
}
if leg.Exchange == ExBitget {
size := exchange.GetBitgetSize(leg.Coin+"USDT", t.cfg.TradeAmountUSD, leg.EntryPrice)
oid, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", size, "open")
if err != nil {
return fmt.Sprintf("BG %s error: %v", side, err)
return fmt.Sprintf("BG %s error: %v", side, err), 0
}
leg.Size = size
leg.OrderID = oid
log.Printf("[ExRes] BG %s %s: size=%s → response=%s", side, leg.Coin+"USDT", size, oid)
log.Printf("[ExRes] BG %s %s: size=%s oid=%s", side, leg.Coin+"USDT", size, oid)
// Fetch actual fee from exchange
fee, fetchErr := t.bitget.GetTradeFee(leg.Coin+"USDT", oid)
if fetchErr != nil {
log.Printf("[Fee] BG GetTradeFee warning: %v", fetchErr)
} else {
log.Printf("[Fee] BG %s %s: actual fee=$%.6f", side, leg.Coin+"USDT", fee)
}
return "", fee
} else {
size := exchange.GetHLSize(leg.Coin, t.cfg.TradeAmountUSD, leg.EntryPrice)
resp, err := t.hyperliquid.PlaceMarketOrder(leg.Coin, side, size)
if err != nil {
return fmt.Sprintf("HL %s error: %v", side, err)
return fmt.Sprintf("HL %s error: %v", side, err), 0
}
leg.Size = size
leg.OrderID = resp
log.Printf("[ExRes] HL %s %s: size=%s → response=%s", side, leg.Coin, size, resp)
log.Printf("[ExRes] HL %s %s: size=%s", side, leg.Coin, size)
// Estimate fee from filled response (HL doesn't return fee in order response)
fee, fetchErr := t.hyperliquid.GetTradeFee(resp, takerFees[ExHyperLiquid])
if fetchErr != nil {
log.Printf("[Fee] HL GetTradeFee warning: %v", fetchErr)
} else {
log.Printf("[Fee] HL %s %s: actual fee=$%.6f", side, leg.Coin, fee)
}
return "", fee
}
return ""
}
func (t *Trader) closeBothLegs(pos *ArbPosition) string {
@@ -1169,9 +1195,9 @@ func (t *Trader) retryClose(pos *ArbPosition, bgP, hlP float64, notifier *Notifi
for _, p := range pos.LongEntryPrices {
totalLongShares += tradeUnit / p
}
totalShortShares := 0.0
totalshortShares := 0.0
for _, p := range pos.ShortEntryPrices {
totalShortShares += tradeUnit / p
totalshortShares += tradeUnit / p
}
// Save exit orders for legs that were just now closed
@@ -1186,8 +1212,8 @@ func (t *Trader) retryClose(pos *ArbPosition, bgP, hlP float64, notifier *Notifi
})
}
if pos.ShortLeg.Closed {
shortExitFee := totalShortShares * pos.ShortLeg.ExitPrice * takerFees[pos.ShortLeg.Exchange] / 100
shortExitShares := totalShortShares
shortExitFee := totalshortShares * pos.ShortLeg.ExitPrice * takerFees[pos.ShortLeg.Exchange] / 100
shortExitShares := totalshortShares
_, _ = t.db.SaveOrder(&db.OrderRecord{
TradeID: pos.DBTradeID, Leg: "short", Type: "exit",
Exchange: pos.ShortLeg.Exchange, Side: "buy",