Add SpreadWindowTracker to measure opportunity duration

- New SpreadWindowTracker in types.go watches BG↔HL spread for all
  tracked coins, both directions
- Logs duration when spread stays above trade threshold then converges
- Wired into main loop after each scan tick
- Filters sub-100ms windows as noise
This commit is contained in:
jackyu66git
2026-05-03 20:24:36 +08:00
parent 89d92b6672
commit 8ae85750b5
2 changed files with 74 additions and 0 deletions
+7
View File
@@ -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
+67
View File
@@ -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)
}
}
}
}
}