fix: 4 bugfixes for production readiness

1. GetHLSize: add minimum size checks (prevent size=0 when price > amount)
2. executeEntry: orphan position warning when leg1 close also fails after leg2
3. checkExit: keep position as close_failed on failure, retryClose on each Tick
4. persistTrade: sync instead of goroutine (prevent data loss on exit)
This commit is contained in:
jackyu66git
2026-05-03 23:13:44 +08:00
parent cdcf0acea6
commit cf6548c37f
2 changed files with 83 additions and 5 deletions
+18
View File
@@ -178,21 +178,39 @@ func GetHLSize(coin string, amountUSD, price float64) string {
switch coin {
case "DOGE":
sz = math.Floor(sz) // step=1, szDecimals=0
if sz < 1 {
sz = 1
}
return fmt.Sprintf("%.0f", sz)
case "LINK":
sz = math.Floor(sz*10) / 10 // step=0.1, szDecimals=1
if sz < 0.1 {
sz = 0.1
}
return fmt.Sprintf("%.1f", sz)
case "ONDO":
sz = math.Floor(sz) // step=1, szDecimals=0
if sz < 1 {
sz = 1
}
return fmt.Sprintf("%.0f", sz)
case "OP":
sz = math.Floor(sz*10) / 10 // step=0.1, szDecimals=1
if sz < 0.1 {
sz = 0.1
}
return fmt.Sprintf("%.1f", sz)
case "WIF":
sz = math.Floor(sz) // step=1, szDecimals=0
if sz < 1 {
sz = 1
}
return fmt.Sprintf("%.0f", sz)
case "ARB":
sz = math.Floor(sz*10) / 10 // step=0.1, szDecimals=1
if sz < 0.1 {
sz = 0.1
}
return fmt.Sprintf("%.1f", sz)
default:
return fmt.Sprintf("%.4f", sz)
+65 -5
View File
@@ -228,6 +228,12 @@ func (t *Trader) Tick(store *PriceStore, notifier *Notifier) {
}
diffPct := (highP - lowP) / lowP * 100
// Retry close for positions that failed to close on previous attempt
if pos.Status == "close_failed" {
t.retryClose(pos, bgP, hlP, notifier)
continue
}
// Check scale-in: if spread widened enough, add more
t.checkScaleIn(pos, bgP, hlP, diffPct, store)
@@ -355,7 +361,16 @@ func (t *Trader) executeEntry(opp *ArbOpportunity, store *PriceStore, notifier *
}
time.Sleep(300 * time.Millisecond)
if err := t.placeOrder(pos.ShortLeg, "sell", store); err != "" {
t.closeLeg(pos.LongLeg)
// Leg1 placed successfully, leg2 failed — try to close leg1
if closeErr := t.closeLeg(pos.LongLeg); closeErr != "" {
// CRITICAL: leg1 is still open on the exchange!
// Record the orphan so we don't silently lose tracking
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)
log.Printf("[Trader] ⚠️ ORPHAN POSITION on %s: %s", pos.Coin, pos.ErrorLog)
}
t.cleanup(pos.Coin)
return false
}
@@ -521,6 +536,20 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
closeErr := t.closeBothLegs(pos)
if closeErr != "" {
// Close failed — keep the position for retry on next Tick
pos.Status = "close_failed"
pos.ErrorLog = closeErr
pos.ExitedAt = time.Now()
log.Printf("[Trader] ❌ %s: Close failed: %s — will retry on next tick", pos.Coin, closeErr)
notifier.Send(fmt.Sprintf(
"<b>[平仓失败]</b> %s/USDT %s\n"+
" 状态: close_failed\n"+
" 错误: %s\n"+
" 下一轮将重试关掉剩余的腿\n", pos.Coin, pos.Direction, closeErr))
return
}
pos.RealizedPnl = netPnl
pos.ExitedAt = time.Now()
pos.Status = "closed"
@@ -549,7 +578,7 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
// Persist to SQLite
if t.db != nil {
go t.persistTrade(pos, diffPct, convergenceLabel, exitReason, netPnl, longPnl, shortPnl, totalFees)
t.persistTrade(pos, diffPct, convergenceLabel, exitReason, netPnl, longPnl, shortPnl, totalFees)
}
msg := fmt.Sprintf(
@@ -570,9 +599,6 @@ func (t *Trader) checkExit(pos *ArbPosition, bgP, hlP, diffPct float64, notifier
shortPnl, pos.ShortLeg.Exchange, pos.ShortLeg.EntryPrice, shortCurrent,
totalFees, netPnl, exitReason,
)
if closeErr != "" {
msg += fmt.Sprintf(" 平仓异常: %s\n", closeErr)
}
notifier.Send(msg)
// P3-4: real-time trade event push
@@ -658,6 +684,40 @@ func (t *Trader) closeLeg(leg *PositionLeg) string {
return ""
}
// retryClose retries closing a position that previously failed.
// Only closes legs not already marked Closed. Notifies periodically.
func (t *Trader) retryClose(pos *ArbPosition, bgP, hlP float64, notifier *Notifier) {
log.Printf("[Trader] %s: Retrying close (previous err: %s)", pos.Coin, pos.ErrorLog)
closeErr := t.closeBothLegs(pos)
if closeErr == "" {
// All legs finally closed — mark as done
pos.Status = "closed"
pos.ExitedAt = time.Now()
t.mu.Lock()
delete(t.positions, pos.Coin)
t.lastTradeTime[pos.Coin] = time.Now()
t.mu.Unlock()
notifier.Send(fmt.Sprintf(
"<b>[平仓重试成功]</b> %s/USDT %s\n"+
" 之前失败: %s\n"+
" 已成功关掉所有腿\n", pos.Coin, pos.Direction, pos.ErrorLog))
return
}
// Still failing — update log and notify periodically
pos.ErrorLog = closeErr
log.Printf("[Trader] ❌ %s: Retry close still failing: %s", pos.Coin, closeErr)
if time.Since(pos.ExitedAt) > 30*time.Second {
notifier.Send(fmt.Sprintf(
"<b>[平仓仍失败]</b> %s/USDT %s\n"+
" 已重试 %s, 仍失败: %s\n"+
" 请手动检查交易所\n", pos.Coin, pos.Direction,
time.Since(pos.ExitedAt).Round(time.Second).String(), closeErr))
pos.ExitedAt = time.Now()
}
}
// placeOrderAt places an order at a specified price (used for scale-in, Issue #2).
// Unlike placeOrder, this doesn't modify the leg's EntryPrice — it places
// an additional order at the current market price for the same trade amount.