fix: Bitget WS keepalive — send text ping, not WebSocket PingMessage

Bitget v2 WS requires a text message "ping" every 30s, not a WebSocket
PingMessage control frame (opcode 0x9). Using the wrong ping type caused
a silent failure: connection stays up and subscription succeeds, but NO
ticker data is pushed — zero errors, zero reconnection logs.

Changes:
- connector.go: Add TextPing bool flag, send text 'ping' when set
- bitget.go: Set TextPing=true, handle text 'pong', add debug logging
- scanner.go: Expand to 179 overlapping Bitget+HL coins
This commit is contained in:
jackyu66git
2026-05-04 00:33:03 +08:00
parent e04a165d69
commit 2ed6ffc747
5 changed files with 45246 additions and 22 deletions
File diff suppressed because it is too large Load Diff
+34 -6
View File
@@ -47,30 +47,58 @@ func (b *BitgetWS) Run(updateFn func(coin string, price, bid, ask float64)) erro
b.Conn = NewPriceConnector(url, "Bitget", 120*time.Second, 30*time.Second)
b.Conn.PingInterval = 25 * time.Second // Bitget requires ping within 30s
b.Conn.TextPing = true // Bitget v2 expects text "ping" message
b.Conn.OnConnect = func() {
log.Printf("[Bitget WS] Connected, subscribing")
log.Printf("[Bitget WS] Connected, subscribing (%d symbols)", len(b.Tracked))
args := make([]map[string]string, 0, len(b.Tracked))
for _, sym := range b.Tracked {
args = append(args, map[string]string{
// Batch subscriptions — Bitget WS has a limit per message
batchSize := 20
for i := 0; i < len(b.Tracked); i += batchSize {
end := i + batchSize
if end > len(b.Tracked) {
end = len(b.Tracked)
}
batch := b.Tracked[i:end]
args := make([]map[string]string, len(batch))
for j, sym := range batch {
args[j] = map[string]string{
"instType": "USDT-FUTURES",
"channel": "ticker",
"instId": sym,
})
}
}
sub := map[string]interface{}{
"op": "subscribe",
"args": args,
}
if err := b.Conn.SendJSON(sub); err != nil {
log.Printf("[Bitget WS] Subscribe error: %v", err)
log.Printf("[Bitget WS] Subscribe error (batch %d): %v", i/batchSize, err)
}
}
}
b.Conn.OnMessage = func(msg []byte) {
// Handle Bitget text "pong" response
if string(msg) == "pong" {
return
}
// Check for Bitget subscription confirmation or error response
var generic map[string]interface{}
if err := json.Unmarshal(msg, &generic); err == nil {
if evt, _ := generic["event"].(string); evt == "error" {
log.Printf("[Bitget WS] Subscribe error response: %s", string(msg))
return
} else if evt == "subscribe" {
log.Printf("[Bitget WS] Subscribe confirmed: %s", string(msg))
return
}
}
var ticker bitgetTickerMsg
if err := json.Unmarshal(msg, &ticker); err != nil {
log.Printf("[Bitget WS] Unrecognized message: %s", string(msg))
return
}
if len(ticker.Data) == 0 || ticker.Data[0].LastPr == "" {
+11
View File
@@ -16,6 +16,10 @@ type PriceConnector struct {
ReconnectBase time.Duration
PingInterval time.Duration // 0 = no client-side pings
// TextPing: if true, sends text message "ping" instead of WebSocket PingMessage frame.
// Used by Bitget v2 WS which expects a JSON/text "ping" string.
TextPing bool
OnConnect func()
OnMessage func([]byte)
OnError func(error)
@@ -91,9 +95,16 @@ func (pc *PriceConnector) Run() error {
for {
select {
case <-ticker.C:
if pc.TextPing {
// Bitget v2 expects a TEXT message "ping", not WebSocket PingMessage frame
if err := c.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
return
}
} else {
if err := c.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
case <-pingStop:
return
case <-pc.done:
+9 -1
View File
@@ -76,14 +76,22 @@ func main() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGUSR1)
// Collect symbols
// Collect symbols — skip exchanges the coin isn't listed on
var bnSymbols, bgSymbols, hlSymbols, dydxSymbols []string
for _, c := range TrackedCoins {
if c.BN != "" {
bnSymbols = append(bnSymbols, c.BN)
}
if c.BG != "" {
bgSymbols = append(bgSymbols, c.BG)
}
if c.HL != "" {
hlSymbols = append(hlSymbols, c.HL)
}
if c.HL != "" {
dydxSymbols = append(dydxSymbols, c.HL)
}
}
// Start all exchange WS connections
startExchange := func(name string, runner func(func(string, float64, float64, float64)) error) {
+173
View File
@@ -24,6 +24,179 @@ var TrackedCoins = []TrackedCoin{
{Name: "OP", BN: "OPUSDT", BG: "OPUSDT", HL: "OP"},
{Name: "WIF", BN: "WIFUSDT", BG: "WIFUSDT", HL: "WIF"},
{Name: "ARB", BN: "ARBUSDT", BG: "ARBUSDT", HL: "ARB"},
{Name: "0G", BN: "", BG: "0GUSDT", HL: "0G"},
{Name: "2Z", BN: "", BG: "2ZUSDT", HL: "2Z"},
{Name: "AAVE", BN: "", BG: "AAVEUSDT", HL: "AAVE"},
{Name: "ACE", BN: "", BG: "ACEUSDT", HL: "ACE"},
{Name: "ADA", BN: "", BG: "ADAUSDT", HL: "ADA"},
{Name: "AERO", BN: "", BG: "AEROUSDT", HL: "AERO"},
{Name: "AIXBT", BN: "", BG: "AIXBTUSDT", HL: "AIXBT"},
{Name: "ALGO", BN: "", BG: "ALGOUSDT", HL: "ALGO"},
{Name: "ALT", BN: "", BG: "ALTUSDT", HL: "ALT"},
{Name: "ANIME", BN: "", BG: "ANIMEUSDT", HL: "ANIME"},
{Name: "APE", BN: "", BG: "APEUSDT", HL: "APE"},
{Name: "APT", BN: "", BG: "APTUSDT", HL: "APT"},
{Name: "AR", BN: "", BG: "ARUSDT", HL: "AR"},
{Name: "ARK", BN: "", BG: "ARKUSDT", HL: "ARK"},
{Name: "ASTER", BN: "", BG: "ASTERUSDT", HL: "ASTER"},
{Name: "ATOM", BN: "", BG: "ATOMUSDT", HL: "ATOM"},
{Name: "AVAX", BN: "", BG: "AVAXUSDT", HL: "AVAX"},
{Name: "AVNT", BN: "", BG: "AVNTUSDT", HL: "AVNT"},
{Name: "AXS", BN: "", BG: "AXSUSDT", HL: "AXS"},
{Name: "AZTEC", BN: "", BG: "AZTECUSDT", HL: "AZTEC"},
{Name: "BABY", BN: "", BG: "BABYUSDT", HL: "BABY"},
{Name: "BANANA", BN: "", BG: "BANANAUSDT", HL: "BANANA"},
{Name: "BCH", BN: "", BG: "BCHUSDT", HL: "BCH"},
{Name: "BERA", BN: "", BG: "BERAUSDT", HL: "BERA"},
{Name: "BIGTIME", BN: "", BG: "BIGTIMEUSDT", HL: "BIGTIME"},
{Name: "BIO", BN: "", BG: "BIOUSDT", HL: "BIO"},
{Name: "BLUR", BN: "", BG: "BLURUSDT", HL: "BLUR"},
{Name: "BNB", BN: "", BG: "BNBUSDT", HL: "BNB"},
{Name: "BNT", BN: "", BG: "BNTUSDT", HL: "BNT"},
{Name: "BOME", BN: "", BG: "BOMEUSDT", HL: "BOME"},
{Name: "BRETT", BN: "", BG: "BRETTUSDT", HL: "BRETT"},
{Name: "BSV", BN: "", BG: "BSVUSDT", HL: "BSV"},
{Name: "BTC", BN: "", BG: "BTCUSDT", HL: "BTC"},
{Name: "CAKE", BN: "", BG: "CAKEUSDT", HL: "CAKE"},
{Name: "CATI", BN: "", BG: "CATIUSDT", HL: "CATI"},
{Name: "CC", BN: "", BG: "CCUSDT", HL: "CC"},
{Name: "CELO", BN: "", BG: "CELOUSDT", HL: "CELO"},
{Name: "CFX", BN: "", BG: "CFXUSDT", HL: "CFX"},
{Name: "CHILLGUY", BN: "", BG: "CHILLGUYUSDT", HL: "CHILLGUY"},
{Name: "CHIP", BN: "", BG: "CHIPUSDT", HL: "CHIP"},
{Name: "COMP", BN: "", BG: "COMPUSDT", HL: "COMP"},
{Name: "CRV", BN: "", BG: "CRVUSDT", HL: "CRV"},
{Name: "CYBER", BN: "", BG: "CYBERUSDT", HL: "CYBER"},
{Name: "DASH", BN: "", BG: "DASHUSDT", HL: "DASH"},
{Name: "DOOD", BN: "", BG: "DOODUSDT", HL: "DOOD"},
{Name: "DOT", BN: "", BG: "DOTUSDT", HL: "DOT"},
{Name: "DYDX", BN: "", BG: "DYDXUSDT", HL: "DYDX"},
{Name: "DYM", BN: "", BG: "DYMUSDT", HL: "DYM"},
{Name: "EIGEN", BN: "", BG: "EIGENUSDT", HL: "EIGEN"},
{Name: "ENA", BN: "", BG: "ENAUSDT", HL: "ENA"},
{Name: "ENS", BN: "", BG: "ENSUSDT", HL: "ENS"},
{Name: "ETC", BN: "", BG: "ETCUSDT", HL: "ETC"},
{Name: "ETH", BN: "", BG: "ETHUSDT", HL: "ETH"},
{Name: "ETHFI", BN: "", BG: "ETHFIUSDT", HL: "ETHFI"},
{Name: "FARTCOIN", BN: "", BG: "FARTCOINUSDT", HL: "FARTCOIN"},
{Name: "FET", BN: "", BG: "FETUSDT", HL: "FET"},
{Name: "FIL", BN: "", BG: "FILUSDT", HL: "FIL"},
{Name: "FOGO", BN: "", BG: "FOGOUSDT", HL: "FOGO"},
{Name: "GALA", BN: "", BG: "GALAUSDT", HL: "GALA"},
{Name: "GAS", BN: "", BG: "GASUSDT", HL: "GAS"},
{Name: "GMT", BN: "", BG: "GMTUSDT", HL: "GMT"},
{Name: "GMX", BN: "", BG: "GMXUSDT", HL: "GMX"},
{Name: "GOAT", BN: "", BG: "GOATUSDT", HL: "GOAT"},
{Name: "GRASS", BN: "", BG: "GRASSUSDT", HL: "GRASS"},
{Name: "GRIFFAIN", BN: "", BG: "GRIFFAINUSDT", HL: "GRIFFAIN"},
{Name: "HBAR", BN: "", BG: "HBARUSDT", HL: "HBAR"},
{Name: "HYPE", BN: "", BG: "HYPEUSDT", HL: "HYPE"},
{Name: "HYPER", BN: "", BG: "HYPERUSDT", HL: "HYPER"},
{Name: "ICP", BN: "", BG: "ICPUSDT", HL: "ICP"},
{Name: "ILV", BN: "", BG: "ILVUSDT", HL: "ILV"},
{Name: "IMX", BN: "", BG: "IMXUSDT", HL: "IMX"},
{Name: "INIT", BN: "", BG: "INITUSDT", HL: "INIT"},
{Name: "INJ", BN: "", BG: "INJUSDT", HL: "INJ"},
{Name: "IO", BN: "", BG: "IOUSDT", HL: "IO"},
{Name: "IOTA", BN: "", BG: "IOTAUSDT", HL: "IOTA"},
{Name: "IP", BN: "", BG: "IPUSDT", HL: "IP"},
{Name: "JTO", BN: "", BG: "JTOUSDT", HL: "JTO"},
{Name: "JUP", BN: "", BG: "JUPUSDT", HL: "JUP"},
{Name: "KAITO", BN: "", BG: "KAITOUSDT", HL: "KAITO"},
{Name: "KAS", BN: "", BG: "KASUSDT", HL: "KAS"},
{Name: "LAYER", BN: "", BG: "LAYERUSDT", HL: "LAYER"},
{Name: "LDO", BN: "", BG: "LDOUSDT", HL: "LDO"},
{Name: "LINEA", BN: "", BG: "LINEAUSDT", HL: "LINEA"},
{Name: "LISTA", BN: "", BG: "LISTAUSDT", HL: "LISTA"},
{Name: "LIT", BN: "", BG: "LITUSDT", HL: "LIT"},
{Name: "LTC", BN: "", BG: "LTCUSDT", HL: "LTC"},
{Name: "MANTA", BN: "", BG: "MANTAUSDT", HL: "MANTA"},
{Name: "MAV", BN: "", BG: "MAVUSDT", HL: "MAV"},
{Name: "ME", BN: "", BG: "MEUSDT", HL: "ME"},
{Name: "MEGA", BN: "", BG: "MEGAUSDT", HL: "MEGA"},
{Name: "MELANIA", BN: "", BG: "MELANIAUSDT", HL: "MELANIA"},
{Name: "MEME", BN: "", BG: "MEMEUSDT", HL: "MEME"},
{Name: "MERL", BN: "", BG: "MERLUSDT", HL: "MERL"},
{Name: "MET", BN: "", BG: "METUSDT", HL: "MET"},
{Name: "MEW", BN: "", BG: "MEWUSDT", HL: "MEW"},
{Name: "MINA", BN: "", BG: "MINAUSDT", HL: "MINA"},
{Name: "MON", BN: "", BG: "MONUSDT", HL: "MON"},
{Name: "MOODENG", BN: "", BG: "MOODENGUSDT", HL: "MOODENG"},
{Name: "MORPHO", BN: "", BG: "MORPHOUSDT", HL: "MORPHO"},
{Name: "MOVE", BN: "", BG: "MOVEUSDT", HL: "MOVE"},
{Name: "NEAR", BN: "", BG: "NEARUSDT", HL: "NEAR"},
{Name: "NEO", BN: "", BG: "NEOUSDT", HL: "NEO"},
{Name: "NIL", BN: "", BG: "NILUSDT", HL: "NIL"},
{Name: "NOT", BN: "", BG: "NOTUSDT", HL: "NOT"},
{Name: "NXPC", BN: "", BG: "NXPCUSDT", HL: "NXPC"},
{Name: "OGN", BN: "", BG: "OGNUSDT", HL: "OGN"},
{Name: "ORDI", BN: "", BG: "ORDIUSDT", HL: "ORDI"},
{Name: "PAXG", BN: "", BG: "PAXGUSDT", HL: "PAXG"},
{Name: "PENDLE", BN: "", BG: "PENDLEUSDT", HL: "PENDLE"},
{Name: "PENGU", BN: "", BG: "PENGUUSDT", HL: "PENGU"},
{Name: "PEOPLE", BN: "", BG: "PEOPLEUSDT", HL: "PEOPLE"},
{Name: "PIXEL", BN: "", BG: "PIXELUSDT", HL: "PIXEL"},
{Name: "PNUT", BN: "", BG: "PNUTUSDT", HL: "PNUT"},
{Name: "POL", BN: "", BG: "POLUSDT", HL: "POL"},
{Name: "POLYX", BN: "", BG: "POLYXUSDT", HL: "POLYX"},
{Name: "POPCAT", BN: "", BG: "POPCATUSDT", HL: "POPCAT"},
{Name: "PROVE", BN: "", BG: "PROVEUSDT", HL: "PROVE"},
{Name: "PUMP", BN: "", BG: "PUMPUSDT", HL: "PUMP"},
{Name: "PYTH", BN: "", BG: "PYTHUSDT", HL: "PYTH"},
{Name: "RENDER", BN: "", BG: "RENDERUSDT", HL: "RENDER"},
{Name: "RESOLV", BN: "", BG: "RESOLVUSDT", HL: "RESOLV"},
{Name: "REZ", BN: "", BG: "REZUSDT", HL: "REZ"},
{Name: "RSR", BN: "", BG: "RSRUSDT", HL: "RSR"},
{Name: "RUNE", BN: "", BG: "RUNEUSDT", HL: "RUNE"},
{Name: "S", BN: "", BG: "SUSDT", HL: "S"},
{Name: "SAGA", BN: "", BG: "SAGAUSDT", HL: "SAGA"},
{Name: "SAND", BN: "", BG: "SANDUSDT", HL: "SAND"},
{Name: "SEI", BN: "", BG: "SEIUSDT", HL: "SEI"},
{Name: "SKR", BN: "", BG: "SKRUSDT", HL: "SKR"},
{Name: "SKY", BN: "", BG: "SKYUSDT", HL: "SKY"},
{Name: "SNX", BN: "", BG: "SNXUSDT", HL: "SNX"},
{Name: "SOL", BN: "", BG: "SOLUSDT", HL: "SOL"},
{Name: "SOPH", BN: "", BG: "SOPHUSDT", HL: "SOPH"},
{Name: "SPX", BN: "", BG: "SPXUSDT", HL: "SPX"},
{Name: "STABLE", BN: "", BG: "STABLEUSDT", HL: "STABLE"},
{Name: "STBL", BN: "", BG: "STBLUSDT", HL: "STBL"},
{Name: "STG", BN: "", BG: "STGUSDT", HL: "STG"},
{Name: "STRK", BN: "", BG: "STRKUSDT", HL: "STRK"},
{Name: "STX", BN: "", BG: "STXUSDT", HL: "STX"},
{Name: "SUI", BN: "", BG: "SUIUSDT", HL: "SUI"},
{Name: "SUPER", BN: "", BG: "SUPERUSDT", HL: "SUPER"},
{Name: "SUSHI", BN: "", BG: "SUSHIUSDT", HL: "SUSHI"},
{Name: "SYRUP", BN: "", BG: "SYRUPUSDT", HL: "SYRUP"},
{Name: "TAO", BN: "", BG: "TAOUSDT", HL: "TAO"},
{Name: "TIA", BN: "", BG: "TIAUSDT", HL: "TIA"},
{Name: "TNSR", BN: "", BG: "TNSRUSDT", HL: "TNSR"},
{Name: "TON", BN: "", BG: "TONUSDT", HL: "TON"},
{Name: "TRB", BN: "", BG: "TRBUSDT", HL: "TRB"},
{Name: "TRUMP", BN: "", BG: "TRUMPUSDT", HL: "TRUMP"},
{Name: "TRX", BN: "", BG: "TRXUSDT", HL: "TRX"},
{Name: "TURBO", BN: "", BG: "TURBOUSDT", HL: "TURBO"},
{Name: "UMA", BN: "", BG: "UMAUSDT", HL: "UMA"},
{Name: "UNI", BN: "", BG: "UNIUSDT", HL: "UNI"},
{Name: "USTC", BN: "", BG: "USTCUSDT", HL: "USTC"},
{Name: "USUAL", BN: "", BG: "USUALUSDT", HL: "USUAL"},
{Name: "VIRTUAL", BN: "", BG: "VIRTUALUSDT", HL: "VIRTUAL"},
{Name: "VVV", BN: "", BG: "VVVUSDT", HL: "VVV"},
{Name: "W", BN: "", BG: "WUSDT", HL: "W"},
{Name: "WCT", BN: "", BG: "WCTUSDT", HL: "WCT"},
{Name: "WLD", BN: "", BG: "WLDUSDT", HL: "WLD"},
{Name: "WLFI", BN: "", BG: "WLFIUSDT", HL: "WLFI"},
{Name: "XAI", BN: "", BG: "XAIUSDT", HL: "XAI"},
{Name: "XLM", BN: "", BG: "XLMUSDT", HL: "XLM"},
{Name: "XMR", BN: "", BG: "XMRUSDT", HL: "XMR"},
{Name: "XPL", BN: "", BG: "XPLUSDT", HL: "XPL"},
{Name: "XRP", BN: "", BG: "XRPUSDT", HL: "XRP"},
{Name: "YGG", BN: "", BG: "YGGUSDT", HL: "YGG"},
{Name: "ZEC", BN: "", BG: "ZECUSDT", HL: "ZEC"},
{Name: "ZEN", BN: "", BG: "ZENUSDT", HL: "ZEN"},
{Name: "ZETA", BN: "", BG: "ZETAUSDT", HL: "ZETA"},
{Name: "ZK", BN: "", BG: "ZKUSDT", HL: "ZK"},
{Name: "ZORA", BN: "", BG: "ZORAUSDT", HL: "ZORA"},
{Name: "ZRO", BN: "", BG: "ZROUSDT", HL: "ZRO"},
}
// netProfit calculates net profit % after fees for a complete round trip (entry + exit).