You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
276 lines
7.7 KiB
276 lines
7.7 KiB
package okx |
|
|
|
import ( |
|
"encoding/json" |
|
"errors" |
|
"fmt" |
|
"net/http" |
|
"net/url" |
|
"sig-pub/pkg/utils/collect" |
|
"sig-pub/pkg/zlog" |
|
"sync" |
|
"time" |
|
|
|
"github.com/bytedance/sonic" |
|
"github.com/gorilla/websocket" |
|
"golang.org/x/net/http/httpproxy" |
|
) |
|
|
|
// https://my.okx.com/docs-v5/zh/#overview-websocket-overview |
|
// 连接限制:3 次/秒 (基于IP) |
|
// 当订阅公有频道时,使用公有服务的地址;当订阅私有频道时,使用私有服务的地址 |
|
// 请求限制:每个连接 对于 订阅/取消订阅/登录 请求的总次数限制为 480 次/小时 |
|
|
|
// 公共频道无需登录,包括行情频道,K线频道,交易数据频道,资金费率频道,限价范围频道,深度数据频道,标记价格频道等。 |
|
// 私有频道需登录,包括用户账户频道,用户交易频道,用户持仓频道等。 |
|
|
|
const ( |
|
WsBaseUrl = "wss://ws.okx.com:8443" |
|
// WsBaseUrl = "wss://wseeapap.okx.com:8443" // 模拟盘 |
|
) |
|
|
|
// SubscribeStatus 订阅状态 |
|
type SubscribeStatus int32 |
|
|
|
const ( |
|
StatusUnsubscribe SubscribeStatus = 0 // 未订阅 |
|
StatusSubscribing SubscribeStatus = 1 // 订阅中 |
|
StatusSubscribed SubscribeStatus = 2 // 已订阅 |
|
) |
|
|
|
// WebSocketEvent okx推送事件 |
|
type WebSocketEvent struct { |
|
// 订阅数据 |
|
Event ChannelEventType `json:"event"` |
|
Code string `json:"code,omitempty"` |
|
Msg string `json:"msg,omitempty"` |
|
ConnId string `json:"connId,omitempty"` |
|
// 推送数据 |
|
Arg WebSocketEventArg `json:"arg,omitempty"` |
|
Action ActionType `json:"action"` // books交易深度订阅,推送数据动作类型 |
|
Data json.RawMessage `json:"data"` |
|
} |
|
|
|
type WebSocketEventArg struct { |
|
Channel string `json:"channel"` // 订阅的频道 candle1s/candle1m/candle3M |
|
InstId string `json:"instId"` // 产品ID |
|
} |
|
|
|
// 订阅配置 |
|
type wsChannelConfig[T, R any] struct { |
|
channelId string // ws日志id |
|
httpProxy string // http 代理 |
|
wsUrl string // 服务地址, K线-> /ws/v5/business |
|
subscribeChannels []string // 订阅频道列表 |
|
dataInstanceFunc func() T // 生成一个data实例用以反序列化 |
|
dataMappingFunc func(*ChannelData[T]) (R, error) // 数据映射函数 |
|
} |
|
|
|
// okx websocket 频道订阅 |
|
// 行情订阅 todo 单连接上限, 上百种币多个ws连接订阅 |
|
type wsChannel[T, R any] struct { |
|
sync.Mutex |
|
channelId string |
|
cfg wsChannelConfig[T, R] |
|
conn *websocket.Conn |
|
subscribeInsts *collect.ConcurrentMap[string, SubscribeStatus] // <instId, status> |
|
dataC chan R // data channel |
|
|
|
prevLogFullMs int64 |
|
// todo subscribe logs... |
|
} |
|
|
|
func newWsChannel[T, R any](cfg wsChannelConfig[T, R]) *wsChannel[T, R] { |
|
subInsts := collect.NewConcurrentMap[string, SubscribeStatus](8, func(k string) string { return k }) |
|
ms := &wsChannel[T, R]{ |
|
channelId: cfg.channelId, |
|
cfg: cfg, |
|
subscribeInsts: subInsts, |
|
dataC: make(chan R, 4*1024), // 4k |
|
} |
|
return ms |
|
} |
|
|
|
func (c *wsChannel[T, R]) Init() (err error) { |
|
if c.cfg.dataInstanceFunc == nil { |
|
return errors.New("ws channel payloadInstanceFunc can't be nil") |
|
} |
|
err = c.connect() |
|
return |
|
} |
|
|
|
func (c *wsChannel[T, R]) connect() (err error) { |
|
var proxyFunc = http.ProxyFromEnvironment |
|
if c.cfg.httpProxy != "" { |
|
// 自定义 http proxy |
|
proxyFunc = func(req *http.Request) (*url.URL, error) { |
|
return (&httpproxy.Config{ |
|
HTTPProxy: c.cfg.httpProxy, |
|
HTTPSProxy: c.cfg.httpProxy, |
|
}).ProxyFunc()(req.URL) |
|
} |
|
} |
|
wsDialer := websocket.Dialer{ |
|
Proxy: proxyFunc, |
|
HandshakeTimeout: 10 * time.Second, |
|
} |
|
|
|
url := fmt.Sprintf("%s%s", WsBaseUrl, c.cfg.wsUrl) |
|
conn, _, err := wsDialer.Dial(url, nil) |
|
if err != nil { |
|
zlog.Error("Error connecting to websocket channel:", url, err) |
|
go c.reconnect() |
|
return |
|
} |
|
c.conn = conn |
|
zlog.Infof("websocket channel %s connected: %s", c.channelId, url) |
|
|
|
go c.readPump() |
|
return |
|
} |
|
|
|
func (c *wsChannel[T, R]) reconnect() { |
|
zlog.Infof("channel %s reconnecting", c.channelId) |
|
|
|
var err error |
|
func() { |
|
c.Lock() |
|
defer c.Unlock() |
|
|
|
c.close() |
|
// wait 1sec |
|
<-time.After(time.Second) |
|
err = c.connect() |
|
}() |
|
if err != nil { |
|
return |
|
} |
|
|
|
// 重新订阅 |
|
var insts []string |
|
c.subscribeInsts.Range(func(instId string, status SubscribeStatus) bool { |
|
if status == StatusUnsubscribe { |
|
insts = append(insts, instId) |
|
} |
|
return true |
|
}) |
|
|
|
if len(insts) > 0 { |
|
err = c.Subscribe(insts...) |
|
if err != nil { |
|
zlog.Error("channel %s subscribe %v error", c.channelId, insts, err) |
|
go c.reconnect() |
|
} |
|
} |
|
} |
|
|
|
func (c *wsChannel[T, R]) close() { |
|
if c.conn != nil { |
|
err := c.conn.Close() |
|
if err != nil { |
|
zlog.Infof("channel %s close error: %v", c.channelId, err) |
|
} |
|
c.conn = nil |
|
} |
|
|
|
// 重置到待订阅状态 |
|
c.subscribeInsts.RangeUpdate(func(instId string, status SubscribeStatus) (bool, bool, SubscribeStatus) { |
|
return true, false, StatusUnsubscribe |
|
}) |
|
} |
|
|
|
// 从ws读取数据并解析 |
|
func (c *wsChannel[T, R]) readPump() { |
|
for { |
|
t, bytes, err := c.conn.ReadMessage() |
|
if err != nil { |
|
go c.reconnect() |
|
zlog.Errorf("channel %s reading message error: %v", c.channelId, err) |
|
return |
|
} |
|
switch t { |
|
default: |
|
zlog.Errorf("channel %s unknown message type: %d", c.channelId, t) |
|
continue |
|
case websocket.PingMessage: |
|
c.conn.WriteMessage(websocket.PongMessage, []byte{}) |
|
continue |
|
case websocket.CloseMessage: |
|
go c.reconnect() |
|
zlog.Infof("channel %s close message", c.channelId) |
|
return |
|
case websocket.TextMessage: |
|
} |
|
|
|
// fmt.Println(string(bytes)) |
|
var event WebSocketEvent |
|
if err = sonic.Unmarshal(bytes, &event); err != nil { |
|
zlog.Errorf("parse websocket event error: ", err) |
|
continue |
|
} |
|
|
|
switch event.Event { |
|
case ChannelEventTypeSubscribe: |
|
c.subscribeInsts.Store(event.Arg.InstId, StatusSubscribed) |
|
zlog.Infof("channel %s subscribed %s %s", c.channelId, event.Arg.InstId, event.Arg.Channel) |
|
continue |
|
case ChannelEventTypeUnsubscribe: |
|
c.subscribeInsts.Delete(event.Arg.InstId) |
|
zlog.Infof("channel %s unsubscribed %s", c.channelId, event.Arg.InstId) |
|
continue |
|
case ChannelEventTypeError: |
|
zlog.Errorf("channel %s event error: %#v", c.channelId, event) |
|
continue |
|
case ChannelEventTypeConnectionInfo: // 新链接订阅频道时, 消息同步链接数量 |
|
zlog.Infof("channel-conn-count event %s: %#v", c.channelId, event) |
|
continue |
|
case ChannelEventTypeConnectionError: // 当超出限制时 |
|
zlog.Errorf("channel-conn-count-error event %s: %#v", c.channelId, event) |
|
continue |
|
case ChannelEventTypeNotice: // websocket 服务升级断线通知 |
|
zlog.Errorf("channel event type notice %s: %#v", c.channelId, event) |
|
continue |
|
// case WsEventTypeLogin: // todo |
|
default: |
|
// 推送数据 |
|
} |
|
if event.Event != "" { |
|
zlog.Infof("unhandle event %s: %#v", c.channelId, event) |
|
continue |
|
} |
|
|
|
// 解析数据 |
|
data := c.cfg.dataInstanceFunc() |
|
if err := sonic.Unmarshal(event.Data, data); err != nil { |
|
zlog.Error("unmarshal event data error: ", err) |
|
continue |
|
} |
|
// zlog.Infof("read data: %v", data) |
|
|
|
// 数据打包 |
|
channelData := &ChannelData[T]{ |
|
Channel: event.Arg.Channel, |
|
InstId: event.Arg.InstId, |
|
Action: event.Action, |
|
Data: data, |
|
} |
|
|
|
// 数据映射 |
|
dataR, err := c.cfg.dataMappingFunc(channelData) |
|
if err != nil { |
|
zlog.Error("okx channel data mapping error: ", err) |
|
continue |
|
} |
|
|
|
select { |
|
case c.dataC <- dataR: |
|
default: |
|
// channel 满了 |
|
now := time.Now().UnixMilli() |
|
if now-c.prevLogFullMs > 5000 { // 每5秒打印不要太频繁 |
|
c.prevLogFullMs = now |
|
zlog.Warningf("DataC %s full, sub %d insts", c.channelId, c.subscribeInsts.Size()) |
|
} |
|
} |
|
} |
|
}
|
|
|