diff --git a/main.go b/main.go index 19a9d9a..c610a92 100644 --- a/main.go +++ b/main.go @@ -51,6 +51,9 @@ func main() { dashboard := NewDashboard(store, trader, database, ":8888") go dashboard.Run() + // Spread window tracker — measures how long spreads stay above threshold + spreadTracker := NewSpreadWindowTracker() + // P3-4: wire real-time trade event broadcast trader.OnTradeEvent = dashboard.BroadcastEvent if trader.IsConfigured() { @@ -170,6 +173,10 @@ func main() { dashboard.UpdateScan(makerOpps) t2 := time.Now() + // Track spread window durations (how long each opportunity stays alive) + snap := store.GetAll() + spreadTracker.Tick(snap, cfg.TradeThreshold) + for _, opp := range makerOpps { if opp.NetProfit < cfg.ArbThreshold { continue diff --git a/types.go b/types.go index a08156d..d7d2d7e 100644 --- a/types.go +++ b/types.go @@ -1,6 +1,7 @@ package main import ( + "log" "sync" "time" ) @@ -119,3 +120,69 @@ type ArbOpportunity struct { NetProfit float64 // percentage after fees GrossBasis float64 // raw price difference % } + +// SpreadWindow tracks how long each coin's spread stays above threshold. +// Used to measure the window of opportunity between threshold-crossing and +// convergence — helps diagnose whether entry latency is a problem. +type SpreadWindow struct { + Coin string + Direction string // "BG->HL" or "HL->BG" + Since time.Time +} + +type SpreadWindowTracker struct { + windows map[string]*SpreadWindow // key: "COIN:DIRECTION" +} + +func NewSpreadWindowTracker() *SpreadWindowTracker { + return &SpreadWindowTracker{windows: make(map[string]*SpreadWindow)} +} + +func (swt *SpreadWindowTracker) Tick(snap map[string]map[string]float64, threshold float64) { + now := time.Now() + for _, coin := range TrackedCoins { + exMap := snap[coin.Name] + if exMap == nil { + continue + } + bgP := exMap[ExBitget] + hlP := exMap[ExHyperLiquid] + if bgP <= 0 || hlP <= 0 { + continue + } + + // Check both directions + for _, dir := range []struct { + name string + low float64 + high float64 + }{ + {"BG->HL", bgP, hlP}, + {"HL->BG", hlP, bgP}, + } { + key := coin.Name + ":" + dir.name + spread := (dir.high - dir.low) / dir.low * 100 + netSpr := spread - (makerFees[ExBitget] + makerFees[ExHyperLiquid]) // rough net + + w, exists := swt.windows[key] + if netSpr >= threshold { + if !exists { + swt.windows[key] = &SpreadWindow{ + Coin: coin.Name, + Direction: dir.name, + Since: now, + } + } + } else { + if exists { + dur := now.Sub(w.Since) + if dur > 100*time.Millisecond { + log.Printf("[SpreadWindow] %s %s exceeded threshold for %v (peak net≈%.4f%%)", + w.Coin, w.Direction, dur.Round(time.Millisecond), netSpr) + } + delete(swt.windows, key) + } + } + } + } +}