package exchange import ( "encoding/json" "fmt" "log" "strconv" "strings" "time" ) type BinanceWS struct { Tracked []string } func NewBinanceWS(tracked []string) *BinanceWS { return &BinanceWS{Tracked: tracked} } // Run connects to Binance WS and streams bookTicker data. func (b *BinanceWS) Run(updateFn func(coin string, price, bid, ask float64)) error { streams := "" for i, sym := range b.Tracked { if i > 0 { streams += "/" } streams += fmt.Sprintf("%s@bookTicker", strings.ToLower(sym)) } url := fmt.Sprintf("wss://fstream.binance.com/stream?streams=%s", streams) conn := NewPriceConnector(url, "Binance", 120*time.Second, 30*time.Second) conn.PingInterval = 45 * time.Second conn.OnConnect = func() { log.Printf("[Binance WS] Connected") } conn.OnMessage = func(msg []byte) { // Combined stream: {"stream":"...","data":{...}} // Navigate through "data" using map to avoid field name conflicts var raw map[string]json.RawMessage if err := json.Unmarshal(msg, &raw); err != nil { return } dataRaw, ok := raw["data"] if !ok { return } // Parse data object as flat map to extract fields by exact name var dataMap map[string]interface{} if err := json.Unmarshal(dataRaw, &dataMap); err != nil { return } symbol, _ := dataMap["s"].(string) bidStr, _ := dataMap["b"].(string) askStr, _ := dataMap["a"].(string) if symbol == "" || bidStr == "" || askStr == "" { return } bid, err1 := strconv.ParseFloat(bidStr, 64) ask, err2 := strconv.ParseFloat(askStr, 64) if err1 != nil || err2 != nil || bid <= 0 || ask <= 0 { return } // Extract coin name (e.g., "BTCUSDT" -> "BTC") coin := symbolToCoin(symbol, "USDT") if coin == "" { return } mid := (bid + ask) / 2.0 updateFn(coin, mid, bid, ask) } return conn.Run() }