diff --git a/trader.go b/trader.go index f7cf14a..1d79936 100644 --- a/trader.go +++ b/trader.go @@ -118,6 +118,7 @@ type Trader struct { db *db.DB mu sync.Mutex positions map[string]*ArbPosition // coin -> position + entering map[string]bool // coin -> being entered (async goroutine) lastTradeTime map[string]time.Time closedTrades []TradeRecord // history of closed trades (current session) @@ -160,6 +161,7 @@ func NewTrader(cfg *Config, database *db.DB) *Trader { bitget: bt, hyperliquid: hl, positions: make(map[string]*ArbPosition), + entering: make(map[string]bool), lastTradeTime: make(map[string]time.Time), } @@ -235,6 +237,8 @@ func (t *Trader) Tick(store *PriceStore, notifier *Notifier) { } // TryEntry opens initial position when threshold is met. +// Returns true if entry was accepted (async goroutine will place orders). +// Non-blocking — the main loop is not stalled by REST calls or the 300ms leg delay. func (t *Trader) TryEntry(opp *ArbOpportunity, store *PriceStore, notifier *Notifier) bool { if !t.IsConfigured() { return false @@ -252,13 +256,25 @@ func (t *Trader) TryEntry(opp *ArbOpportunity, store *PriceStore, notifier *Noti t.mu.Unlock() return false } + if t.entering[opp.Coin] { + t.mu.Unlock() + return false + } if lastTime, ok := t.lastTradeTime[opp.Coin]; ok && time.Since(lastTime) < 30*time.Second { t.mu.Unlock() return false } + t.entering[opp.Coin] = true t.mu.Unlock() - return t.executeEntry(opp, store, notifier) + // Async goroutine — placeOrder calls (REST or mock) don't block the main loop + go func() { + t.executeEntry(opp, store, notifier) + t.mu.Lock() + delete(t.entering, opp.Coin) + t.mu.Unlock() + }() + return true } // executeEntry places both legs using the scan-time prices from ArbOpportunity.