mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-10 22:36:58 +08:00
Applied safe.GoNamed to: - 9 exchange order_sync goroutines (OKX, Hyperliquid, Aster, Bybit, KuCoin, Gate, Bitget, Lighter, Binance×2) - Drawdown monitor (auto_trader_risk.go) - Brain news scanner + market briefs (agent/brain.go) - Sentinel scanner (agent/sentinel.go) - Agent scheduler (agent/scheduler.go) - x402 idle watchdog (mcp/payment/x402.go) - MCP stream idle watchdog (mcp/client.go) - Rate limiter cleanup (api/rate_limiter.go) - 3 telemetry fire-and-forget sends (telemetry/experience.go) - CoinAnk WS handler (provider/coinank/coinank_api/kline_ws.go) - API server goroutine (main.go) Added manual defer/recover with error reporting to: - Telegram AI agent handler (sends error msg to user on panic) - Trader data fetch (returns error result on panic to prevent deadlock) Before: a panic in ANY of these 27 goroutines would crash the entire trading process with zero diagnostics. Now all panics are caught, logged with stack traces, and the process continues running.
205 lines
5.0 KiB
Go
205 lines
5.0 KiB
Go
package coinank_api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"nofx/provider/coinank"
|
|
"nofx/provider/coinank/coinank_enum"
|
|
"nofx/safe"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/net/websocket"
|
|
)
|
|
|
|
const MainWsUrl = "wss://ws.coinank.com/ws"
|
|
|
|
type KlineWs struct {
|
|
conn *websocket.Conn
|
|
KlineCh <-chan *WsResult[coinank.KlineResult]
|
|
TickersCh <-chan *WsResult[KlineTickers]
|
|
}
|
|
|
|
// WsConn connect ws , read data from KlineCh and TickersCh
|
|
func WsConn(ctx context.Context, needKline bool, needTicker bool) (*KlineWs, error) {
|
|
conn, ch, err := ws(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
klineCh, tickersCh := handleResponse(ch, needKline, needTicker)
|
|
ws := &KlineWs{
|
|
conn: conn,
|
|
KlineCh: klineCh,
|
|
TickersCh: tickersCh,
|
|
}
|
|
return ws, nil
|
|
}
|
|
|
|
// Subscribe subscribe kline
|
|
func (ws *KlineWs) Subscribe(symbol string, exchange coinank_enum.Exchange, interval coinank_enum.Interval) error {
|
|
var args = "kline@" + symbol + "@" + string(exchange) + "@" + string(interval)
|
|
info := SubscribeInfo{
|
|
Op: "subscribe",
|
|
Args: args,
|
|
}
|
|
json, err := json.Marshal(info)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = websocket.Message.Send(ws.conn, json)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UnSubscribe unsubscribe kline
|
|
func (ws *KlineWs) UnSubscribe(symbol string, exchange coinank_enum.Exchange, interval coinank_enum.Interval) error {
|
|
var args = "kline@" + symbol + "@" + string(exchange) + "@" + string(interval)
|
|
info := SubscribeInfo{
|
|
Op: "unsubscribe",
|
|
Args: args,
|
|
}
|
|
json, err := json.Marshal(info)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = websocket.Message.Send(ws.conn, json)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Close websocket
|
|
func (ws *KlineWs) Close() error {
|
|
return ws.conn.Close()
|
|
}
|
|
|
|
func ws(ctx context.Context) (*websocket.Conn, <-chan string, error) {
|
|
config, err := websocket.NewConfig(MainWsUrl, "http://localhost")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
conn, err := config.DialContext(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
ch := make(chan string, 1024)
|
|
go read(conn, ch)
|
|
return conn, ch, nil
|
|
}
|
|
|
|
func read(conn *websocket.Conn, ch chan string) {
|
|
defer conn.Close()
|
|
defer close(ch)
|
|
for {
|
|
var msg string
|
|
err := websocket.Message.Receive(conn, &msg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
ch <- msg
|
|
}
|
|
}
|
|
|
|
func handleResponse(ch <-chan string, needKline bool, needTicker bool) (<-chan *WsResult[coinank.KlineResult], <-chan *WsResult[KlineTickers]) {
|
|
klineCh := make(chan *WsResult[coinank.KlineResult], 1024)
|
|
tickersCh := make(chan *WsResult[KlineTickers], 1024)
|
|
safe.GoNamed("coinank-ws-handler", func() {
|
|
if needKline {
|
|
defer close(klineCh)
|
|
} else {
|
|
close(klineCh)
|
|
}
|
|
if needTicker {
|
|
defer close(tickersCh)
|
|
} else {
|
|
close(tickersCh)
|
|
}
|
|
for msg := range ch {
|
|
if needKline && strings.HasPrefix(msg, "{\"op\":\"push\",\"success\":true,\"args\":\"kline") {
|
|
var result WsResult[[]any]
|
|
err := json.Unmarshal([]byte(msg), &result)
|
|
if err == nil && result.Success {
|
|
kline := coinank.KlineResult{}
|
|
k := result.Data
|
|
kline.StartTime = toInt64(k[0])
|
|
kline.EndTime = toInt64(k[1])
|
|
kline.Open = toFloat64(k[2])
|
|
kline.Close = toFloat64(k[3])
|
|
kline.High = toFloat64(k[4])
|
|
kline.Low = toFloat64(k[5])
|
|
kline.Volume = toFloat64(k[6])
|
|
kline.Quantity = toFloat64(k[7])
|
|
kline.Count = toFloat64(k[8])
|
|
var resp WsResult[coinank.KlineResult]
|
|
resp.Success = result.Success
|
|
resp.Data = kline
|
|
resp.Args = result.Args
|
|
resp.Op = result.Op
|
|
klineCh <- &resp
|
|
}
|
|
} else if needTicker && strings.HasPrefix(msg, "{\"op\":\"push\",\"success\":true,\"args\":\"tickers") {
|
|
var result WsResult[KlineTickers]
|
|
err := json.Unmarshal([]byte(msg), &result)
|
|
if err == nil && result.Success {
|
|
tickersCh <- &result
|
|
}
|
|
}
|
|
}
|
|
})
|
|
return klineCh, tickersCh
|
|
}
|
|
|
|
func toInt64(v any) int64 {
|
|
f := toFloat64(v)
|
|
return int64(f)
|
|
}
|
|
|
|
func toFloat64(v any) float64 {
|
|
if f, ok := v.(float64); ok {
|
|
return f
|
|
}
|
|
if f, ok := v.(string); ok {
|
|
s, err := strconv.ParseFloat(f, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return s
|
|
}
|
|
return 0
|
|
}
|
|
|
|
type SubscribeInfo struct {
|
|
Op string `json:"op"`
|
|
Args string `json:"args"`
|
|
}
|
|
|
|
type KlineTickers struct {
|
|
OiCcy string `json:"oiCcy"`
|
|
OiVol string `json:"oiVol"`
|
|
Symbol string `json:"symbol"`
|
|
ExchangeName string `json:"exchangeName"`
|
|
PriceChange24H string `json:"priceChange24h"`
|
|
Low24H string `json:"low24h"`
|
|
High24H string `json:"high24h"`
|
|
VolCcy24H string `json:"volCcy24h"`
|
|
LastPrice string `json:"lastPrice"`
|
|
Vol24H string `json:"vol24h"`
|
|
Turnover24H string `json:"turnover24h"`
|
|
OiUSD string `json:"oiUSD"`
|
|
FundingRate string `json:"fundingRate"`
|
|
LastOiVol string `json:"lastOiVol"`
|
|
MarkPrice string `json:"markPrice"`
|
|
BasisRate string `json:"basisRate"`
|
|
Basis string `json:"basis"`
|
|
}
|
|
|
|
type WsResult[T any] struct {
|
|
Op string `json:"op"`
|
|
Success bool `json:"success"`
|
|
Args string `json:"args"`
|
|
Data T `json:"data"`
|
|
}
|