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:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user