fix: 删除零成交误判逻辑 + 各种稳定性修复
- 删除: Bitget GetTradeFee 零成交检查(PlaceMarketOrder 成功即成交) - 修复: GetTradeFee 加 1s 延迟 + 查不到返回 0(用配置估算费兜底) - 修复: HL InitExchange 在 NewTrader 中提前调用,避免 szDecimals 延迟 - 修复: close_failed 30 次重试上限,超限标记 failed 并清理 - 修复: DB 恢复时校验 Legs 完整性,跳过非法记录 - 修复: checkScaleIn/checkExit nil guard 防 panic - 修复: config.go 参数调整(手续费、阈值等) - 移除: scanner.go 中 MEW/USTC 等低流动性币对 - 添加: 更详细的下单日志(szStr、amountUSD、price) - 添加: bin/ 到 .gitignore
This commit is contained in:
@@ -63,6 +63,7 @@ type ArbPosition struct {
|
||||
ExitShortPnlUSD float64
|
||||
ExitLongFeeUSD float64 // per-exchange fee in USD
|
||||
ExitShortFeeUSD float64
|
||||
CloseRetryCount int // how many times retryClose has been attempted
|
||||
|
||||
// Track all entry prices for weighted-average PnL across scale-ins (Issue #2)
|
||||
LongEntryPrices []float64 // all long entry prices (initial + scale-ins)
|
||||
@@ -192,6 +193,11 @@ func NewTrader(cfg *Config, database *db.DB) *Trader {
|
||||
bt = exchange.NewBitgetTrade(cfg.BitgetAPIKey, cfg.BitgetAPISecret, cfg.BitgetPassphrase)
|
||||
}
|
||||
hl, _ := exchange.NewHyperLiquidTrade(cfg.HLPrivateKey, cfg.HLAddress, cfg.HLAPIAddress)
|
||||
if hl != nil {
|
||||
if err := hl.InitExchange(); err != nil {
|
||||
log.Printf("[HL] InitExchange warning: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
t := &Trader{
|
||||
cfg: cfg,
|
||||
@@ -519,6 +525,13 @@ func (t *Trader) TryEntry(opp *ArbOpportunity, store *PriceStore, notifier *Noti
|
||||
// Blacklist expired — remove it and allow re-entry
|
||||
delete(t.blacklist, opp.Coin)
|
||||
}
|
||||
// Skip excluded coins
|
||||
for _, c := range t.cfg.ExcludedCoins {
|
||||
if c == opp.Coin {
|
||||
t.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
}
|
||||
if lastTime, ok := t.lastTradeTime[opp.Coin]; ok && time.Since(lastTime) < time.Duration(t.cfg.TradeCooldownMs)*time.Millisecond {
|
||||
t.mu.Unlock()
|
||||
return false
|
||||
@@ -772,6 +785,9 @@ func (t *Trader) checkScaleIn(pos *ArbPosition, bgP, hlP, diffPct float64, store
|
||||
if pos.Status != "open" {
|
||||
return
|
||||
}
|
||||
if pos.LongLeg == nil || pos.ShortLeg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Scale-in threshold: every +0.10% beyond entry
|
||||
var entryDiff float64
|
||||
@@ -869,6 +885,9 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
|
||||
if pos.Status != "open" {
|
||||
return
|
||||
}
|
||||
if pos.LongLeg == nil || pos.ShortLeg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Current prices for P&L calculation
|
||||
var longCurrent, shortCurrent float64
|
||||
@@ -1147,32 +1166,35 @@ func (t *Trader) placeOrder(leg *PositionLeg, side string, store *PriceStore) (s
|
||||
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")
|
||||
szStr := exchange.GetBitgetSize(leg.Coin+"USDT", t.cfg.TradeAmountUSD, leg.EntryPrice)
|
||||
log.Printf("[Order] BG %s %s | amountUSD=%.2f entryPrice=%.6f size=%s", side, leg.Coin+"USDT", t.cfg.TradeAmountUSD, leg.EntryPrice, szStr)
|
||||
oid, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", szStr, "open")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("BG %s error: %v", side, err), 0
|
||||
}
|
||||
leg.Size = size
|
||||
leg.Size = szStr
|
||||
leg.OrderID = oid
|
||||
log.Printf("[ExRes] BG %s %s: size=%s oid=%s", side, leg.Coin+"USDT", size, oid)
|
||||
log.Printf("[ExRes] BG %s %s: size=%s oid=%s", side, leg.Coin+"USDT", szStr, 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)
|
||||
fee = 0
|
||||
} else {
|
||||
log.Printf("[Fee] BG %s %s: actual fee=$%.6f", side, leg.Coin+"USDT", fee)
|
||||
log.Printf("[Fee] BG %s %s: actual fee=$%.6f (filled)", side, leg.Coin+"USDT", fee)
|
||||
}
|
||||
return "", fee
|
||||
} else {
|
||||
size := t.hyperliquid.GetSize(leg.Coin, t.cfg.TradeAmountUSD, leg.EntryPrice)
|
||||
resp, err := t.hyperliquid.PlaceMarketOrder(leg.Coin, side, size)
|
||||
szStr := t.hyperliquid.GetSize(leg.Coin, t.cfg.TradeAmountUSD, leg.EntryPrice)
|
||||
log.Printf("[Order] HL %s %s | amountUSD=%.2f entryPrice=%.6f size=%s", side, leg.Coin, t.cfg.TradeAmountUSD, leg.EntryPrice, szStr)
|
||||
resp, err := t.hyperliquid.PlaceMarketOrder(leg.Coin, side, szStr)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("HL %s error: %v", side, err), 0
|
||||
}
|
||||
leg.Size = size
|
||||
leg.Size = szStr
|
||||
leg.OrderID = resp
|
||||
log.Printf("[ExRes] HL %s %s: size=%s", side, leg.Coin, size)
|
||||
log.Printf("[ExRes] HL %s %s: size=%s", side, leg.Coin, szStr)
|
||||
|
||||
// Estimate fee from filled response (HL doesn't return fee in order response)
|
||||
fee, fetchErr := t.hyperliquid.EstimateFeeFromResponse(resp, takerFees[ExHyperLiquid])
|
||||
@@ -1375,12 +1397,14 @@ func (t *Trader) closeLeg(leg *PositionLeg) string {
|
||||
}
|
||||
|
||||
if leg.Exchange == ExBitget {
|
||||
log.Printf("[Order] BG close %s %s | size=%s", side, leg.Coin+"USDT", leg.Size)
|
||||
resp, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", leg.Size, "close")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", err)
|
||||
}
|
||||
log.Printf("[ExRes] BG close %s %s: size=%s → response=%s", side, leg.Coin+"USDT", leg.Size, resp)
|
||||
} else {
|
||||
log.Printf("[Order] HL close %s %s | size=%s", side, leg.Coin, leg.Size)
|
||||
resp, err := t.hyperliquid.PlaceMarketOrder(leg.Coin, side, leg.Size)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", err)
|
||||
@@ -1394,8 +1418,22 @@ func (t *Trader) closeLeg(leg *PositionLeg) string {
|
||||
|
||||
// retryClose retries closing a position that previously failed.
|
||||
// Only closes legs not already marked Closed. Notifies periodically.
|
||||
// Gives up after 30 failed attempts to avoid infinite log loops.
|
||||
func (t *Trader) retryClose(pos *ArbPosition, bgP, hlP float64, notifier *Notifier) {
|
||||
log.Printf("[Trader] %s: Retrying close (previous err: %s)", pos.Coin, pos.ErrorLog)
|
||||
pos.CloseRetryCount++
|
||||
if pos.CloseRetryCount > 30 {
|
||||
log.Printf("[Trader] %s: Retry close abandoned after %d attempts (last: %s)",
|
||||
pos.Coin, pos.CloseRetryCount, pos.ErrorLog)
|
||||
pos.Status = "failed"
|
||||
t.mu.Lock()
|
||||
delete(t.positions, pos.Coin)
|
||||
t.mu.Unlock()
|
||||
if t.db != nil && pos.DBTradeID > 0 {
|
||||
t.db.SetTradeStatus(pos.DBTradeID, "failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("[Trader] %s: Retrying close #%d (previous err: %s)", pos.Coin, pos.CloseRetryCount, pos.ErrorLog)
|
||||
|
||||
closeErr := t.closeBothLegs(pos)
|
||||
if closeErr == "" {
|
||||
@@ -1544,14 +1582,16 @@ func (t *Trader) placeOrderAt(leg *PositionLeg, side string, store *PriceStore,
|
||||
return err
|
||||
}
|
||||
if leg.Exchange == ExBitget {
|
||||
size := exchange.GetBitgetSize(leg.Coin+"USDT", t.cfg.TradeAmountUSD, price)
|
||||
_, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", size, "open")
|
||||
szStr := exchange.GetBitgetSize(leg.Coin+"USDT", t.cfg.TradeAmountUSD, price)
|
||||
log.Printf("[Order] BG scale %s %s | amountUSD=%.2f price=%.6f size=%s", side, leg.Coin+"USDT", t.cfg.TradeAmountUSD, price, szStr)
|
||||
_, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", szStr, "open")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("BG %s error: %v", side, err)
|
||||
}
|
||||
} else {
|
||||
size := t.hyperliquid.GetSize(leg.Coin, t.cfg.TradeAmountUSD, price)
|
||||
_, err := t.hyperliquid.PlaceMarketOrder(leg.Coin, side, size)
|
||||
szStr := t.hyperliquid.GetSize(leg.Coin, t.cfg.TradeAmountUSD, price)
|
||||
log.Printf("[Order] HL scale %s %s | amountUSD=%.2f price=%.6f size=%s", side, leg.Coin, t.cfg.TradeAmountUSD, price, szStr)
|
||||
_, err := t.hyperliquid.PlaceMarketOrder(leg.Coin, side, szStr)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("HL %s error: %v", side, err)
|
||||
}
|
||||
@@ -1730,6 +1770,14 @@ func (t *Trader) restoreOpenPositions() {
|
||||
pos.ShortEntryPrices = []float64{*tr.ShortEntry}
|
||||
}
|
||||
|
||||
// Skip if either leg is missing (incomplete DB record)
|
||||
if pos.LongLeg == nil || pos.ShortLeg == nil {
|
||||
log.Printf("[Trader] Skipping trade %d (%s): incomplete leg data (long=%v short=%v)",
|
||||
tr.ID, tr.Coin, tr.LongEntry, tr.ShortEntry)
|
||||
t.db.SetTradeStatus(tr.ID, "failed")
|
||||
continue
|
||||
}
|
||||
|
||||
// Restore scale-in prices from orders table for correct weighted average
|
||||
scaleLong, scaleShort, err := t.db.GetScalePrices(tr.ID)
|
||||
if err == nil {
|
||||
|
||||
Reference in New Issue
Block a user