From 02b74f1ec06cc1858628ed79a0c94033c6a97f66 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Sun, 3 May 2026 18:35:47 +0800 Subject: [PATCH] decouple display from trading: snapMu + RefreshSnapshot/ReadSnapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add snapMu RWMutex + positionsSnapshot to Trader - RefreshSnapshot() called from main loop after Tick() — acquires t.mu briefly, stores deep copy under snapMu - ReadSnapshot() returns snapshot copy under snapMu.RLock — never touches t.mu, zero contention with trading path - Dashboard + handleStatus + hourly summary + status log all use ReadSnapshot() instead of GetPositionsCopy() - Trading path (Tick/TryEntry/executeEntry/checkExit/checkScaleIn) never blocked by display reads - Snapshot is at most 1 tick behind live state — acceptable delay --- dashboard.go | 6 +++--- main.go | 7 ++++--- trader.go | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/dashboard.go b/dashboard.go index 0297005..c729cf5 100644 --- a/dashboard.go +++ b/dashboard.go @@ -290,8 +290,8 @@ func (d *Dashboard) broadcastLoop() { } d.hub.Broadcast("prices", prices) - // 2. Open positions with live PnL (P3-3) — use safe copy for concurrent read - positions := d.trader.GetPositionsCopy() + // 2. Open positions with live PnL (P3-3) — read from decoupled snapshot, never blocks trader + positions := d.trader.ReadSnapshot() posList := make([]map[string]interface{}, 0, len(positions)) for _, pos := range positions { posEntry := map[string]interface{}{ @@ -428,7 +428,7 @@ func (d *Dashboard) handleIndex(w http.ResponseWriter, r *http.Request) { func (d *Dashboard) handleStatus(w http.ResponseWriter, r *http.Request) { snap := d.store.GetAll() - positions := d.trader.GetPositionsCopy() + positions := d.trader.ReadSnapshot() converged, diverged, flat, total := d.trader.GetClosedStats() resp := map[string]interface{}{ diff --git a/main.go b/main.go index 6362674..5aa1f75 100644 --- a/main.go +++ b/main.go @@ -147,8 +147,8 @@ func main() { } log.Printf("[Status] %d prices / %d coins connected", count, len(snap)) - // Show open positions - if positions := trader.GetOpenPositions(); len(positions) > 0 { + // Show open positions (read from decoupled snapshot) + if positions := trader.ReadSnapshot(); len(positions) > 0 { for _, pos := range positions { log.Printf(" [Position] %s %s open %d scales $%.0f since %s", pos.Coin, pos.Direction, pos.ScaleLevels, pos.AmountUSD, @@ -162,6 +162,7 @@ func main() { // Tick the trader (monitor open positions for exit) trader.Tick(store, notifier) + trader.RefreshSnapshot() // decoupled snapshot for display t1 := time.Now() // Scan for arbitrage entries using maker fees (limit orders) @@ -190,7 +191,7 @@ func main() { // Hourly trade summary — use hour-based tracking (wider window than second-granularity) hour := now.Hour() if hour != lastHour && now.Minute() < 1 { - positions := trader.GetPositionsCopy() + positions := trader.ReadSnapshot() notifier.SendTradeSummary(positions, now.Format("2006-01-02 15:04")) lastHour = hour } diff --git a/trader.go b/trader.go index a222b1b..5ac8f43 100644 --- a/trader.go +++ b/trader.go @@ -89,6 +89,26 @@ func (t *Trader) GetPositionsCopy() []ArbPosition { return r } +// RefreshSnapshot takes a trading-lock snapshot of open positions for display use. +// Call this after each Tick() from the main loop — never during a trading operation. +// The display reads from this snapshot without blocking trading. +func (t *Trader) RefreshSnapshot() { + copy := t.GetPositionsCopy() // acquires t.mu briefly (not held during Tick call) + t.snapMu.Lock() + t.positionsSnapshot = copy + t.snapMu.Unlock() +} + +// ReadSnapshot returns a copy of the last display snapshot — never locks t.mu. +// Safe to call from any goroutine without impacting trading latency. +func (t *Trader) ReadSnapshot() []ArbPosition { + t.snapMu.RLock() + defer t.snapMu.RUnlock() + r := make([]ArbPosition, len(t.positionsSnapshot)) + copy(r, t.positionsSnapshot) + return r +} + // Trader handles scalable arbitrage between Bitget and HyperLiquid. type Trader struct { cfg *Config @@ -102,6 +122,10 @@ type Trader struct { closedTrades []TradeRecord // history of closed trades OnTradeEvent func(event string, data interface{}) // P3-4: real-time SSE push + + // Decoupled snapshot for display — snapMu never contended by trading path + snapMu sync.RWMutex + positionsSnapshot []ArbPosition } // TradeRecord stores a finalized trade for stats tracking.