├── LICENSE ├── config └── conf.go ├── go.mod ├── go.sum ├── main.go ├── readme.md ├── rest ├── contants.go ├── rest.go └── rest_test.go ├── utils ├── utils.go └── utils_test.go └── ws ├── utils.go ├── wImpl ├── BookData.go ├── ErrData.go ├── JRPCData.go ├── ReqData.go ├── contants.go └── contants_test.go ├── wInterface ├── IParam.go ├── IReqData.go └── IRspData.go ├── ws_cli.go ├── ws_contants.go ├── ws_jrpc.go ├── ws_jrpc_test.go ├── ws_middleware.go ├── ws_op.go ├── ws_priv_channel.go ├── ws_priv_channel_test.go ├── ws_pub_channel.go ├── ws_pub_channel_test.go └── ws_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/conf.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "fmt" 4 | 5 | type Env struct { 6 | RestEndpoint string `yaml:"RestEndpoint"` 7 | WsEndpoint string `yaml:"WsEndpoint"` 8 | IsSimulation bool `yaml:"IsSimulation"` 9 | } 10 | 11 | type ApiInfo struct { 12 | ApiKey string `yaml:"ApiKey"` 13 | SecretKey string `yaml:"SecretKey"` 14 | Passphrase string `yaml:"Passphrase"` 15 | } 16 | 17 | type MetaData struct { 18 | Description string `yaml:"Description"` 19 | } 20 | 21 | type Config struct { 22 | MetaData `yaml:"MetaData"` 23 | Env `yaml:"Env"` 24 | ApiInfo `yaml:"ApiInfo"` 25 | } 26 | 27 | func (s *ApiInfo) String() string { 28 | res := "ApiInfo{" 29 | res += fmt.Sprintf("ApiKey:%v,SecretKey:%v,Passphrase:%v", s.ApiKey, s.SecretKey, s.Passphrase) 30 | res += "}" 31 | return res 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module v5sdk_go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.4.2 7 | github.com/stretchr/testify v1.7.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 4 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 9 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | . "v5sdk_go/rest" 9 | . "v5sdk_go/ws" 10 | ) 11 | 12 | /* 13 | rest API请求 14 | 更多示例请查看 rest/rest_test.go 15 | */ 16 | func REST() { 17 | // 设置您的APIKey 18 | apikey := APIKeyInfo{ 19 | ApiKey: "xxxx", 20 | SecKey: "xxxx", 21 | PassPhrase: "xxxx", 22 | } 23 | 24 | // 第三个参数代表是否为模拟环境,更多信息查看接口说明 25 | cli := NewRESTClient("https://www.okex.win", &apikey, true) 26 | rsp, err := cli.Get(context.Background(), "/api/v5/account/balance", nil) 27 | if err != nil { 28 | return 29 | } 30 | 31 | fmt.Println("Response:") 32 | fmt.Println("\thttp code: ", rsp.Code) 33 | fmt.Println("\t总耗时: ", rsp.TotalUsedTime) 34 | fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) 35 | fmt.Println("\t返回消息: ", rsp.Body) 36 | fmt.Println("\terrCode: ", rsp.V5Response.Code) 37 | fmt.Println("\terrMsg: ", rsp.V5Response.Msg) 38 | fmt.Println("\tdata: ", rsp.V5Response.Data) 39 | 40 | } 41 | 42 | // 订阅私有频道 43 | func wsPriv() { 44 | ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" 45 | 46 | // 填写您自己的APIKey信息 47 | apikey := "xxxx" 48 | secretKey := "xxxxx" 49 | passphrase := "xxxxx" 50 | 51 | // 创建ws客户端 52 | r, err := NewWsClient(ep) 53 | if err != nil { 54 | log.Println(err) 55 | return 56 | } 57 | 58 | // 设置连接超时 59 | r.SetDailTimeout(time.Second * 2) 60 | err = r.Start() 61 | if err != nil { 62 | log.Println(err) 63 | return 64 | } 65 | defer r.Stop() 66 | var res bool 67 | 68 | res, _, err = r.Login(apikey, secretKey, passphrase) 69 | if res { 70 | fmt.Println("登录成功!") 71 | } else { 72 | fmt.Println("登录失败!", err) 73 | return 74 | } 75 | 76 | // 订阅账户频道 77 | var args []map[string]string 78 | arg := make(map[string]string) 79 | arg["ccy"] = "BTC" 80 | args = append(args, arg) 81 | 82 | start := time.Now() 83 | res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) 84 | if res { 85 | usedTime := time.Since(start) 86 | fmt.Println("订阅成功!耗时:", usedTime.String()) 87 | } else { 88 | fmt.Println("订阅失败!", err) 89 | } 90 | 91 | time.Sleep(100 * time.Second) 92 | start = time.Now() 93 | res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) 94 | if res { 95 | usedTime := time.Since(start) 96 | fmt.Println("取消订阅成功!", usedTime.String()) 97 | } else { 98 | fmt.Println("取消订阅失败!", err) 99 | } 100 | 101 | } 102 | 103 | // 订阅公共频道 104 | func wsPub() { 105 | ep := "wss://ws.okex.com:8443/ws/v5/public?brokerId=9999" 106 | 107 | // 创建ws客户端 108 | r, err := NewWsClient(ep) 109 | if err != nil { 110 | log.Println(err) 111 | return 112 | } 113 | 114 | // 设置连接超时 115 | r.SetDailTimeout(time.Second * 2) 116 | err = r.Start() 117 | if err != nil { 118 | log.Println(err) 119 | return 120 | } 121 | defer r.Stop() 122 | // 订阅产品频道 123 | var args []map[string]string 124 | arg := make(map[string]string) 125 | arg["instType"] = FUTURES 126 | //arg["instType"] = OPTION 127 | args = append(args, arg) 128 | 129 | start := time.Now() 130 | res, _, err := r.PubInstruemnts(OP_SUBSCRIBE, args) 131 | if res { 132 | usedTime := time.Since(start) 133 | fmt.Println("订阅成功!", usedTime.String()) 134 | } else { 135 | fmt.Println("订阅失败!", err) 136 | } 137 | 138 | time.Sleep(30 * time.Second) 139 | 140 | start = time.Now() 141 | res, _, err = r.PubInstruemnts(OP_UNSUBSCRIBE, args) 142 | if res { 143 | usedTime := time.Since(start) 144 | fmt.Println("取消订阅成功!", usedTime.String()) 145 | } else { 146 | fmt.Println("取消订阅失败!", err) 147 | } 148 | } 149 | 150 | // websocket交易 151 | func wsJrpc() { 152 | ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" 153 | 154 | // 填写您自己的APIKey信息 155 | apikey := "xxxx" 156 | secretKey := "xxxxx" 157 | passphrase := "xxxxx" 158 | 159 | var res bool 160 | var req_id string 161 | 162 | // 创建ws客户端 163 | r, err := NewWsClient(ep) 164 | if err != nil { 165 | log.Println(err) 166 | return 167 | } 168 | 169 | // 设置连接超时 170 | r.SetDailTimeout(time.Second * 2) 171 | err = r.Start() 172 | if err != nil { 173 | log.Println(err) 174 | return 175 | } 176 | 177 | defer r.Stop() 178 | 179 | res, _, err = r.Login(apikey, secretKey, passphrase) 180 | if res { 181 | fmt.Println("登录成功!") 182 | } else { 183 | fmt.Println("登录失败!", err) 184 | return 185 | } 186 | 187 | start := time.Now() 188 | param := map[string]interface{}{} 189 | param["instId"] = "BTC-USDT" 190 | param["tdMode"] = "cash" 191 | param["side"] = "buy" 192 | param["ordType"] = "market" 193 | param["sz"] = "200" 194 | req_id = "00001" 195 | 196 | res, _, err = r.PlaceOrder(req_id, param) 197 | if res { 198 | usedTime := time.Since(start) 199 | fmt.Println("下单成功!", usedTime.String()) 200 | } else { 201 | usedTime := time.Since(start) 202 | fmt.Println("下单失败!", usedTime.String(), err) 203 | } 204 | } 205 | 206 | func main() { 207 | // 公共订阅 208 | wsPub() 209 | 210 | // 私有订阅 211 | wsPriv() 212 | 213 | // websocket交易 214 | wsJrpc() 215 | 216 | // rest请求 217 | REST() 218 | } 219 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | OKEX go版本的v5sdk,仅供学习交流使用。 3 | (文档持续完善中) 4 | # 项目说明 5 | 6 | ## REST调用 7 | ``` go 8 | // 设置您的APIKey 9 | apikey := APIKeyInfo{ 10 | ApiKey: "xxxx", 11 | SecKey: "xxxx", 12 | PassPhrase: "xxxx", 13 | } 14 | 15 | // 第三个参数代表是否为模拟环境,更多信息查看接口说明 16 | cli := NewRESTClient("https://www.okex.win", &apikey, true) 17 | rsp, err := cli.Get(context.Background(), "/api/v5/account/balance", nil) 18 | if err != nil { 19 | return 20 | } 21 | 22 | fmt.Println("Response:") 23 | fmt.Println("\thttp code: ", rsp.Code) 24 | fmt.Println("\t总耗时: ", rsp.TotalUsedTime) 25 | fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) 26 | fmt.Println("\t返回消息: ", rsp.Body) 27 | fmt.Println("\terrCode: ", rsp.V5Response.Code) 28 | fmt.Println("\terrMsg: ", rsp.V5Response.Msg) 29 | fmt.Println("\tdata: ", rsp.V5Response.Data) 30 | ``` 31 | 更多示例请查看rest/rest_test.go 32 | 33 | ## websocket订阅 34 | 35 | ### 私有频道 36 | ```go 37 | ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" 38 | 39 | // 填写您自己的APIKey信息 40 | apikey := "xxxx" 41 | secretKey := "xxxxx" 42 | passphrase := "xxxxx" 43 | 44 | // 创建ws客户端 45 | r, err := NewWsClient(ep) 46 | if err != nil { 47 | log.Println(err) 48 | return 49 | } 50 | 51 | // 设置连接超时 52 | r.SetDailTimeout(time.Second * 2) 53 | err = r.Start() 54 | if err != nil { 55 | log.Println(err) 56 | return 57 | } 58 | defer r.Stop() 59 | 60 | var res bool 61 | // 私有频道需要登录 62 | res, _, err = r.Login(apikey, secretKey, passphrase) 63 | if res { 64 | fmt.Println("登录成功!") 65 | } else { 66 | fmt.Println("登录失败!", err) 67 | return 68 | } 69 | 70 | 71 | var args []map[string]string 72 | arg := make(map[string]string) 73 | arg["ccy"] = "BTC" 74 | args = append(args, arg) 75 | 76 | start := time.Now() 77 | // 订阅账户频道 78 | res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) 79 | if res { 80 | usedTime := time.Since(start) 81 | fmt.Println("订阅成功!耗时:", usedTime.String()) 82 | } else { 83 | fmt.Println("订阅失败!", err) 84 | } 85 | 86 | time.Sleep(100 * time.Second) 87 | start = time.Now() 88 | // 取消订阅账户频道 89 | res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) 90 | if res { 91 | usedTime := time.Since(start) 92 | fmt.Println("取消订阅成功!", usedTime.String()) 93 | } else { 94 | fmt.Println("取消订阅失败!", err) 95 | } 96 | ``` 97 | 更多示例请查看ws/ws_priv_channel_test.go 98 | 99 | ### 公有频道 100 | ```go 101 | ep := "wss://ws.okex.com:8443/ws/v5/public?brokerId=9999" 102 | 103 | // 创建ws客户端 104 | r, err := NewWsClient(ep) 105 | if err != nil { 106 | log.Println(err) 107 | return 108 | } 109 | 110 | 111 | // 设置连接超时 112 | r.SetDailTimeout(time.Second * 2) 113 | err = r.Start() 114 | if err != nil { 115 | log.Println(err) 116 | return 117 | } 118 | 119 | defer r.Stop() 120 | 121 | 122 | var args []map[string]string 123 | arg := make(map[string]string) 124 | arg["instType"] = FUTURES 125 | //arg["instType"] = OPTION 126 | args = append(args, arg) 127 | 128 | start := time.Now() 129 | 130 | // 订阅产品频道 131 | res, _, err := r.PubInstruemnts(OP_SUBSCRIBE, args) 132 | if res { 133 | usedTime := time.Since(start) 134 | fmt.Println("订阅成功!", usedTime.String()) 135 | } else { 136 | fmt.Println("订阅失败!", err) 137 | } 138 | 139 | time.Sleep(30 * time.Second) 140 | 141 | start = time.Now() 142 | 143 | // 取消订阅产品频道 144 | res, _, err = r.PubInstruemnts(OP_UNSUBSCRIBE, args) 145 | if res { 146 | usedTime := time.Since(start) 147 | fmt.Println("取消订阅成功!", usedTime.String()) 148 | } else { 149 | fmt.Println("取消订阅失败!", err) 150 | } 151 | ``` 152 | 更多示例请查看ws/ws_pub_channel_test.go 153 | 154 | ## websocket交易 155 | ```go 156 | ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" 157 | 158 | // 填写您自己的APIKey信息 159 | apikey := "xxxx" 160 | secretKey := "xxxxx" 161 | passphrase := "xxxxx" 162 | 163 | var res bool 164 | var req_id string 165 | 166 | // 创建ws客户端 167 | r, err := NewWsClient(ep) 168 | if err != nil { 169 | log.Println(err) 170 | return 171 | } 172 | 173 | // 设置连接超时 174 | r.SetDailTimeout(time.Second * 2) 175 | err = r.Start() 176 | if err != nil { 177 | log.Println(err) 178 | return 179 | } 180 | 181 | defer r.Stop() 182 | 183 | res, _, err = r.Login(apikey, secretKey, passphrase) 184 | if res { 185 | fmt.Println("登录成功!") 186 | } else { 187 | fmt.Println("登录失败!", err) 188 | return 189 | } 190 | 191 | start := time.Now() 192 | param := map[string]interface{}{} 193 | param["instId"] = "BTC-USDT" 194 | param["tdMode"] = "cash" 195 | param["side"] = "buy" 196 | param["ordType"] = "market" 197 | param["sz"] = "200" 198 | req_id = "00001" 199 | 200 | // 单个下单 201 | res, _, err = r.PlaceOrder(req_id, param) 202 | if res { 203 | usedTime := time.Since(start) 204 | fmt.Println("下单成功!", usedTime.String()) 205 | } else { 206 | usedTime := time.Since(start) 207 | fmt.Println("下单失败!", usedTime.String(), err) 208 | } 209 | 210 | ``` 211 | 更多示例请查看ws/ws_jrpc_test.go 212 | 213 | ## wesocket推送 214 | websocket推送数据分为两种类型数据:`普通推送数据`和`深度类型数据`。 215 | 216 | ```go 217 | ws/wImpl/BookData.go 218 | 219 | // 普通推送 220 | type MsgData struct { 221 | Arg map[string]string `json:"arg"` 222 | Data []interface{} `json:"data"` 223 | } 224 | 225 | // 深度数据 226 | type DepthData struct { 227 | Arg map[string]string `json:"arg"` 228 | Action string `json:"action"` 229 | Data []DepthDetail `json:"data"` 230 | } 231 | ``` 232 | 如果需要对推送数据做处理用户可以自定义回调函数: 233 | 1. 全局消息处理的回调函数 234 | 该回调函数会处理所有从服务端接受到的数据。 235 | ```go 236 | /* 237 | 添加全局消息处理的回调函数 238 | */ 239 | func (a *WsClient) AddMessageHook(fn ReceivedDataCallback) error { 240 | a.onMessageHook = fn 241 | return nil 242 | } 243 | ``` 244 | 使用方法参见 ws/ws_test.go中测试用例TestAddMessageHook。 245 | 246 | 2. 订阅消息处理回调函数 247 | 可以处理所有非深度类型的数据,包括 订阅/取消订阅,普通推送数据。 248 | ```go 249 | /* 250 | 添加订阅消息处理的回调函数 251 | */ 252 | func (a *WsClient) AddBookMsgHook(fn ReceivedMsgDataCallback) error { 253 | a.onBookMsgHook = fn 254 | return nil 255 | } 256 | ``` 257 | 使用方法参见 ws/ws_test.go中测试用例TestAddBookedDataHook。 258 | 259 | 260 | 3. 深度消息处理的回调函数 261 | 这里需要说明的是,Wsclient提供了深度数据管理和自动checksum的功能,用户如果需要关闭此功能,只需要调用EnableAutoDepthMgr方法。 262 | ```go 263 | /* 264 | 添加深度消息处理的回调函数 265 | */ 266 | func (a *WsClient) AddDepthHook(fn ReceivedDepthDataCallback) error { 267 | a.onDepthHook = fn 268 | return nil 269 | } 270 | ``` 271 | 使用方法参见 ws/ws_pub_channel_test.go中测试用例TestOrderBooks。 272 | 273 | 4. 错误消息类型回调函数 274 | ```go 275 | func (a *WsClient) AddErrMsgHook(fn ReceivedDataCallback) error { 276 | a.OnErrorHook = fn 277 | return nil 278 | } 279 | ``` 280 | 281 | # 联系方式 282 | 邮箱:caron_co@163.com 283 | 微信:caron_co 284 | -------------------------------------------------------------------------------- /rest/contants.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | const ( 4 | 5 | /* 6 | http headers 7 | */ 8 | OK_ACCESS_KEY = "OK-ACCESS-KEY" 9 | OK_ACCESS_SIGN = "OK-ACCESS-SIGN" 10 | OK_ACCESS_TIMESTAMP = "OK-ACCESS-TIMESTAMP" 11 | OK_ACCESS_PASSPHRASE = "OK-ACCESS-PASSPHRASE" 12 | X_SIMULATE_TRADING = "x-simulated-trading" 13 | 14 | CONTENT_TYPE = "Content-Type" 15 | ACCEPT = "Accept" 16 | COOKIE = "Cookie" 17 | LOCALE = "locale=" 18 | 19 | APPLICATION_JSON = "application/json" 20 | APPLICATION_JSON_UTF8 = "application/json; charset=UTF-8" 21 | 22 | /* 23 | i18n: internationalization 24 | */ 25 | ENGLISH = "en_US" 26 | SIMPLIFIED_CHINESE = "zh_CN" 27 | //zh_TW || zh_HK 28 | TRADITIONAL_CHINESE = "zh_HK" 29 | 30 | GET = "GET" 31 | POST = "POST" 32 | ) 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /rest/rest.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "strings" 12 | "time" 13 | . "v5sdk_go/utils" 14 | ) 15 | 16 | type RESTAPI struct { 17 | EndPoint string `json:"endPoint"` 18 | // GET/POST 19 | Method string `json:"method"` 20 | Uri string `json:"uri"` 21 | Param map[string]interface{} `json:"param"` 22 | Timeout time.Duration 23 | ApiKeyInfo *APIKeyInfo 24 | isSimulate bool 25 | } 26 | 27 | type APIKeyInfo struct { 28 | ApiKey string 29 | PassPhrase string 30 | SecKey string 31 | UserId string 32 | } 33 | 34 | type RESTAPIResult struct { 35 | Url string `json:"url"` 36 | Param string `json:"param"` 37 | Header string `json:"header"` 38 | Code int `json:"code"` 39 | // 原始返回信息 40 | Body string `json:"body"` 41 | // okexV5返回的数据 42 | V5Response Okexv5APIResponse `json:"v5Response"` 43 | ReqUsedTime time.Duration `json:"reqUsedTime"` 44 | TotalUsedTime time.Duration `json:"totalUsedTime"` 45 | } 46 | 47 | type Okexv5APIResponse struct { 48 | Code string `json:"code"` 49 | Msg string `json:"msg"` 50 | Data []map[string]interface{} `json:"data"` 51 | } 52 | 53 | /* 54 | endPoint:请求地址 55 | apiKey 56 | isSimulate: 是否为模拟环境 57 | */ 58 | func NewRESTClient(endPoint string, apiKey *APIKeyInfo, isSimulate bool) *RESTAPI { 59 | 60 | res := &RESTAPI{ 61 | EndPoint: endPoint, 62 | ApiKeyInfo: apiKey, 63 | isSimulate: isSimulate, 64 | Timeout: 5 * time.Second, 65 | } 66 | return res 67 | } 68 | 69 | func NewRESTAPI(ep, method, uri string, param *map[string]interface{}) *RESTAPI { 70 | //TODO:参数校验 71 | reqParam := make(map[string]interface{}) 72 | 73 | if param != nil { 74 | reqParam = *param 75 | } 76 | res := &RESTAPI{ 77 | EndPoint: ep, 78 | Method: method, 79 | Uri: uri, 80 | Param: reqParam, 81 | Timeout: 150 * time.Second, 82 | } 83 | return res 84 | } 85 | 86 | func (this *RESTAPI) SetSimulate(b bool) *RESTAPI { 87 | this.isSimulate = b 88 | return this 89 | } 90 | 91 | func (this *RESTAPI) SetAPIKey(apiKey, secKey, passPhrase string) *RESTAPI { 92 | if this.ApiKeyInfo == nil { 93 | this.ApiKeyInfo = &APIKeyInfo{ 94 | ApiKey: apiKey, 95 | PassPhrase: passPhrase, 96 | SecKey: secKey, 97 | } 98 | } else { 99 | this.ApiKeyInfo.ApiKey = apiKey 100 | this.ApiKeyInfo.PassPhrase = passPhrase 101 | this.ApiKeyInfo.SecKey = secKey 102 | } 103 | return this 104 | } 105 | 106 | func (this *RESTAPI) SetUserId(userId string) *RESTAPI { 107 | if this.ApiKeyInfo == nil { 108 | fmt.Println("ApiKey为空") 109 | return this 110 | } 111 | 112 | this.ApiKeyInfo.UserId = userId 113 | return this 114 | } 115 | 116 | func (this *RESTAPI) SetTimeOut(timeout time.Duration) *RESTAPI { 117 | this.Timeout = timeout 118 | return this 119 | } 120 | 121 | // GET请求 122 | func (this *RESTAPI) Get(ctx context.Context, uri string, param *map[string]interface{}) (res *RESTAPIResult, err error) { 123 | this.Method = GET 124 | this.Uri = uri 125 | 126 | reqParam := make(map[string]interface{}) 127 | 128 | if param != nil { 129 | reqParam = *param 130 | } 131 | this.Param = reqParam 132 | return this.Run(ctx) 133 | } 134 | 135 | // POST请求 136 | func (this *RESTAPI) Post(ctx context.Context, uri string, param *map[string]interface{}) (res *RESTAPIResult, err error) { 137 | this.Method = POST 138 | this.Uri = uri 139 | 140 | reqParam := make(map[string]interface{}) 141 | 142 | if param != nil { 143 | reqParam = *param 144 | } 145 | this.Param = reqParam 146 | 147 | return this.Run(ctx) 148 | } 149 | 150 | func (this *RESTAPI) Run(ctx context.Context) (res *RESTAPIResult, err error) { 151 | 152 | if this.ApiKeyInfo == nil { 153 | err = errors.New("APIKey不可为空") 154 | return 155 | } 156 | 157 | procStart := time.Now() 158 | 159 | defer func() { 160 | if res != nil { 161 | res.TotalUsedTime = time.Since(procStart) 162 | } 163 | }() 164 | 165 | client := &http.Client{ 166 | Timeout: this.Timeout, 167 | } 168 | 169 | uri, body, err := this.GenReqInfo() 170 | if err != nil { 171 | return 172 | } 173 | 174 | url := this.EndPoint + uri 175 | bodyBuf := new(bytes.Buffer) 176 | bodyBuf.ReadFrom(strings.NewReader(body)) 177 | 178 | req, err := http.NewRequest(this.Method, url, bodyBuf) 179 | if err != nil { 180 | return 181 | } 182 | 183 | res = &RESTAPIResult{ 184 | Url: url, 185 | Param: body, 186 | } 187 | 188 | // Sign and set request headers 189 | timestamp := IsoTime() 190 | preHash := PreHashString(timestamp, this.Method, uri, body) 191 | //log.Println("preHash:", preHash) 192 | sign, err := HmacSha256Base64Signer(preHash, this.ApiKeyInfo.SecKey) 193 | if err != nil { 194 | return 195 | } 196 | //log.Println("sign:", sign) 197 | headStr := this.SetHeaders(req, timestamp, sign) 198 | res.Header = headStr 199 | 200 | this.PrintRequest(req, body, preHash) 201 | resp, err := client.Do(req) 202 | if err != nil { 203 | fmt.Println("请求失败!", err) 204 | return 205 | } 206 | defer resp.Body.Close() 207 | 208 | res.ReqUsedTime = time.Since(procStart) 209 | 210 | resBuff, err := ioutil.ReadAll(resp.Body) 211 | if err != nil { 212 | fmt.Println("获取请求结果失败!", err) 213 | return 214 | } 215 | 216 | res.Body = string(resBuff) 217 | res.Code = resp.StatusCode 218 | 219 | // 解析结果 220 | var v5rsp Okexv5APIResponse 221 | err = json.Unmarshal(resBuff, &v5rsp) 222 | if err != nil { 223 | fmt.Println("解析v5返回失败!", err) 224 | return 225 | } 226 | 227 | res.V5Response = v5rsp 228 | 229 | return 230 | } 231 | 232 | /* 233 | 生成请求对应的参数 234 | */ 235 | func (this *RESTAPI) GenReqInfo() (uri string, body string, err error) { 236 | uri = this.Uri 237 | 238 | switch this.Method { 239 | case GET: 240 | getParam := []string{} 241 | 242 | if len(this.Param) == 0 { 243 | return 244 | } 245 | 246 | for k, v := range this.Param { 247 | getParam = append(getParam, fmt.Sprintf("%v=%v", k, v)) 248 | } 249 | uri = uri + "?" + strings.Join(getParam, "&") 250 | 251 | case POST: 252 | 253 | var rawBody []byte 254 | rawBody, err = json.Marshal(this.Param) 255 | if err != nil { 256 | return 257 | } 258 | body = string(rawBody) 259 | default: 260 | err = errors.New("request type unknown!") 261 | return 262 | } 263 | 264 | return 265 | } 266 | 267 | /* 268 | Set http request headers: 269 | Accept: application/json 270 | Content-Type: application/json; charset=UTF-8 (default) 271 | Cookie: locale=en_US (English) 272 | OK-ACCESS-KEY: (Your setting) 273 | OK-ACCESS-SIGN: (Use your setting, auto sign and add) 274 | OK-ACCESS-TIMESTAMP: (Auto add) 275 | OK-ACCESS-PASSPHRASE: Your setting 276 | */ 277 | func (this *RESTAPI) SetHeaders(request *http.Request, timestamp string, sign string) (header string) { 278 | 279 | request.Header.Add(ACCEPT, APPLICATION_JSON) 280 | header += ACCEPT + ":" + APPLICATION_JSON + "\n" 281 | 282 | request.Header.Add(CONTENT_TYPE, APPLICATION_JSON_UTF8) 283 | header += CONTENT_TYPE + ":" + APPLICATION_JSON_UTF8 + "\n" 284 | 285 | request.Header.Add(COOKIE, LOCALE+ENGLISH) 286 | header += COOKIE + ":" + LOCALE + ENGLISH + "\n" 287 | 288 | request.Header.Add(OK_ACCESS_KEY, this.ApiKeyInfo.ApiKey) 289 | header += OK_ACCESS_KEY + ":" + this.ApiKeyInfo.ApiKey + "\n" 290 | 291 | request.Header.Add(OK_ACCESS_SIGN, sign) 292 | header += OK_ACCESS_SIGN + ":" + sign + "\n" 293 | 294 | request.Header.Add(OK_ACCESS_TIMESTAMP, timestamp) 295 | header += OK_ACCESS_TIMESTAMP + ":" + timestamp + "\n" 296 | 297 | request.Header.Add(OK_ACCESS_PASSPHRASE, this.ApiKeyInfo.PassPhrase) 298 | header += OK_ACCESS_PASSPHRASE + ":" + this.ApiKeyInfo.PassPhrase + "\n" 299 | 300 | //模拟盘交易标记 301 | if this.isSimulate { 302 | request.Header.Add(X_SIMULATE_TRADING, "1") 303 | header += X_SIMULATE_TRADING + ":1" + "\n" 304 | } 305 | return 306 | } 307 | 308 | /* 309 | 打印header信息 310 | */ 311 | func (this *RESTAPI) PrintRequest(request *http.Request, body string, preHash string) { 312 | if this.ApiKeyInfo.SecKey != "" { 313 | fmt.Println(" Secret-Key: " + this.ApiKeyInfo.SecKey) 314 | } 315 | fmt.Println(" Request(" + IsoTime() + "):") 316 | fmt.Println("\tUrl: " + request.URL.String()) 317 | fmt.Println("\tMethod: " + strings.ToUpper(request.Method)) 318 | if len(request.Header) > 0 { 319 | fmt.Println("\tHeaders: ") 320 | for k, v := range request.Header { 321 | if strings.Contains(k, "Ok-") { 322 | k = strings.ToUpper(k) 323 | } 324 | fmt.Println("\t\t" + k + ": " + v[0]) 325 | } 326 | } 327 | fmt.Println("\tBody: " + body) 328 | if preHash != "" { 329 | fmt.Println(" PreHash: " + preHash) 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /rest/rest_test.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | /* 10 | GET请求 11 | */ 12 | func TestRESTAPIGet(t *testing.T) { 13 | 14 | rest := NewRESTAPI("https://www.okex.win", GET, "/api/v5/account/balance", nil) 15 | rest.SetSimulate(true).SetAPIKey("xxxx", "xxxx", "xxxx") 16 | rest.SetUserId("xxxxx") 17 | response, err := rest.Run(context.Background()) 18 | if err != nil { 19 | fmt.Println(err) 20 | return 21 | } 22 | 23 | fmt.Println("Response:") 24 | fmt.Println("\thttp code: ", response.Code) 25 | fmt.Println("\t总耗时: ", response.TotalUsedTime) 26 | fmt.Println("\t请求耗时: ", response.ReqUsedTime) 27 | fmt.Println("\t返回消息: ", response.Body) 28 | fmt.Println("\terrCode: ", response.V5Response.Code) 29 | fmt.Println("\terrMsg: ", response.V5Response.Msg) 30 | fmt.Println("\tdata: ", response.V5Response.Data) 31 | 32 | // 请求的另一种方式 33 | apikey := APIKeyInfo{ 34 | ApiKey: "xxxxx", 35 | SecKey: "xxxxx", 36 | PassPhrase: "xxx", 37 | } 38 | 39 | cli := NewRESTClient("https://www.okex.win", &apikey, true) 40 | rsp, err := cli.Get(context.Background(), "/api/v5/account/balance", nil) 41 | if err != nil { 42 | return 43 | } 44 | 45 | fmt.Println("Response:") 46 | fmt.Println("\thttp code: ", rsp.Code) 47 | fmt.Println("\t总耗时: ", rsp.TotalUsedTime) 48 | fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) 49 | fmt.Println("\t返回消息: ", rsp.Body) 50 | fmt.Println("\terrCode: ", rsp.V5Response.Code) 51 | fmt.Println("\terrMsg: ", rsp.V5Response.Msg) 52 | fmt.Println("\tdata: ", rsp.V5Response.Data) 53 | } 54 | 55 | /* 56 | POST请求 57 | */ 58 | func TestRESTAPIPost(t *testing.T) { 59 | param := make(map[string]interface{}) 60 | param["greeksType"] = "PA" 61 | 62 | rest := NewRESTAPI("https://www.okex.win", POST, "/api/v5/account/set-greeks", ¶m) 63 | rest.SetSimulate(true).SetAPIKey("xxxx", "xxxx", "xxxx") 64 | response, err := rest.Run(context.Background()) 65 | if err != nil { 66 | fmt.Println(err) 67 | return 68 | } 69 | 70 | fmt.Println("Response:") 71 | fmt.Println("\thttp code: ", response.Code) 72 | fmt.Println("\t总耗时: ", response.TotalUsedTime) 73 | fmt.Println("\t请求耗时: ", response.ReqUsedTime) 74 | fmt.Println("\t返回消息: ", response.Body) 75 | fmt.Println("\terrCode: ", response.V5Response.Code) 76 | fmt.Println("\terrMsg: ", response.V5Response.Msg) 77 | fmt.Println("\tdata: ", response.V5Response.Data) 78 | 79 | // 请求的另一种方式 80 | apikey := APIKeyInfo{ 81 | ApiKey: "xxxx", 82 | SecKey: "xxxxx", 83 | PassPhrase: "xxxx", 84 | } 85 | 86 | cli := NewRESTClient("https://www.okex.win", &apikey, true) 87 | rsp, err := cli.Post(context.Background(), "/api/v5/account/set-greeks", ¶m) 88 | if err != nil { 89 | return 90 | } 91 | 92 | fmt.Println("Response:") 93 | fmt.Println("\thttp code: ", rsp.Code) 94 | fmt.Println("\t总耗时: ", rsp.TotalUsedTime) 95 | fmt.Println("\t请求耗时: ", rsp.ReqUsedTime) 96 | fmt.Println("\t返回消息: ", rsp.Body) 97 | fmt.Println("\terrCode: ", rsp.V5Response.Code) 98 | fmt.Println("\terrMsg: ", rsp.V5Response.Msg) 99 | fmt.Println("\tdata: ", rsp.V5Response.Data) 100 | } 101 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "compress/flate" 6 | "crypto/hmac" 7 | "crypto/sha256" 8 | "encoding/base64" 9 | "encoding/json" 10 | "io/ioutil" 11 | "log" 12 | "strconv" 13 | "strings" 14 | "time" 15 | //"net/http" 16 | ) 17 | 18 | /* 19 | Get a epoch time 20 | eg: 1521221737 21 | */ 22 | func EpochTime() string { 23 | millisecond := time.Now().UnixNano() / 1000000 24 | epoch := strconv.Itoa(int(millisecond)) 25 | epochBytes := []byte(epoch) 26 | epoch = string(epochBytes[:10]) 27 | return epoch 28 | } 29 | 30 | /* 31 | signing a message 32 | using: hmac sha256 + base64 33 | eg: 34 | message = Pre_hash function comment 35 | secretKey = E65791902180E9EF4510DB6A77F6EBAE 36 | 37 | return signed string = TO6uwdqz+31SIPkd4I+9NiZGmVH74dXi+Fd5X0EzzSQ= 38 | */ 39 | func HmacSha256Base64Signer(message string, secretKey string) (string, error) { 40 | mac := hmac.New(sha256.New, []byte(secretKey)) 41 | _, err := mac.Write([]byte(message)) 42 | if err != nil { 43 | return "", err 44 | } 45 | return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil 46 | } 47 | 48 | /* 49 | the pre hash string 50 | eg: 51 | timestamp = 2018-03-08T10:59:25.789Z 52 | method = POST 53 | request_path = /orders?before=2&limit=30 54 | body = {"product_id":"BTC-USD-0309","order_id":"377454671037440"} 55 | 56 | return pre hash string = 2018-03-08T10:59:25.789ZPOST/orders?before=2&limit=30{"product_id":"BTC-USD-0309","order_id":"377454671037440"} 57 | */ 58 | func PreHashString(timestamp string, method string, requestPath string, body string) string { 59 | return timestamp + strings.ToUpper(method) + requestPath + body 60 | } 61 | 62 | /* 63 | struct convert json string 64 | */ 65 | func Struct2JsonString(raw interface{}) (jsonString string, err error) { 66 | //fmt.Println("转化json,", raw) 67 | data, err := json.Marshal(raw) 68 | if err != nil { 69 | log.Println("convert json failed!", err) 70 | return "", err 71 | } 72 | //log.Println(string(data)) 73 | return string(data), nil 74 | } 75 | 76 | // 解压缩消息 77 | func GzipDecode(in []byte) ([]byte, error) { 78 | reader := flate.NewReader(bytes.NewReader(in)) 79 | defer reader.Close() 80 | 81 | return ioutil.ReadAll(reader) 82 | } 83 | 84 | 85 | /* 86 | Get a iso time 87 | eg: 2018-03-16T18:02:48.284Z 88 | */ 89 | func IsoTime() string { 90 | utcTime := time.Now().UTC() 91 | iso := utcTime.String() 92 | isoBytes := []byte(iso) 93 | iso = string(isoBytes[:10]) + "T" + string(isoBytes[11:23]) + "Z" 94 | return iso 95 | } 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestHmacSha256Base64Signer(t *testing.T) { 9 | raw := `2021-04-06T03:33:21.681ZPOST/api/v5/trade/order{"instId":"ETH-USDT-SWAP","ordType":"limit","px":"2300","side":"sell","sz":"1","tdMode":"cross"}` 10 | key := "1A9E86759F2D2AA16E389FD3B7F8273E" 11 | res, err := HmacSha256Base64Signer(raw, key) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | fmt.Println(res) 16 | t.Log(res) 17 | } 18 | -------------------------------------------------------------------------------- /ws/utils.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "runtime/debug" 7 | . "v5sdk_go/ws/wImpl" 8 | . "v5sdk_go/ws/wInterface" 9 | ) 10 | 11 | // 判断返回结果成功失败 12 | func checkResult(wsReq WSReqData, wsRsps []*Msg) (res bool, err error) { 13 | defer func() { 14 | a := recover() 15 | if a != nil { 16 | log.Printf("Receive End. Recover msg: %+v", a) 17 | debug.PrintStack() 18 | } 19 | return 20 | }() 21 | 22 | res = false 23 | if len(wsRsps) == 0 { 24 | return 25 | } 26 | 27 | for _, v := range wsRsps { 28 | switch v.Info.(type) { 29 | case ErrData: 30 | return 31 | } 32 | if wsReq.GetType() != v.Info.(WSRspData).MsgType() { 33 | err = errors.New("消息类型不一致") 34 | return 35 | } 36 | } 37 | 38 | //检查所有频道是否都更新成功 39 | if wsReq.GetType() == MSG_NORMAL { 40 | req, ok := wsReq.(ReqData) 41 | if !ok { 42 | log.Println("类型转化失败", req) 43 | err = errors.New("类型转化失败") 44 | return 45 | } 46 | 47 | for idx, _ := range req.Args { 48 | ok := false 49 | i_req := req.Args[idx] 50 | //fmt.Println("检查",i_req) 51 | for i, _ := range wsRsps { 52 | info, _ := wsRsps[i].Info.(RspData) 53 | //fmt.Println("<<",info) 54 | if info.Event == req.Op && info.Arg["channel"] == i_req["channel"] && info.Arg["instType"] == i_req["instType"] { 55 | ok = true 56 | continue 57 | } 58 | } 59 | if !ok { 60 | err = errors.New("未得到所有的期望的返回结果") 61 | return 62 | } 63 | } 64 | } else { 65 | for i, _ := range wsRsps { 66 | info, _ := wsRsps[i].Info.(JRPCRsp) 67 | if info.Code != "0" { 68 | return 69 | } 70 | } 71 | } 72 | 73 | res = true 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /ws/wImpl/BookData.go: -------------------------------------------------------------------------------- 1 | /* 2 | 订阅频道后收到的推送数据 3 | */ 4 | 5 | package wImpl 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "hash/crc32" 12 | "log" 13 | "strconv" 14 | ) 15 | 16 | // 普通推送 17 | type MsgData struct { 18 | Arg map[string]string `json:"arg"` 19 | Data []interface{} `json:"data"` 20 | } 21 | 22 | // 深度数据 23 | type DepthData struct { 24 | Arg map[string]string `json:"arg"` 25 | Action string `json:"action"` 26 | Data []DepthDetail `json:"data"` 27 | } 28 | 29 | type DepthDetail struct { 30 | Asks [][]string `json:"asks"` 31 | Bids [][]string `json:"bids"` 32 | Ts string `json:"ts"` 33 | Checksum int32 `json:"checksum"` 34 | } 35 | 36 | /* 37 | 深度数据校验 38 | */ 39 | func (this *DepthData) CheckSum(snap *DepthDetail) (pDepData *DepthDetail, err error) { 40 | 41 | if len(this.Data) != 1 { 42 | err = errors.New("深度数据错误!") 43 | return 44 | } 45 | 46 | if this.Action == DEPTH_SNAPSHOT { 47 | _, cs := CalCrc32(this.Data[0].Asks, this.Data[0].Bids) 48 | 49 | if cs != this.Data[0].Checksum { 50 | err = errors.New("校验失败!") 51 | return 52 | } 53 | pDepData = &this.Data[0] 54 | log.Println("snapshot校验成功", this.Data[0].Checksum) 55 | 56 | } 57 | 58 | if this.Action == DEPTH_UPDATE { 59 | if snap == nil { 60 | err = errors.New("深度快照数据不可为空!") 61 | return 62 | } 63 | 64 | pDepData, err = MergDepthData(*snap, this.Data[0], this.Data[0].Checksum) 65 | if err != nil { 66 | return 67 | } 68 | log.Println("update校验成功", this.Data[0].Checksum) 69 | } 70 | 71 | return 72 | } 73 | 74 | func CalCrc32(askDepths [][]string, bidDepths [][]string) (bytes.Buffer, int32) { 75 | 76 | crc32BaseBuffer := bytes.Buffer{} 77 | crcAskDepth, crcBidDepth := 25, 25 78 | 79 | if len(askDepths) < 25 { 80 | crcAskDepth = len(askDepths) 81 | } 82 | if len(bidDepths) < 25 { 83 | crcBidDepth = len(bidDepths) 84 | } 85 | if crcAskDepth == crcBidDepth { 86 | for i := 0; i < crcAskDepth; i++ { 87 | if crc32BaseBuffer.Len() > 0 { 88 | crc32BaseBuffer.WriteString(":") 89 | } 90 | crc32BaseBuffer.WriteString( 91 | fmt.Sprintf("%v:%v:%v:%v", 92 | (bidDepths)[i][0], (bidDepths)[i][1], 93 | (askDepths)[i][0], (askDepths)[i][1])) 94 | } 95 | } else { 96 | 97 | var crcArr []string 98 | for i, j := 0, 0; i < crcBidDepth || j < crcAskDepth; { 99 | 100 | if i < crcBidDepth { 101 | crcArr = append(crcArr, fmt.Sprintf("%v:%v", (bidDepths)[i][0], (bidDepths)[i][1])) 102 | i++ 103 | } 104 | 105 | if j < crcAskDepth { 106 | crcArr = append(crcArr, fmt.Sprintf("%v:%v", (askDepths)[j][0], (askDepths)[j][1])) 107 | j++ 108 | } 109 | } 110 | 111 | crc32BaseBuffer.WriteString(strings.Join(crcArr, ":")) 112 | } 113 | 114 | expectCrc32 := int32(crc32.ChecksumIEEE(crc32BaseBuffer.Bytes())) 115 | 116 | return crc32BaseBuffer, expectCrc32 117 | } 118 | 119 | /* 120 | 深度合并的内部方法 121 | 返回结果: 122 | res:合并后的深度 123 | index: 最新的 ask1/bids1 的索引 124 | */ 125 | func mergeDepth(oldDepths [][]string, newDepths [][]string, method string) (res [][]string, err error) { 126 | 127 | oldIdx, newIdx := 0, 0 128 | 129 | for oldIdx < len(oldDepths) && newIdx < len(newDepths) { 130 | 131 | oldItem := oldDepths[oldIdx] 132 | newItem := newDepths[newIdx] 133 | var oldPrice, newPrice float64 134 | oldPrice, err = strconv.ParseFloat(oldItem[0], 10) 135 | if err != nil { 136 | return 137 | } 138 | newPrice, err = strconv.ParseFloat(newItem[0], 10) 139 | if err != nil { 140 | return 141 | } 142 | 143 | if oldPrice == newPrice { 144 | if newItem[1] != "0" { 145 | res = append(res, newItem) 146 | } 147 | 148 | oldIdx++ 149 | newIdx++ 150 | } else { 151 | switch method { 152 | // 降序 153 | case "bids": 154 | if oldPrice < newPrice { 155 | res = append(res, newItem) 156 | newIdx++ 157 | } else { 158 | 159 | res = append(res, oldItem) 160 | oldIdx++ 161 | } 162 | // 升序 163 | case "asks": 164 | if oldPrice > newPrice { 165 | res = append(res, newItem) 166 | newIdx++ 167 | } else { 168 | 169 | res = append(res, oldItem) 170 | oldIdx++ 171 | } 172 | } 173 | } 174 | 175 | } 176 | 177 | if oldIdx < len(oldDepths) { 178 | res = append(res, oldDepths[oldIdx:]...) 179 | } 180 | 181 | if newIdx < len(newDepths) { 182 | res = append(res, newDepths[newIdx:]...) 183 | } 184 | 185 | return 186 | } 187 | 188 | /* 189 | 深度合并,并校验 190 | */ 191 | func MergDepthData(snap DepthDetail, update DepthDetail, expChecksum int32) (res *DepthDetail, err error) { 192 | 193 | newAskDepths, err1 := mergeDepth(snap.Asks, update.Asks, "asks") 194 | if err1 != nil { 195 | return 196 | } 197 | 198 | // log.Println("old Ask - ", snap.Asks) 199 | // log.Println("update Ask - ", update.Asks) 200 | // log.Println("new Ask - ", newAskDepths) 201 | newBidDepths, err2 := mergeDepth(snap.Bids, update.Bids, "bids") 202 | if err2 != nil { 203 | return 204 | } 205 | // log.Println("old Bids - ", snap.Bids) 206 | // log.Println("update Bids - ", update.Bids) 207 | // log.Println("new Bids - ", newBidDepths) 208 | 209 | cBuf, checksum := CalCrc32(newAskDepths, newBidDepths) 210 | if checksum != expChecksum { 211 | err = errors.New("校验失败!") 212 | log.Println("buffer:", cBuf.String()) 213 | log.Fatal(checksum, expChecksum) 214 | return 215 | } 216 | 217 | res = &DepthDetail{ 218 | Asks: newAskDepths, 219 | Bids: newBidDepths, 220 | Ts: update.Ts, 221 | Checksum: update.Checksum, 222 | } 223 | 224 | return 225 | } 226 | -------------------------------------------------------------------------------- /ws/wImpl/ErrData.go: -------------------------------------------------------------------------------- 1 | /* 2 | 错误数据 3 | */ 4 | package wImpl 5 | 6 | // 服务端请求错误返回消息格式 7 | type ErrData struct { 8 | Event string `json:"event"` 9 | Code string `json:"code"` 10 | Msg string `json:"msg"` 11 | } 12 | 13 | 14 | -------------------------------------------------------------------------------- /ws/wImpl/JRPCData.go: -------------------------------------------------------------------------------- 1 | /* 2 | JRPC请求/响应数据 3 | */ 4 | package wImpl 5 | 6 | import ( 7 | "encoding/json" 8 | . "v5sdk_go/utils" 9 | ) 10 | 11 | // jrpc请求结构体 12 | type JRPCReq struct { 13 | Id string `json:"id"` 14 | Op string `json:"op"` 15 | Args []map[string]interface{} `json:"args"` 16 | } 17 | 18 | func (r JRPCReq) GetType() int { 19 | return MSG_JRPC 20 | } 21 | 22 | func (r JRPCReq) ToString() string { 23 | data, err := Struct2JsonString(r) 24 | if err != nil { 25 | return "" 26 | } 27 | return data 28 | } 29 | 30 | func (r JRPCReq) Len() int { 31 | return 1 32 | } 33 | 34 | // jrpc响应结构体 35 | type JRPCRsp struct { 36 | Id string `json:"id"` 37 | Op string `json:"op"` 38 | Data []map[string]interface{} `json:"data"` 39 | Code string `json:"code"` 40 | Msg string `json:"msg"` 41 | } 42 | 43 | func (r JRPCRsp) MsgType() int { 44 | return MSG_JRPC 45 | } 46 | 47 | func (r JRPCRsp) String() string { 48 | raw, _ := json.Marshal(r) 49 | return string(raw) 50 | } 51 | -------------------------------------------------------------------------------- /ws/wImpl/ReqData.go: -------------------------------------------------------------------------------- 1 | /* 2 | 普通订阅请求和响应的数据格式 3 | */ 4 | 5 | package wImpl 6 | 7 | import ( 8 | "encoding/json" 9 | . "v5sdk_go/utils" 10 | ) 11 | 12 | // 客户端请求消息格式 13 | type ReqData struct { 14 | Op string `json:"op"` 15 | Args []map[string]string `json:"args"` 16 | } 17 | 18 | func (r ReqData) GetType() int { 19 | return MSG_NORMAL 20 | } 21 | 22 | func (r ReqData) ToString() string { 23 | data, err := Struct2JsonString(r) 24 | if err != nil { 25 | return "" 26 | } 27 | return data 28 | } 29 | 30 | func (r ReqData) Len() int { 31 | return len(r.Args) 32 | } 33 | 34 | // 服务端请求响应消息格式 35 | type RspData struct { 36 | Event string `json:"event"` 37 | Arg map[string]string `json:"arg"` 38 | } 39 | 40 | func (r RspData) MsgType() int { 41 | return MSG_NORMAL 42 | } 43 | 44 | func (r RspData) String() string { 45 | raw, _ := json.Marshal(r) 46 | return string(raw) 47 | } 48 | -------------------------------------------------------------------------------- /ws/wImpl/contants.go: -------------------------------------------------------------------------------- 1 | package wImpl 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | /* 8 | 9 | */ 10 | 11 | const ( 12 | MSG_NORMAL = iota 13 | MSG_JRPC 14 | ) 15 | 16 | //事件 17 | type Event int 18 | 19 | /* 20 | EventID 21 | */ 22 | const ( 23 | EVENT_UNKNOWN Event = iota 24 | EVENT_ERROR 25 | EVENT_PING 26 | EVENT_LOGIN 27 | 28 | //订阅公共频道 29 | EVENT_BOOK_INSTRUMENTS 30 | EVENT_STATUS 31 | EVENT_BOOK_TICKERS 32 | EVENT_BOOK_OPEN_INTEREST 33 | EVENT_BOOK_KLINE 34 | EVENT_BOOK_TRADE 35 | EVENT_BOOK_ESTIMATE_PRICE 36 | EVENT_BOOK_MARK_PRICE 37 | EVENT_BOOK_MARK_PRICE_CANDLE_CHART 38 | EVENT_BOOK_LIMIT_PRICE 39 | EVENT_BOOK_ORDER_BOOK 40 | EVENT_BOOK_ORDER_BOOK5 41 | EVENT_BOOK_ORDER_BOOK_TBT 42 | EVENT_BOOK_ORDER_BOOK50_TBT 43 | EVENT_BOOK_OPTION_SUMMARY 44 | EVENT_BOOK_FUND_RATE 45 | EVENT_BOOK_KLINE_INDEX 46 | EVENT_BOOK_INDEX_TICKERS 47 | 48 | //订阅私有频道 49 | EVENT_BOOK_ACCOUNT 50 | EVENT_BOOK_POSTION 51 | EVENT_BOOK_ORDER 52 | EVENT_BOOK_ALG_ORDER 53 | EVENT_BOOK_B_AND_P 54 | 55 | // JRPC 56 | EVENT_PLACE_ORDER 57 | EVENT_PLACE_BATCH_ORDERS 58 | EVENT_CANCEL_ORDER 59 | EVENT_CANCEL_BATCH_ORDERS 60 | EVENT_AMEND_ORDER 61 | EVENT_AMEND_BATCH_ORDERS 62 | 63 | //订阅返回数据 64 | EVENT_BOOKED_DATA 65 | EVENT_DEPTH_DATA 66 | ) 67 | 68 | /* 69 | EventID,事件名称,channel 70 | 注: 带有周期参数的频道 如 行情频道 ,需要将channel写为 正则表达模式方便 类型匹配,如 "^candle*" 71 | */ 72 | var EVENT_TABLE = [][]interface{}{ 73 | // 未知的消息 74 | {EVENT_UNKNOWN, "未知", ""}, 75 | // 错误的消息 76 | {EVENT_ERROR, "错误", ""}, 77 | // Ping 78 | {EVENT_PING, "ping", ""}, 79 | // 登陆 80 | {EVENT_LOGIN, "登录", ""}, 81 | 82 | /* 83 | 订阅公共频道 84 | */ 85 | 86 | {EVENT_BOOK_INSTRUMENTS, "产品", "instruments"}, 87 | {EVENT_STATUS, "status", "status"}, 88 | {EVENT_BOOK_TICKERS, "行情", "tickers"}, 89 | {EVENT_BOOK_OPEN_INTEREST, "持仓总量", "open-interest"}, 90 | {EVENT_BOOK_KLINE, "K线", "candle"}, 91 | {EVENT_BOOK_TRADE, "交易", "trades"}, 92 | {EVENT_BOOK_ESTIMATE_PRICE, "预估交割/行权价格", "estimated-price"}, 93 | {EVENT_BOOK_MARK_PRICE, "标记价格", "mark-price"}, 94 | {EVENT_BOOK_MARK_PRICE_CANDLE_CHART, "标记价格K线", "mark-price-candle"}, 95 | {EVENT_BOOK_LIMIT_PRICE, "限价", "price-limit"}, 96 | {EVENT_BOOK_ORDER_BOOK, "400档深度", "books"}, 97 | {EVENT_BOOK_ORDER_BOOK5, "5档深度", "books5"}, 98 | {EVENT_BOOK_ORDER_BOOK_TBT, "tbt深度", "books-l2-tbt"}, 99 | {EVENT_BOOK_ORDER_BOOK50_TBT, "tbt50深度", "books50-l2-tbt"}, 100 | {EVENT_BOOK_OPTION_SUMMARY, "期权定价", "opt-summary"}, 101 | {EVENT_BOOK_FUND_RATE, "资金费率", "funding-rate"}, 102 | {EVENT_BOOK_KLINE_INDEX, "指数K线", "index-candle"}, 103 | {EVENT_BOOK_INDEX_TICKERS, "指数行情", "index-tickers"}, 104 | 105 | /* 106 | 订阅私有频道 107 | */ 108 | {EVENT_BOOK_ACCOUNT, "账户", "account"}, 109 | {EVENT_BOOK_POSTION, "持仓", "positions"}, 110 | {EVENT_BOOK_ORDER, "订单", "orders"}, 111 | {EVENT_BOOK_ALG_ORDER, "策略委托订单", "orders-algo"}, 112 | {EVENT_BOOK_B_AND_P, "账户余额和持仓", "balance_and_position"}, 113 | 114 | /* 115 | JRPC 116 | */ 117 | {EVENT_PLACE_ORDER, "下单", "order"}, 118 | {EVENT_PLACE_BATCH_ORDERS, "批量下单", "batch-orders"}, 119 | {EVENT_CANCEL_ORDER, "撤单", "cancel-order"}, 120 | {EVENT_CANCEL_BATCH_ORDERS, "批量撤单", "batch-cancel-orders"}, 121 | {EVENT_AMEND_ORDER, "改单", "amend-order"}, 122 | {EVENT_AMEND_BATCH_ORDERS, "批量改单", "batch-amend-orders"}, 123 | 124 | /* 125 | 订阅返回数据 126 | 注意:推送数据channle统一为"" 127 | */ 128 | {EVENT_BOOKED_DATA, "普通推送", ""}, 129 | {EVENT_DEPTH_DATA, "深度推送", ""}, 130 | } 131 | 132 | /* 133 | 获取事件名称 134 | */ 135 | func (e Event) String() string { 136 | for _, v := range EVENT_TABLE { 137 | eventId := v[0].(Event) 138 | if e == eventId { 139 | return v[1].(string) 140 | } 141 | } 142 | 143 | return "" 144 | } 145 | 146 | /* 147 | 通过事件获取对应的channel信息 148 | 对于频道名称有时间周期的 通过参数 pd 传入,拼接后返回完整channel信息 149 | */ 150 | func (e Event) GetChannel(pd Period) string { 151 | 152 | channel := "" 153 | 154 | for _, v := range EVENT_TABLE { 155 | eventId := v[0].(Event) 156 | if e == eventId { 157 | channel = v[2].(string) 158 | break 159 | } 160 | } 161 | 162 | if channel == "" { 163 | return "" 164 | } 165 | 166 | return channel + string(pd) 167 | } 168 | 169 | /* 170 | 通过channel信息匹配获取事件类型 171 | */ 172 | func GetEventId(raw string) Event { 173 | evt := EVENT_UNKNOWN 174 | 175 | for _, v := range EVENT_TABLE { 176 | channel := v[2].(string) 177 | if raw == channel { 178 | evt = v[0].(Event) 179 | break 180 | } 181 | 182 | regexp := regexp.MustCompile(`^(.*)([1-9][0-9]?[\w])$`) 183 | //regexp := regexp.MustCompile(`^http://www.flysnow.org/([\d]{4})/([\d]{2})/([\d]{2})/([\w-]+).html$`) 184 | 185 | substr := regexp.FindStringSubmatch(raw) 186 | //fmt.Println(substr) 187 | if len(substr) >= 2 { 188 | if substr[1] == channel { 189 | evt = v[0].(Event) 190 | break 191 | } 192 | } 193 | } 194 | 195 | return evt 196 | } 197 | 198 | // 时间维度 199 | type Period string 200 | 201 | const ( 202 | // 年 203 | PERIOD_1YEAR Period = "1Y" 204 | 205 | // 月 206 | PERIOD_6Mon Period = "6M" 207 | PERIOD_3Mon Period = "3M" 208 | PERIOD_1Mon Period = "1M" 209 | 210 | // 周 211 | PERIOD_1WEEK Period = "1W" 212 | 213 | // 天 214 | PERIOD_5DAY Period = "5D" 215 | PERIOD_3DAY Period = "3D" 216 | PERIOD_2DAY Period = "2D" 217 | PERIOD_1DAY Period = "1D" 218 | 219 | // 小时 220 | PERIOD_12HOUR Period = "12H" 221 | PERIOD_6HOUR Period = "6H" 222 | PERIOD_4HOUR Period = "4H" 223 | PERIOD_2HOUR Period = "2H" 224 | PERIOD_1HOUR Period = "1H" 225 | 226 | // 分钟 227 | PERIOD_30MIN Period = "30m" 228 | PERIOD_15MIN Period = "15m" 229 | PERIOD_5MIN Period = "5m" 230 | PERIOD_3MIN Period = "3m" 231 | PERIOD_1MIN Period = "1m" 232 | 233 | // 缺省 234 | PERIOD_NONE Period = "" 235 | ) 236 | 237 | // 深度枚举 238 | const ( 239 | DEPTH_SNAPSHOT = "snapshot" 240 | DEPTH_UPDATE = "update" 241 | ) 242 | -------------------------------------------------------------------------------- /ws/wImpl/contants_test.go: -------------------------------------------------------------------------------- 1 | package wImpl 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetEventId(t *testing.T) { 10 | 11 | id1 := GetEventId("index-candle30m") 12 | 13 | assert.True(t, id1 == EVENT_BOOK_KLINE_INDEX) 14 | 15 | id2 := GetEventId("candle1Y") 16 | 17 | assert.True(t, id2 == EVENT_BOOK_KLINE) 18 | 19 | id3 := GetEventId("orders-algo") 20 | assert.True(t, id3 == EVENT_BOOK_ALG_ORDER) 21 | 22 | id4 := GetEventId("balance_and_position") 23 | assert.True(t, id4 == EVENT_BOOK_B_AND_P) 24 | 25 | id5 := GetEventId("index-candle1m") 26 | assert.True(t, id5 == EVENT_BOOK_KLINE_INDEX) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ws/wInterface/IParam.go: -------------------------------------------------------------------------------- 1 | package wInterface 2 | 3 | import . "v5sdk_go/ws/wImpl" 4 | 5 | // 请求数据 6 | type WSParam interface { 7 | EventType() Event 8 | ToMap() *map[string]string 9 | } 10 | -------------------------------------------------------------------------------- /ws/wInterface/IReqData.go: -------------------------------------------------------------------------------- 1 | package wInterface 2 | 3 | // 请求数据 4 | type WSReqData interface { 5 | GetType() int 6 | Len() int 7 | ToString() string 8 | } 9 | -------------------------------------------------------------------------------- /ws/wInterface/IRspData.go: -------------------------------------------------------------------------------- 1 | package wInterface 2 | 3 | // 返回数据 4 | type WSRspData interface { 5 | MsgType() int 6 | } 7 | -------------------------------------------------------------------------------- /ws/ws_cli.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "regexp" 10 | "runtime/debug" 11 | "sync" 12 | "time" 13 | . "v5sdk_go/config" 14 | . "v5sdk_go/utils" 15 | . "v5sdk_go/ws/wImpl" 16 | 17 | "github.com/gorilla/websocket" 18 | ) 19 | 20 | // 全局回调函数 21 | type ReceivedDataCallback func(*Msg) error 22 | 23 | // 普通订阅推送数据回调函数 24 | type ReceivedMsgDataCallback func(time.Time, MsgData) error 25 | 26 | // 深度订阅推送数据回调函数 27 | type ReceivedDepthDataCallback func(time.Time, DepthData) error 28 | 29 | // websocket 30 | type WsClient struct { 31 | WsEndPoint string 32 | WsApi *ApiInfo 33 | conn *websocket.Conn 34 | sendCh chan string //发消息队列 35 | resCh chan *Msg //收消息队列 36 | 37 | errCh chan *Msg 38 | regCh map[Event]chan *Msg //请求响应队列 39 | 40 | quitCh chan struct{} 41 | lock sync.RWMutex 42 | 43 | onMessageHook ReceivedDataCallback //全局消息回调函数 44 | onBookMsgHook ReceivedMsgDataCallback //普通订阅消息回调函数 45 | onDepthHook ReceivedDepthDataCallback //深度订阅消息回调函数 46 | OnErrorHook ReceivedDataCallback //错误处理回调函数 47 | 48 | // 记录深度信息 49 | DepthDataList map[string]DepthDetail 50 | autoDepthMgr bool // 深度数据管理(checksum等) 51 | DepthDataLock sync.RWMutex 52 | 53 | isStarted bool //防止重复启动和关闭 54 | dailTimeout time.Duration 55 | } 56 | 57 | /* 58 | 服务端响应详细信息 59 | Timestamp: 接受到消息的时间 60 | Info: 接受到的消息字符串 61 | */ 62 | type Msg struct { 63 | Timestamp time.Time `json:"timestamp"` 64 | Info interface{} `json:"info"` 65 | } 66 | 67 | func (this *Msg) Print() { 68 | fmt.Println("【消息时间】", this.Timestamp.Format("2006-01-02 15:04:05.000")) 69 | str, _ := json.Marshal(this.Info) 70 | fmt.Println("【消息内容】", string(str)) 71 | } 72 | 73 | /* 74 | 订阅结果封装后的消息结构体 75 | */ 76 | type ProcessDetail struct { 77 | EndPoint string `json:"endPoint"` 78 | ReqInfo string `json:"ReqInfo"` //订阅请求 79 | SendTime time.Time `json:"sendTime"` //发送订阅请求的时间 80 | RecvTime time.Time `json:"recvTime"` //接受到订阅结果的时间 81 | UsedTime time.Duration `json:"UsedTime"` //耗时 82 | Data []*Msg `json:"data"` //订阅结果数据 83 | } 84 | 85 | func (p *ProcessDetail) String() string { 86 | data, _ := json.Marshal(p) 87 | return string(data) 88 | } 89 | 90 | // 创建ws对象 91 | func NewWsClient(ep string) (r *WsClient, err error) { 92 | if ep == "" { 93 | err = errors.New("websocket endpoint cannot be null") 94 | return 95 | } 96 | 97 | r = &WsClient{ 98 | WsEndPoint: ep, 99 | sendCh: make(chan string), 100 | resCh: make(chan *Msg), 101 | errCh: make(chan *Msg), 102 | regCh: make(map[Event]chan *Msg), 103 | //cbs: make(map[Event]ReceivedDataCallback), 104 | quitCh: make(chan struct{}), 105 | DepthDataList: make(map[string]DepthDetail), 106 | dailTimeout: time.Second * 5, 107 | // 自动深度校验默认开启 108 | autoDepthMgr: true, 109 | } 110 | 111 | return 112 | } 113 | 114 | /* 115 | 新增记录深度信息 116 | */ 117 | func (a *WsClient) addDepthDataList(key string, dd DepthDetail) error { 118 | a.DepthDataLock.Lock() 119 | defer a.DepthDataLock.Unlock() 120 | a.DepthDataList[key] = dd 121 | return nil 122 | } 123 | 124 | /* 125 | 更新记录深度信息(如果没有记录不会更新成功) 126 | */ 127 | func (a *WsClient) updateDepthDataList(key string, dd DepthDetail) error { 128 | a.DepthDataLock.Lock() 129 | defer a.DepthDataLock.Unlock() 130 | if _, ok := a.DepthDataList[key]; !ok { 131 | return errors.New("更新失败!未发现记录" + key) 132 | } 133 | 134 | a.DepthDataList[key] = dd 135 | return nil 136 | } 137 | 138 | /* 139 | 删除记录深度信息 140 | */ 141 | func (a *WsClient) deleteDepthDataList(key string) error { 142 | a.DepthDataLock.Lock() 143 | defer a.DepthDataLock.Unlock() 144 | delete(a.DepthDataList, key) 145 | return nil 146 | } 147 | 148 | /* 149 | 设置是否自动深度管理,开启 true,关闭 false 150 | */ 151 | func (a *WsClient) EnableAutoDepthMgr(b bool) error { 152 | a.DepthDataLock.Lock() 153 | defer a.DepthDataLock.Unlock() 154 | 155 | if len(a.DepthDataList) != 0 { 156 | err := errors.New("当前有深度数据处于订阅中") 157 | return err 158 | } 159 | 160 | a.autoDepthMgr = b 161 | return nil 162 | } 163 | 164 | /* 165 | 获取当前的深度快照信息(合并后的) 166 | */ 167 | func (a *WsClient) GetSnapshotByChannel(data DepthData) (snapshot *DepthDetail, err error) { 168 | key, err := json.Marshal(data.Arg) 169 | if err != nil { 170 | return 171 | } 172 | a.DepthDataLock.Lock() 173 | defer a.DepthDataLock.Unlock() 174 | val, ok := a.DepthDataList[string(key)] 175 | if !ok { 176 | return 177 | } 178 | snapshot = new(DepthDetail) 179 | raw, err := json.Marshal(val) 180 | if err != nil { 181 | return 182 | } 183 | err = json.Unmarshal(raw, &snapshot) 184 | if err != nil { 185 | return 186 | } 187 | return 188 | } 189 | 190 | // 设置dial超时时间 191 | func (a *WsClient) SetDailTimeout(tm time.Duration) { 192 | a.dailTimeout = tm 193 | } 194 | 195 | // 非阻塞启动 196 | func (a *WsClient) Start() error { 197 | a.lock.RLock() 198 | if a.isStarted { 199 | a.lock.RUnlock() 200 | fmt.Println("ws已经启动") 201 | return nil 202 | } else { 203 | a.lock.RUnlock() 204 | a.lock.Lock() 205 | defer a.lock.Unlock() 206 | // 增加超时处理 207 | done := make(chan struct{}) 208 | ctx, cancel := context.WithTimeout(context.Background(), a.dailTimeout) 209 | defer cancel() 210 | go func(ctx context.Context) { 211 | defer func() { 212 | close(done) 213 | }() 214 | var c *websocket.Conn 215 | c, _, err := websocket.DefaultDialer.Dial(a.WsEndPoint, nil) 216 | if err != nil { 217 | err = errors.New("dial error:" + err.Error()) 218 | return 219 | } 220 | a.conn = c 221 | 222 | }(ctx) 223 | select { 224 | case <-ctx.Done(): 225 | err := errors.New("连接超时退出!") 226 | return err 227 | case <-done: 228 | 229 | } 230 | 231 | go a.receive() 232 | go a.work() 233 | a.isStarted = true 234 | log.Println("客户端已启动!", a.WsEndPoint) 235 | return nil 236 | } 237 | } 238 | 239 | // 客户端退出消息channel 240 | func (a *WsClient) IsQuit() <-chan struct{} { 241 | return a.quitCh 242 | } 243 | 244 | func (a *WsClient) work() { 245 | defer func() { 246 | a.Stop() 247 | err := recover() 248 | if err != nil { 249 | log.Printf("work End. Recover msg: %+v", a) 250 | debug.PrintStack() 251 | } 252 | 253 | }() 254 | 255 | ticker := time.NewTicker(10 * time.Second) 256 | defer ticker.Stop() 257 | 258 | for { 259 | select { 260 | case <-ticker.C: // 保持心跳 261 | // go a.Ping(1000) 262 | go func() { 263 | _, _, err := a.Ping(1000) 264 | if err != nil { 265 | fmt.Println("心跳检测失败!", err) 266 | a.Stop() 267 | return 268 | } 269 | 270 | }() 271 | 272 | case <-a.quitCh: // 保持心跳 273 | return 274 | case data, ok := <-a.resCh: //接收到服务端发来的消息 275 | if !ok { 276 | return 277 | } 278 | //log.Println("接收到来自resCh的消息:", data) 279 | if a.onMessageHook != nil { 280 | err := a.onMessageHook(data) 281 | if err != nil { 282 | log.Println("执行onMessageHook函数错误!", err) 283 | } 284 | } 285 | case errMsg, ok := <-a.errCh: //错误处理 286 | if !ok { 287 | return 288 | } 289 | if a.OnErrorHook != nil { 290 | err := a.OnErrorHook(errMsg) 291 | if err != nil { 292 | log.Println("执行OnErrorHook函数错误!", err) 293 | } 294 | } 295 | case req, ok := <-a.sendCh: //从发送队列中取出数据发送到服务端 296 | if !ok { 297 | return 298 | } 299 | //log.Println("接收到来自req的消息:", req) 300 | err := a.conn.WriteMessage(websocket.TextMessage, []byte(req)) 301 | if err != nil { 302 | log.Printf("发送请求失败: %s\n", err) 303 | return 304 | } 305 | log.Printf("[发送请求] %v\n", req) 306 | } 307 | } 308 | 309 | } 310 | 311 | /* 312 | 处理接受到的消息 313 | */ 314 | func (a *WsClient) receive() { 315 | defer func() { 316 | a.Stop() 317 | err := recover() 318 | if err != nil { 319 | log.Printf("Receive End. Recover msg: %+v", a) 320 | debug.PrintStack() 321 | } 322 | 323 | }() 324 | 325 | for { 326 | messageType, message, err := a.conn.ReadMessage() 327 | if err != nil { 328 | if a.isStarted { 329 | log.Println("receive message error!" + err.Error()) 330 | } 331 | 332 | break 333 | } 334 | 335 | txtMsg := message 336 | switch messageType { 337 | case websocket.TextMessage: 338 | case websocket.BinaryMessage: 339 | txtMsg, err = GzipDecode(message) 340 | if err != nil { 341 | log.Println("解压失败!") 342 | continue 343 | } 344 | } 345 | 346 | log.Println("[收到消息]", string(txtMsg)) 347 | 348 | //发送结果到默认消息处理通道 349 | 350 | timestamp := time.Now() 351 | msg := &Msg{Timestamp: timestamp, Info: string(txtMsg)} 352 | 353 | a.resCh <- msg 354 | 355 | evt, data, err := a.parseMessage(txtMsg) 356 | if err != nil { 357 | log.Println("解析消息失败!", err) 358 | continue 359 | } 360 | 361 | //log.Println("解析消息成功!消息类型 =", evt) 362 | 363 | a.lock.RLock() 364 | ch, ok := a.regCh[evt] 365 | a.lock.RUnlock() 366 | if !ok { 367 | //只有推送消息才会主动创建通道和消费队列 368 | if evt == EVENT_BOOKED_DATA || evt == EVENT_DEPTH_DATA { 369 | //log.Println("channel不存在!event:", evt) 370 | //a.lock.RUnlock() 371 | a.lock.Lock() 372 | a.regCh[evt] = make(chan *Msg) 373 | ch = a.regCh[evt] 374 | a.lock.Unlock() 375 | 376 | //log.Println("创建", evt, "通道") 377 | 378 | // 创建消费队列 379 | go func(evt Event) { 380 | //log.Println("创建goroutine evt:", evt) 381 | 382 | for msg := range a.regCh[evt] { 383 | //log.Println(msg) 384 | // msg.Print() 385 | switch evt { 386 | // 处理普通推送数据 387 | case EVENT_BOOKED_DATA: 388 | fn := a.onBookMsgHook 389 | if fn != nil { 390 | err = fn(msg.Timestamp, msg.Info.(MsgData)) 391 | if err != nil { 392 | log.Println("订阅数据回调函数执行失败!", err) 393 | } 394 | //log.Println("函数执行成功!", err) 395 | } 396 | // 处理深度推送数据 397 | case EVENT_DEPTH_DATA: 398 | fn := a.onDepthHook 399 | 400 | depData := msg.Info.(DepthData) 401 | 402 | // 开启深度数据管理功能的,会合并深度数据 403 | if a.autoDepthMgr { 404 | a.MergeDepth(depData) 405 | } 406 | 407 | // 运行用户定义回调函数 408 | if fn != nil { 409 | err = fn(msg.Timestamp, msg.Info.(DepthData)) 410 | if err != nil { 411 | log.Println("深度回调函数执行失败!", err) 412 | } 413 | 414 | } 415 | } 416 | 417 | } 418 | //log.Println("退出goroutine evt:", evt) 419 | }(evt) 420 | 421 | //continue 422 | } else { 423 | //log.Println("程序异常!通道已关闭", evt) 424 | continue 425 | } 426 | 427 | } 428 | 429 | //log.Println(evt,"事件已注册",ch) 430 | 431 | ctx := context.Background() 432 | ctx, cancel := context.WithTimeout(ctx, time.Millisecond*1000) 433 | select { 434 | /* 435 | 丢弃消息容易引发数据处理处理错误 436 | */ 437 | // case <-ctx.Done(): 438 | // log.Println("等待超时,消息丢弃 - ", data) 439 | case ch <- &Msg{Timestamp: timestamp, Info: data}: 440 | } 441 | cancel() 442 | } 443 | } 444 | 445 | /* 446 | 开启了深度数据管理功能后,系统会自动合并深度信息 447 | */ 448 | func (a *WsClient) MergeDepth(depData DepthData) (err error) { 449 | if !a.autoDepthMgr { 450 | return 451 | } 452 | 453 | key, err := json.Marshal(depData.Arg) 454 | if err != nil { 455 | err = errors.New("数据错误") 456 | return 457 | } 458 | 459 | // books5 不需要做checksum 460 | if depData.Arg["channel"] == "books5" { 461 | a.addDepthDataList(string(key), depData.Data[0]) 462 | return 463 | } 464 | 465 | if depData.Action == "snapshot" { 466 | 467 | _, err = depData.CheckSum(nil) 468 | if err != nil { 469 | log.Println("校验失败", err) 470 | return 471 | } 472 | 473 | a.addDepthDataList(string(key), depData.Data[0]) 474 | 475 | } else { 476 | 477 | var newSnapshot *DepthDetail 478 | a.DepthDataLock.RLock() 479 | oldSnapshot, ok := a.DepthDataList[string(key)] 480 | if !ok { 481 | log.Println("深度数据错误,全量数据未发现!") 482 | err = errors.New("数据错误") 483 | return 484 | } 485 | a.DepthDataLock.RUnlock() 486 | newSnapshot, err = depData.CheckSum(&oldSnapshot) 487 | if err != nil { 488 | log.Println("深度校验失败", err) 489 | err = errors.New("校验失败") 490 | return 491 | } 492 | 493 | a.updateDepthDataList(string(key), *newSnapshot) 494 | } 495 | return 496 | } 497 | 498 | /* 499 | 通过ErrorCode判断事件类型 500 | */ 501 | func GetInfoFromErrCode(data ErrData) Event { 502 | switch data.Code { 503 | case "60001": 504 | return EVENT_LOGIN 505 | case "60002": 506 | return EVENT_LOGIN 507 | case "60003": 508 | return EVENT_LOGIN 509 | case "60004": 510 | return EVENT_LOGIN 511 | case "60005": 512 | return EVENT_LOGIN 513 | case "60006": 514 | return EVENT_LOGIN 515 | case "60007": 516 | return EVENT_LOGIN 517 | case "60008": 518 | return EVENT_LOGIN 519 | case "60009": 520 | return EVENT_LOGIN 521 | case "60010": 522 | return EVENT_LOGIN 523 | case "60011": 524 | return EVENT_LOGIN 525 | } 526 | 527 | return EVENT_UNKNOWN 528 | } 529 | 530 | /* 531 | 从error返回中解析出对应的channel 532 | error信息样例 533 | {"event":"error","msg":"channel:index-tickers,instId:BTC-USDT1 doesn't exist","code":"60018"} 534 | */ 535 | func GetInfoFromErrMsg(raw string) (channel string) { 536 | reg := regexp.MustCompile(`channel:(.*?),`) 537 | if reg == nil { 538 | fmt.Println("MustCompile err") 539 | return 540 | } 541 | //提取关键信息 542 | result := reg.FindAllStringSubmatch(raw, -1) 543 | for _, text := range result { 544 | channel = text[1] 545 | } 546 | return 547 | } 548 | 549 | /* 550 | 解析消息类型 551 | */ 552 | func (a *WsClient) parseMessage(raw []byte) (evt Event, data interface{}, err error) { 553 | evt = EVENT_UNKNOWN 554 | //log.Println("解析消息") 555 | //log.Println("消息内容:", string(raw)) 556 | if string(raw) == "pong" { 557 | evt = EVENT_PING 558 | data = raw 559 | return 560 | } 561 | //log.Println(0, evt) 562 | var rspData = RspData{} 563 | err = json.Unmarshal(raw, &rspData) 564 | if err == nil { 565 | op := rspData.Event 566 | if op == OP_SUBSCRIBE || op == OP_UNSUBSCRIBE { 567 | channel := rspData.Arg["channel"] 568 | evt = GetEventId(channel) 569 | data = rspData 570 | return 571 | } 572 | } 573 | 574 | //log.Println("ErrData") 575 | var errData = ErrData{} 576 | err = json.Unmarshal(raw, &errData) 577 | if err == nil { 578 | op := errData.Event 579 | switch op { 580 | case OP_LOGIN: 581 | evt = EVENT_LOGIN 582 | data = errData 583 | //log.Println(3, evt) 584 | return 585 | case OP_ERROR: 586 | data = errData 587 | // TODO:细化报错对应的事件判断 588 | 589 | //尝试从msg字段中解析对应的事件类型 590 | evt = GetInfoFromErrCode(errData) 591 | if evt != EVENT_UNKNOWN { 592 | return 593 | } 594 | evt = GetEventId(GetInfoFromErrMsg(errData.Msg)) 595 | if evt == EVENT_UNKNOWN { 596 | evt = EVENT_ERROR 597 | return 598 | } 599 | return 600 | } 601 | //log.Println(5, evt) 602 | } 603 | 604 | //log.Println("JRPCRsp") 605 | var jRPCRsp = JRPCRsp{} 606 | err = json.Unmarshal(raw, &jRPCRsp) 607 | if err == nil { 608 | data = jRPCRsp 609 | evt = GetEventId(jRPCRsp.Op) 610 | if evt != EVENT_UNKNOWN { 611 | return 612 | } 613 | } 614 | 615 | var depthData = DepthData{} 616 | err = json.Unmarshal(raw, &depthData) 617 | if err == nil { 618 | evt = EVENT_DEPTH_DATA 619 | data = depthData 620 | //log.Println("-->>EVENT_DEPTH_DATA", evt) 621 | //log.Println(evt, data) 622 | //log.Println(6) 623 | switch depthData.Arg["channel"] { 624 | case "books": 625 | return 626 | case "books-l2-tbt": 627 | return 628 | case "books50-l2-tbt": 629 | return 630 | case "books5": 631 | return 632 | default: 633 | 634 | } 635 | } 636 | 637 | //log.Println("MsgData") 638 | var msgData = MsgData{} 639 | err = json.Unmarshal(raw, &msgData) 640 | if err == nil { 641 | evt = EVENT_BOOKED_DATA 642 | data = msgData 643 | //log.Println("-->>EVENT_BOOK_DATA", evt) 644 | //log.Println(evt, data) 645 | //log.Println(6) 646 | return 647 | } 648 | 649 | evt = EVENT_UNKNOWN 650 | err = errors.New("message unknown") 651 | return 652 | } 653 | 654 | func (a *WsClient) Stop() error { 655 | 656 | a.lock.Lock() 657 | defer a.lock.Unlock() 658 | if !a.isStarted { 659 | return nil 660 | } 661 | 662 | a.isStarted = false 663 | 664 | if a.conn != nil { 665 | a.conn.Close() 666 | } 667 | close(a.errCh) 668 | close(a.sendCh) 669 | close(a.resCh) 670 | close(a.quitCh) 671 | 672 | for _, ch := range a.regCh { 673 | close(ch) 674 | } 675 | 676 | log.Println("ws客户端退出!") 677 | return nil 678 | } 679 | 680 | /* 681 | 添加全局消息处理的回调函数 682 | */ 683 | func (a *WsClient) AddMessageHook(fn ReceivedDataCallback) error { 684 | a.onMessageHook = fn 685 | return nil 686 | } 687 | 688 | /* 689 | 添加订阅消息处理的回调函数 690 | */ 691 | func (a *WsClient) AddBookMsgHook(fn ReceivedMsgDataCallback) error { 692 | a.onBookMsgHook = fn 693 | return nil 694 | } 695 | 696 | /* 697 | 添加深度消息处理的回调函数 698 | 例如: 699 | cli.AddDepthHook(func(ts time.Time, data DepthData) error { return nil }) 700 | */ 701 | func (a *WsClient) AddDepthHook(fn ReceivedDepthDataCallback) error { 702 | a.onDepthHook = fn 703 | return nil 704 | } 705 | 706 | /* 707 | 添加错误类型消息处理的回调函数 708 | */ 709 | func (a *WsClient) AddErrMsgHook(fn ReceivedDataCallback) error { 710 | a.OnErrorHook = fn 711 | return nil 712 | } 713 | 714 | /* 715 | 判断连接是否存活 716 | */ 717 | func (a *WsClient) IsAlive() bool { 718 | res := false 719 | if a.conn == nil { 720 | return res 721 | } 722 | res, _, _ = a.Ping(500) 723 | return res 724 | } 725 | -------------------------------------------------------------------------------- /ws/ws_contants.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | //操作符 4 | const ( 5 | OP_LOGIN = "login" 6 | OP_ERROR = "error" 7 | OP_SUBSCRIBE = "subscribe" 8 | OP_UNSUBSCRIBE = "unsubscribe" 9 | ) 10 | 11 | // instrument Type 12 | const ( 13 | SPOT = "SPOT" 14 | SWAP = "SWAP" 15 | FUTURES = "FUTURES" 16 | OPTION = "OPTION" 17 | ANY = "ANY" 18 | ) 19 | -------------------------------------------------------------------------------- /ws/ws_jrpc.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | . "v5sdk_go/ws/wImpl" 8 | ) 9 | 10 | /* 11 | websocket交易 通用请求 12 | 参数说明: 13 | evtId:封装的事件类型 14 | id: 请求ID 15 | op: 请求参数op 16 | params: 请求参数 17 | timeOut: 超时时间 18 | */ 19 | func (a *WsClient) jrpcReq(evtId Event, op string, id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 20 | res = true 21 | tm := 5000 22 | if len(timeOut) != 0 { 23 | tm = timeOut[0] 24 | } 25 | 26 | req := &JRPCReq{ 27 | Id: id, 28 | Op: op, 29 | Args: params, 30 | } 31 | 32 | detail = &ProcessDetail{ 33 | EndPoint: a.WsEndPoint, 34 | } 35 | 36 | ctx := context.Background() 37 | ctx, cancel := context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) 38 | defer cancel() 39 | ctx = context.WithValue(ctx, "detail", detail) 40 | 41 | msg, err := a.process(ctx, evtId, req) 42 | if err != nil { 43 | res = false 44 | log.Println("处理请求失败!", req, err) 45 | return 46 | } 47 | detail.Data = msg 48 | 49 | res, err = checkResult(req, msg) 50 | if err != nil { 51 | res = false 52 | return 53 | } 54 | 55 | return 56 | } 57 | 58 | /* 59 | 单个下单 60 | 参数说明: 61 | id: 请求ID 62 | params: 请求参数 63 | timeOut: 超时时间 64 | */ 65 | func (a *WsClient) PlaceOrder(id string, param map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 66 | op := "order" 67 | evtId := EVENT_PLACE_ORDER 68 | 69 | var args []map[string]interface{} 70 | args = append(args, param) 71 | 72 | return a.jrpcReq(evtId, op, id, args, timeOut...) 73 | 74 | } 75 | 76 | /* 77 | 批量下单 78 | 参数说明: 79 | id: 请求ID 80 | params: 请求参数 81 | timeOut: 超时时间 82 | */ 83 | func (a *WsClient) BatchPlaceOrders(id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 84 | 85 | op := "batch-orders" 86 | evtId := EVENT_PLACE_BATCH_ORDERS 87 | return a.jrpcReq(evtId, op, id, params, timeOut...) 88 | 89 | } 90 | 91 | /* 92 | 单个撤单 93 | 参数说明: 94 | id: 请求ID 95 | params: 请求参数 96 | timeOut: 超时时间 97 | */ 98 | func (a *WsClient) CancelOrder(id string, param map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 99 | 100 | op := "cancel-order" 101 | evtId := EVENT_CANCEL_ORDER 102 | 103 | var args []map[string]interface{} 104 | args = append(args, param) 105 | 106 | return a.jrpcReq(evtId, op, id, args, timeOut...) 107 | 108 | } 109 | 110 | /* 111 | 批量撤单 112 | 参数说明: 113 | id: 请求ID 114 | params: 请求参数 115 | timeOut: 超时时间 116 | */ 117 | func (a *WsClient) BatchCancelOrders(id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 118 | 119 | op := "batch-cancel-orders" 120 | evtId := EVENT_CANCEL_BATCH_ORDERS 121 | return a.jrpcReq(evtId, op, id, params, timeOut...) 122 | 123 | } 124 | 125 | /* 126 | 单个改单 127 | 参数说明: 128 | id: 请求ID 129 | params: 请求参数 130 | timeOut: 超时时间 131 | */ 132 | func (a *WsClient) AmendOrder(id string, param map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 133 | 134 | op := "amend-order" 135 | evtId := EVENT_AMEND_ORDER 136 | 137 | var args []map[string]interface{} 138 | args = append(args, param) 139 | 140 | return a.jrpcReq(evtId, op, id, args, timeOut...) 141 | 142 | } 143 | 144 | /* 145 | 批量改单 146 | 参数说明: 147 | id: 请求ID 148 | params: 请求参数 149 | timeOut: 超时时间 150 | */ 151 | func (a *WsClient) BatchAmendOrders(id string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 152 | 153 | op := "batch-amend-orders" 154 | evtId := EVENT_AMEND_BATCH_ORDERS 155 | return a.jrpcReq(evtId, op, id, params, timeOut...) 156 | 157 | } 158 | -------------------------------------------------------------------------------- /ws/ws_jrpc_test.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | . "v5sdk_go/ws/wImpl" 8 | ) 9 | 10 | func PrintDetail(d *ProcessDetail) { 11 | fmt.Println("[详细信息]") 12 | fmt.Println("请求地址:", d.EndPoint) 13 | fmt.Println("请求内容:", d.ReqInfo) 14 | fmt.Println("发送时间:", d.SendTime.Format("2006-01-02 15:04:05.000")) 15 | fmt.Println("响应时间:", d.RecvTime.Format("2006-01-02 15:04:05.000")) 16 | fmt.Println("耗时:", d.UsedTime.String()) 17 | fmt.Printf("接受到 %v 条消息:\n", len(d.Data)) 18 | for _, v := range d.Data { 19 | fmt.Printf("[%v] %v\n", v.Timestamp.Format("2006-01-02 15:04:05.000"), v.Info) 20 | } 21 | } 22 | 23 | func (r *WsClient) makeOrder(instId string, tdMode string, side string, ordType string, px string, sz string) (orderId string, err error) { 24 | 25 | var res bool 26 | var data *ProcessDetail 27 | 28 | param := map[string]interface{}{} 29 | param["instId"] = instId 30 | param["tdMode"] = tdMode 31 | param["side"] = side 32 | param["ordType"] = ordType 33 | if px != "" { 34 | param["px"] = px 35 | } 36 | param["sz"] = sz 37 | 38 | res, data, err = r.PlaceOrder("0011", param) 39 | if err != nil { 40 | return 41 | } 42 | if res && len(data.Data) == 1 { 43 | rsp := data.Data[0].Info.(JRPCRsp) 44 | if len(rsp.Data) == 1 { 45 | val, ok := rsp.Data[0]["ordId"] 46 | if !ok { 47 | return 48 | } 49 | orderId = val.(string) 50 | return 51 | } 52 | } 53 | 54 | return 55 | } 56 | 57 | /* 58 | 单个下单 59 | */ 60 | func TestPlaceOrder(t *testing.T) { 61 | r := prework_pri(CROSS_ACCOUNT) 62 | //r := prework_pri(TRADE_ACCOUNT) 63 | var res bool 64 | var err error 65 | var data *ProcessDetail 66 | 67 | start := time.Now() 68 | param := map[string]interface{}{} 69 | param["instId"] = "BTC-USDT" 70 | param["tdMode"] = "cash" 71 | param["side"] = "buy" 72 | param["ordType"] = "market" 73 | //param["px"] = "1" 74 | param["sz"] = "200" 75 | 76 | res, data, err = r.PlaceOrder("0011", param) 77 | if res { 78 | usedTime := time.Since(start) 79 | fmt.Println("下单成功!", usedTime.String()) 80 | PrintDetail(data) 81 | } else { 82 | usedTime := time.Since(start) 83 | fmt.Println("下单失败!", usedTime.String(), err) 84 | } 85 | 86 | } 87 | 88 | /* 89 | 批量下单 90 | */ 91 | func TestPlaceBatchOrder(t *testing.T) { 92 | r := prework_pri(CROSS_ACCOUNT) 93 | var res bool 94 | var err error 95 | var data *ProcessDetail 96 | 97 | start := time.Now() 98 | var params []map[string]interface{} 99 | param := map[string]interface{}{} 100 | param["instId"] = "BTC-USDT" 101 | param["tdMode"] = "cash" 102 | param["side"] = "sell" 103 | param["ordType"] = "market" 104 | param["sz"] = "0.001" 105 | params = append(params, param) 106 | param = map[string]interface{}{} 107 | param["instId"] = "BTC-USDT" 108 | param["tdMode"] = "cash" 109 | param["side"] = "buy" 110 | param["ordType"] = "market" 111 | param["sz"] = "100" 112 | params = append(params, param) 113 | res, data, err = r.BatchPlaceOrders("001", params) 114 | usedTime := time.Since(start) 115 | if err != nil { 116 | fmt.Println("下单失败!", err, usedTime.String()) 117 | t.Fail() 118 | } 119 | if res { 120 | fmt.Println("下单成功!", usedTime.String()) 121 | PrintDetail(data) 122 | } else { 123 | 124 | fmt.Println("下单失败!", usedTime.String()) 125 | t.Fail() 126 | } 127 | 128 | } 129 | 130 | /* 131 | 撤销订单 132 | */ 133 | func TestCancelOrder(t *testing.T) { 134 | r := prework_pri(CROSS_ACCOUNT) 135 | 136 | // 用户自定义limit限价价格 137 | ordId, _ := r.makeOrder("BTC-USDT", "cash", "sell", "limit", "57000", "0.01") 138 | if ordId == "" { 139 | t.Fatal() 140 | } 141 | 142 | t.Log("生成挂单:orderId=", ordId) 143 | 144 | param := map[string]interface{}{} 145 | param["instId"] = "BTC-USDT" 146 | param["ordId"] = ordId 147 | start := time.Now() 148 | res, _, _ := r.CancelOrder("1", param) 149 | if res { 150 | usedTime := time.Since(start) 151 | fmt.Println("撤单成功!", usedTime.String()) 152 | } else { 153 | t.Fatal("撤单失败!") 154 | } 155 | } 156 | 157 | /* 158 | 修改订单 159 | */ 160 | func TestAmendlOrder(t *testing.T) { 161 | r := prework_pri(CROSS_ACCOUNT) 162 | 163 | // 用户自定义limit限价价格 164 | ordId, _ := r.makeOrder("BTC-USDT", "cash", "sell", "limit", "57000", "0.01") 165 | if ordId == "" { 166 | t.Fatal() 167 | } 168 | 169 | t.Log("生成挂单:orderId=", ordId) 170 | 171 | param := map[string]interface{}{} 172 | param["instId"] = "BTC-USDT" 173 | param["ordId"] = ordId 174 | // 调整修改订单的参数 175 | //param["newSz"] = "0.02" 176 | param["newPx"] = "57001" 177 | 178 | start := time.Now() 179 | res, _, _ := r.AmendOrder("1", param) 180 | if res { 181 | usedTime := time.Since(start) 182 | fmt.Println("修改订单成功!", usedTime.String()) 183 | } else { 184 | t.Fatal("修改订单失败!") 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /ws/ws_middleware.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import "fmt" 4 | 5 | type ReqFunc func(...interface{}) (res bool, msg *Msg, err error) 6 | type Decorator func(ReqFunc) ReqFunc 7 | 8 | func handler(h ReqFunc, decors ...Decorator) ReqFunc { 9 | for i := range decors { 10 | d := decors[len(decors)-1-i] 11 | h = d(h) 12 | } 13 | return h 14 | } 15 | 16 | func preprocess() (res bool, msg *Msg, err error) { 17 | fmt.Println("preprocess") 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /ws/ws_op.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "sync" 8 | "time" 9 | . "v5sdk_go/config" 10 | "v5sdk_go/rest" 11 | . "v5sdk_go/utils" 12 | . "v5sdk_go/ws/wImpl" 13 | . "v5sdk_go/ws/wInterface" 14 | ) 15 | 16 | /* 17 | Ping服务端保持心跳。 18 | timeOut:超时时间(毫秒),如果不填默认为5000ms 19 | */ 20 | func (a *WsClient) Ping(timeOut ...int) (res bool, detail *ProcessDetail, err error) { 21 | tm := 5000 22 | if len(timeOut) != 0 { 23 | tm = timeOut[0] 24 | } 25 | res = true 26 | 27 | detail = &ProcessDetail{ 28 | EndPoint: a.WsEndPoint, 29 | } 30 | 31 | ctx := context.Background() 32 | ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) 33 | ctx = context.WithValue(ctx, "detail", detail) 34 | msg, err := a.process(ctx, EVENT_PING, nil) 35 | if err != nil { 36 | res = false 37 | log.Println("处理请求失败!", err) 38 | return 39 | } 40 | detail.Data = msg 41 | 42 | if len(msg) == 0 { 43 | res = false 44 | return 45 | } 46 | 47 | str := string(msg[0].Info.([]byte)) 48 | if str != "pong" { 49 | res = false 50 | return 51 | } 52 | 53 | return 54 | } 55 | 56 | /* 57 | 登录私有频道 58 | */ 59 | func (a *WsClient) Login(apiKey, secKey, passPhrase string, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 60 | 61 | if apiKey == "" { 62 | err = errors.New("ApiKey cannot be null") 63 | return 64 | } 65 | 66 | if secKey == "" { 67 | err = errors.New("SecretKey cannot be null") 68 | return 69 | } 70 | 71 | if passPhrase == "" { 72 | err = errors.New("Passphrase cannot be null") 73 | return 74 | } 75 | 76 | a.WsApi = &ApiInfo{ 77 | ApiKey: apiKey, 78 | SecretKey: secKey, 79 | Passphrase: passPhrase, 80 | } 81 | 82 | tm := 5000 83 | if len(timeOut) != 0 { 84 | tm = timeOut[0] 85 | } 86 | res = true 87 | 88 | timestamp := EpochTime() 89 | 90 | preHash := PreHashString(timestamp, rest.GET, "/users/self/verify", "") 91 | //fmt.Println("preHash:", preHash) 92 | var sign string 93 | if sign, err = HmacSha256Base64Signer(preHash, secKey); err != nil { 94 | log.Println("处理签名失败!", err) 95 | return 96 | } 97 | 98 | args := map[string]string{} 99 | args["apiKey"] = apiKey 100 | args["passphrase"] = passPhrase 101 | args["timestamp"] = timestamp 102 | args["sign"] = sign 103 | req := &ReqData{ 104 | Op: OP_LOGIN, 105 | Args: []map[string]string{args}, 106 | } 107 | 108 | detail = &ProcessDetail{ 109 | EndPoint: a.WsEndPoint, 110 | } 111 | 112 | ctx := context.Background() 113 | ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) 114 | ctx = context.WithValue(ctx, "detail", detail) 115 | 116 | msg, err := a.process(ctx, EVENT_LOGIN, req) 117 | if err != nil { 118 | res = false 119 | log.Println("处理请求失败!", req, err) 120 | return 121 | } 122 | detail.Data = msg 123 | 124 | if len(msg) == 0 { 125 | res = false 126 | return 127 | } 128 | 129 | info, _ := msg[0].Info.(ErrData) 130 | 131 | if info.Code == "0" && info.Event == OP_LOGIN { 132 | log.Println("登录成功!") 133 | } else { 134 | log.Println("登录失败!") 135 | res = false 136 | return 137 | } 138 | 139 | return 140 | } 141 | 142 | /* 143 | 等待结果响应 144 | */ 145 | func (a *WsClient) waitForResult(e Event, timeOut int) (data interface{}, err error) { 146 | 147 | if _, ok := a.regCh[e]; !ok { 148 | a.lock.Lock() 149 | a.regCh[e] = make(chan *Msg) 150 | a.lock.Unlock() 151 | //log.Println("注册", e, "事件成功") 152 | } 153 | 154 | a.lock.RLock() 155 | defer a.lock.RUnlock() 156 | ch := a.regCh[e] 157 | //log.Println(e, "等待响应!") 158 | select { 159 | case <-time.After(time.Duration(timeOut) * time.Millisecond): 160 | log.Println(e, "超时未响应!") 161 | err = errors.New(e.String() + "超时未响应!") 162 | return 163 | case data = <-ch: 164 | //log.Println(data) 165 | } 166 | 167 | return 168 | } 169 | 170 | /* 171 | 发送消息到服务端 172 | */ 173 | func (a *WsClient) Send(ctx context.Context, op WSReqData) (err error) { 174 | select { 175 | case <-ctx.Done(): 176 | log.Println("发生失败退出!") 177 | err = errors.New("发送超时退出!") 178 | case a.sendCh <- op.ToString(): 179 | } 180 | 181 | return 182 | } 183 | 184 | func (a *WsClient) process(ctx context.Context, e Event, op WSReqData) (data []*Msg, err error) { 185 | defer func() { 186 | _ = recover() 187 | }() 188 | 189 | var detail *ProcessDetail 190 | if val := ctx.Value("detail"); val != nil { 191 | detail = val.(*ProcessDetail) 192 | } else { 193 | detail = &ProcessDetail{ 194 | EndPoint: a.WsEndPoint, 195 | } 196 | } 197 | defer func() { 198 | //fmt.Println("处理完成,", e.String()) 199 | detail.UsedTime = detail.RecvTime.Sub(detail.SendTime) 200 | }() 201 | 202 | //查看事件是否被注册 203 | if _, ok := a.regCh[e]; !ok { 204 | a.lock.Lock() 205 | a.regCh[e] = make(chan *Msg) 206 | a.lock.Unlock() 207 | //log.Println("注册", e, "事件成功") 208 | } else { 209 | //log.Println("事件", e, "已注册!") 210 | err = errors.New("事件" + e.String() + "尚未处理完毕") 211 | return 212 | } 213 | 214 | //预期请求响应的条数 215 | expectCnt := 1 216 | if op != nil { 217 | expectCnt = op.Len() 218 | } 219 | recvCnt := 0 220 | 221 | //等待完成通知 222 | wg := sync.WaitGroup{} 223 | wg.Add(1) 224 | go func(ctx context.Context) { 225 | defer func() { 226 | a.lock.Lock() 227 | delete(a.regCh, e) 228 | //log.Println("事件已注销!",e) 229 | a.lock.Unlock() 230 | wg.Done() 231 | }() 232 | 233 | a.lock.RLock() 234 | ch := a.regCh[e] 235 | a.lock.RUnlock() 236 | 237 | //log.Println(e, "等待响应!") 238 | done := false 239 | ok := true 240 | for { 241 | var item *Msg 242 | select { 243 | case <-ctx.Done(): 244 | log.Println(e, "超时未响应!") 245 | err = errors.New(e.String() + "超时未响应!") 246 | return 247 | case item, ok = <-ch: 248 | if !ok { 249 | return 250 | } 251 | detail.RecvTime = time.Now() 252 | //log.Println(e, "接受到数据", item) 253 | data = append(data, item) 254 | recvCnt++ 255 | //log.Println(data) 256 | if recvCnt == expectCnt { 257 | done = true 258 | break 259 | } 260 | } 261 | if done { 262 | break 263 | } 264 | } 265 | if ok { 266 | close(ch) 267 | } 268 | 269 | }(ctx) 270 | 271 | switch e { 272 | case EVENT_PING: 273 | msg := "ping" 274 | detail.ReqInfo = msg 275 | a.sendCh <- msg 276 | detail.SendTime = time.Now() 277 | default: 278 | detail.ReqInfo = op.ToString() 279 | err = a.Send(ctx, op) 280 | if err != nil { 281 | log.Println("发送[", e, "]消息失败!", err) 282 | return 283 | } 284 | detail.SendTime = time.Now() 285 | } 286 | 287 | wg.Wait() 288 | return 289 | } 290 | 291 | /* 292 | 根据args请求参数判断请求类型 293 | 如:{"channel": "account","ccy": "BTC"} 类型为 EVENT_BOOK_ACCOUNT 294 | */ 295 | func GetEventByParam(param map[string]string) (evtId Event) { 296 | evtId = EVENT_UNKNOWN 297 | channel, ok := param["channel"] 298 | if !ok { 299 | return 300 | } 301 | 302 | evtId = GetEventId(channel) 303 | return 304 | } 305 | 306 | /* 307 | 订阅频道。 308 | req:请求json字符串 309 | */ 310 | func (a *WsClient) Subscribe(param map[string]string, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 311 | res = true 312 | tm := 5000 313 | if len(timeOut) != 0 { 314 | tm = timeOut[0] 315 | } 316 | 317 | evtid := GetEventByParam(param) 318 | if evtid == EVENT_UNKNOWN { 319 | err = errors.New("非法的请求参数!") 320 | return 321 | } 322 | 323 | var args []map[string]string 324 | args = append(args, param) 325 | 326 | req := ReqData{ 327 | Op: OP_SUBSCRIBE, 328 | Args: args, 329 | } 330 | 331 | detail = &ProcessDetail{ 332 | EndPoint: a.WsEndPoint, 333 | } 334 | 335 | ctx := context.Background() 336 | ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) 337 | ctx = context.WithValue(ctx, "detail", detail) 338 | 339 | msg, err := a.process(ctx, evtid, req) 340 | if err != nil { 341 | res = false 342 | log.Println("处理请求失败!", req, err) 343 | return 344 | } 345 | detail.Data = msg 346 | 347 | //检查所有频道是否都更新成功 348 | res, err = checkResult(req, msg) 349 | if err != nil { 350 | res = false 351 | return 352 | } 353 | 354 | return 355 | } 356 | 357 | /* 358 | 取消订阅频道。 359 | req:请求json字符串 360 | */ 361 | func (a *WsClient) UnSubscribe(param map[string]string, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 362 | res = true 363 | tm := 5000 364 | if len(timeOut) != 0 { 365 | tm = timeOut[0] 366 | } 367 | 368 | evtid := GetEventByParam(param) 369 | if evtid == EVENT_UNKNOWN { 370 | err = errors.New("非法的请求参数!") 371 | return 372 | } 373 | 374 | var args []map[string]string 375 | args = append(args, param) 376 | 377 | req := ReqData{ 378 | Op: OP_UNSUBSCRIBE, 379 | Args: args, 380 | } 381 | 382 | detail = &ProcessDetail{ 383 | EndPoint: a.WsEndPoint, 384 | } 385 | 386 | ctx := context.Background() 387 | ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) 388 | ctx = context.WithValue(ctx, "detail", detail) 389 | msg, err := a.process(ctx, evtid, req) 390 | if err != nil { 391 | res = false 392 | log.Println("处理请求失败!", req, err) 393 | return 394 | } 395 | detail.Data = msg 396 | //检查所有频道是否都更新成功 397 | res, err = checkResult(req, msg) 398 | if err != nil { 399 | res = false 400 | return 401 | } 402 | 403 | return 404 | } 405 | 406 | /* 407 | jrpc请求 408 | */ 409 | func (a *WsClient) Jrpc(id, op string, params []map[string]interface{}, timeOut ...int) (res bool, detail *ProcessDetail, err error) { 410 | res = true 411 | tm := 5000 412 | if len(timeOut) != 0 { 413 | tm = timeOut[0] 414 | } 415 | 416 | evtid := GetEventId(op) 417 | if evtid == EVENT_UNKNOWN { 418 | err = errors.New("非法的请求参数!") 419 | return 420 | } 421 | 422 | req := JRPCReq{ 423 | Id: id, 424 | Op: op, 425 | Args: params, 426 | } 427 | detail = &ProcessDetail{ 428 | EndPoint: a.WsEndPoint, 429 | } 430 | 431 | ctx := context.Background() 432 | ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) 433 | ctx = context.WithValue(ctx, "detail", detail) 434 | msg, err := a.process(ctx, evtid, req) 435 | if err != nil { 436 | res = false 437 | log.Println("处理请求失败!", req, err) 438 | return 439 | } 440 | detail.Data = msg 441 | 442 | //检查所有频道是否都更新成功 443 | res, err = checkResult(req, msg) 444 | if err != nil { 445 | res = false 446 | return 447 | } 448 | 449 | return 450 | } 451 | 452 | func (a *WsClient) PubChannel(evtId Event, op string, params []map[string]string, pd Period, timeOut ...int) (res bool, msg []*Msg, err error) { 453 | 454 | // 参数校验 455 | pa, err := checkParams(evtId, params, pd) 456 | if err != nil { 457 | return 458 | } 459 | 460 | res = true 461 | tm := 5000 462 | if len(timeOut) != 0 { 463 | tm = timeOut[0] 464 | } 465 | 466 | req := ReqData{ 467 | Op: op, 468 | Args: pa, 469 | } 470 | 471 | ctx := context.Background() 472 | ctx, _ = context.WithTimeout(ctx, time.Duration(tm)*time.Millisecond) 473 | msg, err = a.process(ctx, evtId, req) 474 | if err != nil { 475 | res = false 476 | log.Println("处理请求失败!", req, err) 477 | return 478 | } 479 | 480 | //检查所有频道是否都更新成功 481 | 482 | res, err = checkResult(req, msg) 483 | if err != nil { 484 | res = false 485 | return 486 | } 487 | 488 | return 489 | } 490 | 491 | // 参数校验 492 | func checkParams(evtId Event, params []map[string]string, pd Period) (res []map[string]string, err error) { 493 | 494 | channel := evtId.GetChannel(pd) 495 | if channel == "" { 496 | err = errors.New("参数校验失败!未知的类型:" + evtId.String()) 497 | return 498 | } 499 | log.Println(channel) 500 | if params == nil { 501 | tmp := make(map[string]string) 502 | tmp["channel"] = channel 503 | res = append(res, tmp) 504 | } else { 505 | //log.Println(params) 506 | for _, param := range params { 507 | 508 | tmp := make(map[string]string) 509 | for k, v := range param { 510 | tmp[k] = v 511 | } 512 | 513 | val, ok := tmp["channel"] 514 | if !ok { 515 | tmp["channel"] = channel 516 | } else { 517 | if val != channel { 518 | err = errors.New("参数校验失败!channel应为" + channel + val) 519 | return 520 | } 521 | } 522 | 523 | res = append(res, tmp) 524 | } 525 | } 526 | 527 | return 528 | } 529 | -------------------------------------------------------------------------------- /ws/ws_priv_channel.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | . "v5sdk_go/ws/wImpl" 5 | ) 6 | 7 | /* 8 | 订阅账户频道 9 | */ 10 | func (a *WsClient) PrivAccout(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 11 | return a.PubChannel(EVENT_BOOK_ACCOUNT, op, params, PERIOD_NONE, timeOut...) 12 | } 13 | 14 | /* 15 | 订阅持仓频道 16 | */ 17 | func (a *WsClient) PrivPostion(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 18 | return a.PubChannel(EVENT_BOOK_POSTION, op, params, PERIOD_NONE, timeOut...) 19 | } 20 | 21 | /* 22 | 订阅订单频道 23 | */ 24 | func (a *WsClient) PrivBookOrder(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 25 | return a.PubChannel(EVENT_BOOK_ORDER, op, params, PERIOD_NONE, timeOut...) 26 | } 27 | 28 | /* 29 | 订阅策略委托订单频道 30 | */ 31 | func (a *WsClient) PrivBookAlgoOrder(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 32 | return a.PubChannel(EVENT_BOOK_ALG_ORDER, op, params, PERIOD_NONE, timeOut...) 33 | } 34 | 35 | /* 36 | 订阅账户余额和持仓频道 37 | */ 38 | func (a *WsClient) PrivBalAndPos(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 39 | return a.PubChannel(EVENT_BOOK_B_AND_P, op, params, PERIOD_NONE, timeOut...) 40 | } 41 | -------------------------------------------------------------------------------- /ws/ws_priv_channel_test.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const ( 11 | TRADE_ACCOUNT = iota 12 | ISOLATE_ACCOUNT 13 | CROSS_ACCOUNT 14 | CROSS_ACCOUNT_B 15 | ) 16 | 17 | func prework_pri(t int) *WsClient { 18 | // 模拟环境 19 | ep := "wss://ws.okex.com:8443/ws/v5/private?brokerId=9999" 20 | var apikey, passphrase, secretKey string 21 | 22 | switch t { 23 | case TRADE_ACCOUNT: 24 | apikey = "x" 25 | passphrase = "x" 26 | secretKey = "x" 27 | case ISOLATE_ACCOUNT: 28 | apikey = "x" 29 | passphrase = "x" 30 | secretKey = "x" 31 | case CROSS_ACCOUNT: 32 | apikey = "x" 33 | secretKey = "x" 34 | passphrase = "x" 35 | case CROSS_ACCOUNT_B: 36 | apikey = "x" 37 | passphrase = "x" 38 | secretKey = "x" 39 | } 40 | 41 | r, err := NewWsClient(ep) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | err = r.Start() 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | var res bool 52 | //start := time.Now() 53 | res, _, err = r.Login(apikey, secretKey, passphrase) 54 | if res { 55 | //usedTime := time.Since(start) 56 | //fmt.Println("登录成功!",usedTime.String()) 57 | } else { 58 | log.Fatal("登录失败!", err) 59 | } 60 | fmt.Println(apikey, secretKey, passphrase) 61 | return r 62 | } 63 | 64 | // 账户频道 测试 65 | func TestAccout(t *testing.T) { 66 | r := prework_pri(CROSS_ACCOUNT) 67 | var res bool 68 | var err error 69 | 70 | var args []map[string]string 71 | arg := make(map[string]string) 72 | //arg["ccy"] = "BTC" 73 | args = append(args, arg) 74 | 75 | start := time.Now() 76 | res, _, err = r.PrivAccout(OP_SUBSCRIBE, args) 77 | if res { 78 | usedTime := time.Since(start) 79 | fmt.Println("订阅所有成功!", usedTime.String()) 80 | } else { 81 | fmt.Println("订阅所有成功!", err) 82 | t.Fatal("订阅所有成功!", err) 83 | } 84 | 85 | time.Sleep(100 * time.Second) 86 | start = time.Now() 87 | res, _, err = r.PrivAccout(OP_UNSUBSCRIBE, args) 88 | if res { 89 | usedTime := time.Since(start) 90 | fmt.Println("取消订阅所有成功!", usedTime.String()) 91 | } else { 92 | fmt.Println("取消订阅所有失败!", err) 93 | t.Fatal("取消订阅所有失败!", err) 94 | } 95 | 96 | } 97 | 98 | // 持仓频道 测试 99 | func TestPositon(t *testing.T) { 100 | r := prework_pri(CROSS_ACCOUNT) 101 | var err error 102 | var res bool 103 | 104 | var args []map[string]string 105 | arg := make(map[string]string) 106 | arg["instType"] = FUTURES 107 | arg["uly"] = "BTC-USD" 108 | //arg["instId"] = "BTC-USD-210319" 109 | args = append(args, arg) 110 | 111 | start := time.Now() 112 | res, _, err = r.PrivPostion(OP_SUBSCRIBE, args) 113 | if res { 114 | usedTime := time.Since(start) 115 | fmt.Println("订阅成功!", usedTime.String()) 116 | } else { 117 | fmt.Println("订阅失败!", err) 118 | t.Fatal("订阅失败!", err) 119 | //return 120 | } 121 | 122 | time.Sleep(60000 * time.Second) 123 | //等待推送 124 | 125 | start = time.Now() 126 | res, _, err = r.PrivPostion(OP_UNSUBSCRIBE, args) 127 | if res { 128 | usedTime := time.Since(start) 129 | fmt.Println("取消订阅成功!", usedTime.String()) 130 | 131 | } else { 132 | fmt.Println("取消订阅失败!", err) 133 | t.Fatal("取消订阅失败!", err) 134 | } 135 | 136 | } 137 | 138 | // 订单频道 测试 139 | func TestBookOrder(t *testing.T) { 140 | r := prework_pri(CROSS_ACCOUNT) 141 | var err error 142 | var res bool 143 | 144 | var args []map[string]string 145 | arg := make(map[string]string) 146 | arg["instId"] = "BTC-USDT" 147 | arg["instType"] = "ANY" 148 | //arg["instType"] = "SWAP" 149 | args = append(args, arg) 150 | 151 | start := time.Now() 152 | res, _, err = r.PrivBookOrder(OP_SUBSCRIBE, args) 153 | if res { 154 | usedTime := time.Since(start) 155 | fmt.Println("订阅成功!", usedTime.String()) 156 | } else { 157 | fmt.Println("订阅失败!", err) 158 | t.Fatal("订阅失败!", err) 159 | //return 160 | } 161 | 162 | time.Sleep(6000 * time.Second) 163 | //等待推送 164 | 165 | start = time.Now() 166 | res, _, err = r.PrivBookOrder(OP_UNSUBSCRIBE, args) 167 | if res { 168 | usedTime := time.Since(start) 169 | fmt.Println("取消订阅成功!", usedTime.String()) 170 | } else { 171 | fmt.Println("取消订阅失败!", err) 172 | t.Fatal("取消订阅失败!", err) 173 | } 174 | 175 | } 176 | 177 | // 策略委托订单频道 测试 178 | func TestAlgoOrder(t *testing.T) { 179 | r := prework_pri(CROSS_ACCOUNT) 180 | var err error 181 | var res bool 182 | 183 | var args []map[string]string 184 | arg := make(map[string]string) 185 | arg["instType"] = "SPOT" 186 | args = append(args, arg) 187 | 188 | start := time.Now() 189 | res, _, err = r.PrivBookAlgoOrder(OP_SUBSCRIBE, args) 190 | if res { 191 | usedTime := time.Since(start) 192 | fmt.Println("订阅成功!", usedTime.String()) 193 | } else { 194 | fmt.Println("订阅失败!", err) 195 | t.Fatal("订阅失败!", err) 196 | //return 197 | } 198 | 199 | time.Sleep(60 * time.Second) 200 | //等待推送 201 | 202 | start = time.Now() 203 | res, _, err = r.PrivBookAlgoOrder(OP_UNSUBSCRIBE, args) 204 | if res { 205 | usedTime := time.Since(start) 206 | fmt.Println("取消订阅成功!", usedTime.String()) 207 | } else { 208 | fmt.Println("取消订阅失败!", err) 209 | t.Fatal("取消订阅失败!", err) 210 | } 211 | 212 | } 213 | 214 | // 账户余额和持仓频道 测试 215 | func TestPrivBalAndPos(t *testing.T) { 216 | r := prework_pri(CROSS_ACCOUNT) 217 | var err error 218 | var res bool 219 | 220 | var args []map[string]string 221 | arg := make(map[string]string) 222 | args = append(args, arg) 223 | 224 | start := time.Now() 225 | res, _, err = r.PrivBalAndPos(OP_SUBSCRIBE, args) 226 | if res { 227 | usedTime := time.Since(start) 228 | fmt.Println("订阅成功!", usedTime.String()) 229 | } else { 230 | fmt.Println("订阅失败!", err) 231 | t.Fatal("订阅失败!", err) 232 | //return 233 | } 234 | 235 | time.Sleep(600 * time.Second) 236 | //等待推送 237 | 238 | start = time.Now() 239 | res, _, err = r.PrivBalAndPos(OP_UNSUBSCRIBE, args) 240 | if res { 241 | usedTime := time.Since(start) 242 | fmt.Println("取消订阅成功!", usedTime.String()) 243 | } else { 244 | fmt.Println("取消订阅失败!", err) 245 | t.Fatal("取消订阅失败!", err) 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /ws/ws_pub_channel.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "errors" 5 | . "v5sdk_go/ws/wImpl" 6 | ) 7 | 8 | /* 9 | 产品频道 10 | */ 11 | func (a *WsClient) PubInstruemnts(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 12 | 13 | return a.PubChannel(EVENT_BOOK_INSTRUMENTS, op, params, PERIOD_NONE, timeOut...) 14 | } 15 | 16 | func (a *WsClient) PubStatus(op string, timeOut ...int) (res bool, msg []*Msg, err error) { 17 | return a.PubChannel(EVENT_STATUS, op, nil, PERIOD_NONE, timeOut...) 18 | } 19 | 20 | /* 21 | 行情频道 22 | */ 23 | func (a *WsClient) PubTickers(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 24 | 25 | return a.PubChannel(EVENT_BOOK_TICKERS, op, params, PERIOD_NONE, timeOut...) 26 | } 27 | 28 | /* 29 | 持仓总量频道 30 | */ 31 | func (a *WsClient) PubOpenInsterest(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 32 | return a.PubChannel(EVENT_BOOK_OPEN_INTEREST, op, params, PERIOD_NONE, timeOut...) 33 | } 34 | 35 | /* 36 | K线频道 37 | */ 38 | func (a *WsClient) PubKLine(op string, period Period, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 39 | 40 | return a.PubChannel(EVENT_BOOK_KLINE, op, params, period, timeOut...) 41 | } 42 | 43 | /* 44 | 交易频道 45 | */ 46 | func (a *WsClient) PubTrade(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 47 | 48 | return a.PubChannel(EVENT_BOOK_TRADE, op, params, PERIOD_NONE, timeOut...) 49 | } 50 | 51 | /* 52 | 预估交割/行权价格频道 53 | */ 54 | func (a *WsClient) PubEstDePrice(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 55 | 56 | return a.PubChannel(EVENT_BOOK_ESTIMATE_PRICE, op, params, PERIOD_NONE, timeOut...) 57 | 58 | } 59 | 60 | /* 61 | 标记价格频道 62 | */ 63 | func (a *WsClient) PubMarkPrice(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 64 | 65 | return a.PubChannel(EVENT_BOOK_MARK_PRICE, op, params, PERIOD_NONE, timeOut...) 66 | } 67 | 68 | /* 69 | 标记价格K线频道 70 | */ 71 | func (a *WsClient) PubMarkPriceCandle(op string, pd Period, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 72 | 73 | return a.PubChannel(EVENT_BOOK_MARK_PRICE_CANDLE_CHART, op, params, pd, timeOut...) 74 | } 75 | 76 | /* 77 | 限价频道 78 | */ 79 | func (a *WsClient) PubLimitPrice(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 80 | 81 | return a.PubChannel(EVENT_BOOK_LIMIT_PRICE, op, params, PERIOD_NONE, timeOut...) 82 | } 83 | 84 | /* 85 | 深度频道 86 | */ 87 | func (a *WsClient) PubOrderBooks(op string, channel string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 88 | 89 | switch channel { 90 | // 400档快照 91 | case "books": 92 | return a.PubChannel(EVENT_BOOK_ORDER_BOOK, op, params, PERIOD_NONE, timeOut...) 93 | // 5档快照 94 | case "books5": 95 | return a.PubChannel(EVENT_BOOK_ORDER_BOOK5, op, params, PERIOD_NONE, timeOut...) 96 | // 400 tbt 97 | case "books-l2-tbt": 98 | return a.PubChannel(EVENT_BOOK_ORDER_BOOK_TBT, op, params, PERIOD_NONE, timeOut...) 99 | // 50 tbt 100 | case "books50-l2-tbt": 101 | return a.PubChannel(EVENT_BOOK_ORDER_BOOK50_TBT, op, params, PERIOD_NONE, timeOut...) 102 | 103 | default: 104 | err = errors.New("未知的channel") 105 | return 106 | } 107 | 108 | } 109 | 110 | /* 111 | 期权定价频道 112 | */ 113 | func (a *WsClient) PubOptionSummary(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 114 | 115 | return a.PubChannel(EVENT_BOOK_OPTION_SUMMARY, op, params, PERIOD_NONE, timeOut...) 116 | } 117 | 118 | /* 119 | 资金费率频道 120 | */ 121 | func (a *WsClient) PubFundRate(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 122 | 123 | return a.PubChannel(EVENT_BOOK_FUND_RATE, op, params, PERIOD_NONE, timeOut...) 124 | } 125 | 126 | /* 127 | 指数K线频道 128 | */ 129 | func (a *WsClient) PubKLineIndex(op string, pd Period, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 130 | 131 | return a.PubChannel(EVENT_BOOK_KLINE_INDEX, op, params, pd, timeOut...) 132 | } 133 | 134 | /* 135 | 指数行情频道 136 | */ 137 | func (a *WsClient) PubIndexTickers(op string, params []map[string]string, timeOut ...int) (res bool, msg []*Msg, err error) { 138 | 139 | return a.PubChannel(EVENT_BOOK_INDEX_TICKERS, op, params, PERIOD_NONE, timeOut...) 140 | } 141 | -------------------------------------------------------------------------------- /ws/ws_pub_channel_test.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "testing" 9 | "time" 10 | . "v5sdk_go/ws/wImpl" 11 | ) 12 | 13 | func prework() *WsClient { 14 | ep := "wss://ws.okex.com:8443/ws/v5/public?brokerId=9999" 15 | r, err := NewWsClient(ep) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | err = r.Start() 21 | if err != nil { 22 | log.Fatal(err, ep) 23 | } 24 | return r 25 | } 26 | 27 | // 产品频道测试 28 | func TestInstruemnts(t *testing.T) { 29 | r := prework() 30 | var err error 31 | var res bool 32 | 33 | var args []map[string]string 34 | arg := make(map[string]string) 35 | arg["instType"] = FUTURES 36 | //arg["instType"] = OPTION 37 | args = append(args, arg) 38 | 39 | start := time.Now() 40 | res, _, err = r.PubInstruemnts(OP_SUBSCRIBE, args) 41 | if res { 42 | usedTime := time.Since(start) 43 | fmt.Println("订阅成功!", usedTime.String()) 44 | } else { 45 | fmt.Println("订阅失败!", err) 46 | t.Fatal("订阅失败!", err) 47 | //return 48 | } 49 | 50 | time.Sleep(3 * time.Second) 51 | //等待推送 52 | 53 | start = time.Now() 54 | res, _, err = r.PubInstruemnts(OP_UNSUBSCRIBE, args) 55 | if res { 56 | usedTime := time.Since(start) 57 | fmt.Println("取消订阅成功!", usedTime.String()) 58 | } else { 59 | fmt.Println("取消订阅失败!", err) 60 | t.Fatal("取消订阅失败!", err) 61 | } 62 | 63 | } 64 | 65 | // status频道测试 66 | func TestStatus(t *testing.T) { 67 | r := prework() 68 | var err error 69 | var res bool 70 | 71 | start := time.Now() 72 | res, _, err = r.PubStatus(OP_SUBSCRIBE) 73 | if res { 74 | usedTime := time.Since(start) 75 | fmt.Println("订阅成功!", usedTime.String()) 76 | } else { 77 | fmt.Println("订阅失败!", err) 78 | t.Fatal("订阅失败!", err) 79 | //return 80 | } 81 | 82 | time.Sleep(10000 * time.Second) 83 | //等待推送 84 | 85 | start = time.Now() 86 | res, _, err = r.PubStatus(OP_UNSUBSCRIBE) 87 | if res { 88 | usedTime := time.Since(start) 89 | fmt.Println("取消订阅成功!", usedTime.String()) 90 | } else { 91 | fmt.Println("取消订阅失败!", err) 92 | t.Fatal("取消订阅失败!", err) 93 | } 94 | 95 | } 96 | 97 | // 行情频道测试 98 | func TestTickers(t *testing.T) { 99 | r := prework() 100 | var err error 101 | var res bool 102 | 103 | var args []map[string]string 104 | arg := make(map[string]string) 105 | arg["instId"] = "BTC-USDT" 106 | 107 | args = append(args, arg) 108 | 109 | start := time.Now() 110 | res, _, err = r.PubTickers(OP_SUBSCRIBE, args) 111 | if res { 112 | usedTime := time.Since(start) 113 | fmt.Println("订阅成功!", usedTime.String()) 114 | } else { 115 | fmt.Println("订阅失败!", err) 116 | t.Fatal("订阅失败!", err) 117 | //return 118 | } 119 | 120 | time.Sleep(600 * time.Second) 121 | //等待推送 122 | 123 | start = time.Now() 124 | res, _, err = r.PubTickers(OP_UNSUBSCRIBE, args) 125 | if res { 126 | usedTime := time.Since(start) 127 | fmt.Println("取消订阅成功!", usedTime.String()) 128 | } else { 129 | fmt.Println("取消订阅失败!", err) 130 | t.Fatal("取消订阅失败!", err) 131 | } 132 | 133 | } 134 | 135 | // 持仓总量频道 测试 136 | func TestOpenInsterest(t *testing.T) { 137 | r := prework() 138 | var err error 139 | var res bool 140 | 141 | var args []map[string]string 142 | arg := make(map[string]string) 143 | arg["instId"] = "LTC-USD-SWAP" 144 | 145 | args = append(args, arg) 146 | 147 | start := time.Now() 148 | res, _, err = r.PubOpenInsterest(OP_SUBSCRIBE, args) 149 | if res { 150 | usedTime := time.Since(start) 151 | fmt.Println("订阅成功!", usedTime.String()) 152 | } else { 153 | fmt.Println("订阅失败!", err) 154 | t.Fatal("订阅失败!", err) 155 | //return 156 | } 157 | 158 | time.Sleep(60 * time.Second) 159 | //等待推送 160 | 161 | start = time.Now() 162 | res, _, err = r.PubOpenInsterest(OP_UNSUBSCRIBE, args) 163 | if res { 164 | usedTime := time.Since(start) 165 | fmt.Println("取消订阅成功!", usedTime.String()) 166 | } else { 167 | fmt.Println("取消订阅失败!", err) 168 | t.Fatal("取消订阅失败!", err) 169 | } 170 | 171 | } 172 | 173 | // K线频道测试 174 | func TestKLine(t *testing.T) { 175 | r := prework() 176 | var err error 177 | var res bool 178 | 179 | var args []map[string]string 180 | arg := make(map[string]string) 181 | arg["instId"] = "BTC-USDT" 182 | args = append(args, arg) 183 | 184 | // 1分钟K 185 | period := PERIOD_1MIN 186 | 187 | start := time.Now() 188 | res, _, err = r.PubKLine(OP_SUBSCRIBE, period, args) 189 | if res { 190 | usedTime := time.Since(start) 191 | fmt.Println("订阅成功!", usedTime.String()) 192 | } else { 193 | fmt.Println("订阅失败!", err) 194 | t.Fatal("订阅失败!", err) 195 | } 196 | 197 | time.Sleep(60 * time.Second) 198 | //等待推送 199 | 200 | start = time.Now() 201 | res, _, err = r.PubKLine(OP_UNSUBSCRIBE, period, args) 202 | if res { 203 | usedTime := time.Since(start) 204 | fmt.Println("取消订阅成功!", usedTime.String()) 205 | } else { 206 | fmt.Println("取消订阅失败!", err) 207 | t.Fatal("取消订阅失败!", err) 208 | } 209 | 210 | } 211 | 212 | // 交易频道测试 213 | func TestTrade(t *testing.T) { 214 | r := prework() 215 | var err error 216 | var res bool 217 | 218 | var args []map[string]string 219 | arg := make(map[string]string) 220 | arg["instId"] = "BTC-USDT" 221 | args = append(args, arg) 222 | 223 | start := time.Now() 224 | res, _, err = r.PubTrade(OP_SUBSCRIBE, args) 225 | if res { 226 | usedTime := time.Since(start) 227 | fmt.Println("订阅成功!", usedTime.String()) 228 | } else { 229 | fmt.Println("订阅失败!", err) 230 | t.Fatal("订阅失败!", err) 231 | //return 232 | } 233 | 234 | time.Sleep(60 * time.Second) 235 | //等待推送 236 | 237 | start = time.Now() 238 | res, _, err = r.PubTrade(OP_UNSUBSCRIBE, args) 239 | if res { 240 | usedTime := time.Since(start) 241 | fmt.Println("取消订阅成功!", usedTime.String()) 242 | } else { 243 | fmt.Println("取消订阅失败!", err) 244 | t.Fatal("取消订阅失败!", err) 245 | } 246 | 247 | } 248 | 249 | // 预估交割/行权价格频道 测试 250 | func TestEstDePrice(t *testing.T) { 251 | r := prework() 252 | var err error 253 | var res bool 254 | 255 | var args []map[string]string 256 | arg := make(map[string]string) 257 | arg["instType"] = FUTURES 258 | arg["uly"] = "BTC-USD" 259 | args = append(args, arg) 260 | 261 | start := time.Now() 262 | res, _, err = r.PubEstDePrice(OP_SUBSCRIBE, args) 263 | if res { 264 | usedTime := time.Since(start) 265 | fmt.Println("订阅成功!", usedTime.String()) 266 | } else { 267 | fmt.Println("订阅失败!", err) 268 | t.Fatal("订阅失败!", err) 269 | //return 270 | } 271 | 272 | time.Sleep(60 * time.Second) 273 | //等待推送 274 | 275 | start = time.Now() 276 | res, _, err = r.PubEstDePrice(OP_UNSUBSCRIBE, args) 277 | if res { 278 | usedTime := time.Since(start) 279 | fmt.Println("取消订阅成功!", usedTime.String()) 280 | } else { 281 | fmt.Println("取消订阅失败!", err) 282 | t.Fatal("取消订阅失败!", err) 283 | } 284 | 285 | } 286 | 287 | // 标记价格频道 测试 288 | func TestMarkPrice(t *testing.T) { 289 | r := prework() 290 | var err error 291 | var res bool 292 | 293 | var args []map[string]string 294 | arg := make(map[string]string) 295 | arg["instId"] = "BTC-USDT" 296 | 297 | args = append(args, arg) 298 | 299 | start := time.Now() 300 | res, _, err = r.PubMarkPrice(OP_SUBSCRIBE, args) 301 | if res { 302 | usedTime := time.Since(start) 303 | fmt.Println("订阅成功!", usedTime.String()) 304 | } else { 305 | fmt.Println("订阅失败!", err) 306 | t.Fatal("订阅失败!", err) 307 | //return 308 | } 309 | 310 | time.Sleep(60 * time.Second) 311 | //等待推送 312 | 313 | start = time.Now() 314 | res, _, err = r.PubMarkPrice(OP_UNSUBSCRIBE, args) 315 | if res { 316 | usedTime := time.Since(start) 317 | fmt.Println("取消订阅成功!", usedTime.String()) 318 | } else { 319 | fmt.Println("取消订阅失败!", err) 320 | t.Fatal("取消订阅失败!", err) 321 | } 322 | 323 | } 324 | 325 | // 标记价格K线频道 测试s 326 | func TestMarkPriceCandle(t *testing.T) { 327 | r := prework() 328 | var err error 329 | var res bool 330 | 331 | var args []map[string]string 332 | arg := make(map[string]string) 333 | arg["instId"] = "BTC-USDT" 334 | args = append(args, arg) 335 | 336 | period := PERIOD_1MIN 337 | 338 | start := time.Now() 339 | res, _, err = r.PubMarkPriceCandle(OP_SUBSCRIBE, period, args) 340 | if res { 341 | usedTime := time.Since(start) 342 | fmt.Println("订阅成功!", usedTime.String()) 343 | } else { 344 | fmt.Println("订阅失败!", err) 345 | t.Fatal("订阅失败!", err) 346 | //return 347 | } 348 | 349 | time.Sleep(60 * time.Second) 350 | //等待推送 351 | 352 | start = time.Now() 353 | res, _, err = r.PubMarkPriceCandle(OP_UNSUBSCRIBE, period, args) 354 | if res { 355 | usedTime := time.Since(start) 356 | fmt.Println("取消订阅成功!", usedTime.String()) 357 | } else { 358 | fmt.Println("取消订阅失败!", err) 359 | t.Fatal("取消订阅失败!", err) 360 | } 361 | 362 | } 363 | 364 | // 限价频道 测试 365 | func TestLimitPrice(t *testing.T) { 366 | r := prework() 367 | var err error 368 | var res bool 369 | 370 | var args []map[string]string 371 | arg := make(map[string]string) 372 | arg["instId"] = "BTC-USDT" 373 | args = append(args, arg) 374 | 375 | start := time.Now() 376 | res, _, err = r.PubLimitPrice(OP_SUBSCRIBE, args) 377 | if res { 378 | usedTime := time.Since(start) 379 | fmt.Println("订阅成功!", usedTime.String()) 380 | } else { 381 | fmt.Println("订阅失败!", err) 382 | t.Fatal("订阅失败!", err) 383 | //return 384 | } 385 | 386 | time.Sleep(60 * time.Second) 387 | //等待推送 388 | 389 | start = time.Now() 390 | res, _, err = r.PubLimitPrice(OP_UNSUBSCRIBE, args) 391 | if res { 392 | usedTime := time.Since(start) 393 | fmt.Println("取消订阅成功!", usedTime.String()) 394 | } else { 395 | fmt.Println("取消订阅失败!", err) 396 | t.Fatal("取消订阅失败!", err) 397 | } 398 | 399 | } 400 | 401 | // 深度频道 测试 402 | func TestOrderBooks(t *testing.T) { 403 | r := prework() 404 | var err error 405 | var res bool 406 | 407 | /* 408 | 设置关闭深度数据管理 409 | */ 410 | // err = r.EnableAutoDepthMgr(false) 411 | // if err != nil { 412 | // fmt.Println("关闭自动校验失败!") 413 | // } 414 | 415 | end := make(chan struct{}) 416 | 417 | r.AddDepthHook(func(ts time.Time, data DepthData) error { 418 | // 对于深度类型数据处理的用户可以自定义 419 | 420 | // 检测深度数据是否正常 421 | key, _ := json.Marshal(data.Arg) 422 | fmt.Println("个数:", len(data.Data[0].Asks)) 423 | checksum := data.Data[0].Checksum 424 | fmt.Println("[自定义方法] ", string(key), ", checksum = ", checksum) 425 | 426 | for _, ask := range data.Data[0].Asks { 427 | 428 | arr := strings.Split(ask[0], ".") 429 | //fmt.Println(arr) 430 | if len(arr) > 1 && len(arr[1]) > 2 { 431 | fmt.Println("ask数据异常,", checksum, "ask:", ask) 432 | t.Fatal() 433 | end <- struct{}{} 434 | return nil 435 | } else { 436 | fmt.Println("bid数据正常,", checksum, "ask:", ask) 437 | } 438 | 439 | } 440 | 441 | for _, bid := range data.Data[0].Bids { 442 | 443 | arr := strings.Split(bid[0], ".") 444 | //fmt.Println(arr) 445 | if len(arr) > 1 && len(arr[1]) > 2 { 446 | fmt.Println("bid数据异常,", checksum, "bid:", bid) 447 | t.Fatal() 448 | end <- struct{}{} 449 | return nil 450 | } else { 451 | fmt.Println("ask数据正常,", checksum, "bid:", bid) 452 | } 453 | 454 | } 455 | 456 | // // 查看当前合并后的全量深度数据 457 | // snapshot, err := r.GetSnapshotByChannel(data) 458 | // if err != nil { 459 | // t.Fatal("深度数据不存在!") 460 | // } 461 | // // 展示ask/bid 前5档数据 462 | // fmt.Println(" Ask 5 档数据 >> ") 463 | // for _, v := range snapshot.Asks[:5] { 464 | // fmt.Println(" price:", v[0], " amount:", v[1]) 465 | // } 466 | // fmt.Println(" Bid 5 档数据 >> ") 467 | // for _, v := range snapshot.Bids[:5] { 468 | // fmt.Println(" price:", v[0], " amount:", v[1]) 469 | // } 470 | return nil 471 | }) 472 | 473 | // 可选类型:books books5 books-l2-tbt 474 | channel := "books50-l2-tbt" 475 | 476 | instIds := []string{"BTC-USDT"} 477 | for _, instId := range instIds { 478 | var args []map[string]string 479 | arg := make(map[string]string) 480 | arg["instId"] = instId 481 | args = append(args, arg) 482 | 483 | start := time.Now() 484 | res, _, err = r.PubOrderBooks(OP_SUBSCRIBE, channel, args) 485 | if res { 486 | usedTime := time.Since(start) 487 | fmt.Println("订阅成功!", usedTime.String()) 488 | } else { 489 | fmt.Println("订阅失败!", err) 490 | t.Fatal("订阅失败!", err) 491 | } 492 | } 493 | 494 | select { 495 | case <-end: 496 | 497 | } 498 | //等待推送 499 | for _, instId := range instIds { 500 | var args []map[string]string 501 | arg := make(map[string]string) 502 | arg["instId"] = instId 503 | args = append(args, arg) 504 | 505 | start := time.Now() 506 | res, _, err = r.PubOrderBooks(OP_UNSUBSCRIBE, channel, args) 507 | if res { 508 | usedTime := time.Since(start) 509 | fmt.Println("取消订阅成功!", usedTime.String()) 510 | } else { 511 | fmt.Println("取消订阅失败!", err) 512 | t.Fatal("取消订阅失败!", err) 513 | } 514 | } 515 | 516 | } 517 | 518 | // 期权定价频道 测试 519 | func TestOptionSummary(t *testing.T) { 520 | r := prework() 521 | var err error 522 | var res bool 523 | 524 | var args []map[string]string 525 | arg := make(map[string]string) 526 | arg["uly"] = "BTC-USD" 527 | args = append(args, arg) 528 | 529 | start := time.Now() 530 | res, _, err = r.PubOptionSummary(OP_SUBSCRIBE, args) 531 | if res { 532 | usedTime := time.Since(start) 533 | fmt.Println("订阅成功!", usedTime.String()) 534 | } else { 535 | fmt.Println("订阅失败!", err) 536 | t.Fatal("订阅失败!", err) 537 | //return 538 | } 539 | 540 | time.Sleep(60 * time.Second) 541 | //等待推送 542 | 543 | start = time.Now() 544 | res, _, err = r.PubOptionSummary(OP_UNSUBSCRIBE, args) 545 | if res { 546 | usedTime := time.Since(start) 547 | fmt.Println("取消订阅成功!", usedTime.String()) 548 | } else { 549 | fmt.Println("取消订阅失败!", err) 550 | t.Fatal("取消订阅失败!", err) 551 | } 552 | 553 | } 554 | 555 | // 资金费率 测试 556 | func TestFundRate(t *testing.T) { 557 | r := prework() 558 | var err error 559 | var res bool 560 | 561 | var args []map[string]string 562 | arg := make(map[string]string) 563 | arg["instId"] = "BTC-USD-SWAP" 564 | args = append(args, arg) 565 | 566 | start := time.Now() 567 | res, _, err = r.PubFundRate(OP_SUBSCRIBE, args) 568 | if res { 569 | usedTime := time.Since(start) 570 | fmt.Println("订阅成功!", usedTime.String()) 571 | } else { 572 | fmt.Println("订阅失败!", err) 573 | t.Fatal("订阅失败!", err) 574 | //return 575 | } 576 | 577 | time.Sleep(600 * time.Second) 578 | //等待推送 579 | 580 | start = time.Now() 581 | res, _, err = r.PubFundRate(OP_UNSUBSCRIBE, args) 582 | if res { 583 | usedTime := time.Since(start) 584 | fmt.Println("取消订阅成功!", usedTime.String()) 585 | } else { 586 | fmt.Println("取消订阅失败!", err) 587 | t.Fatal("取消订阅失败!", err) 588 | } 589 | 590 | } 591 | 592 | // 指数K线频道 测试 593 | func TestKLineIndex(t *testing.T) { 594 | r := prework() 595 | var err error 596 | var res bool 597 | 598 | var args []map[string]string 599 | arg := make(map[string]string) 600 | 601 | arg["instId"] = "BTC-USDT" 602 | args = append(args, arg) 603 | period := PERIOD_1MIN 604 | 605 | start := time.Now() 606 | res, _, err = r.PubKLineIndex(OP_SUBSCRIBE, period, args) 607 | if res { 608 | usedTime := time.Since(start) 609 | fmt.Println("订阅成功!", usedTime.String()) 610 | } else { 611 | fmt.Println("订阅失败!", err) 612 | t.Fatal("订阅失败!", err) 613 | //return 614 | } 615 | 616 | time.Sleep(60 * time.Second) 617 | //等待推送 618 | 619 | start = time.Now() 620 | res, _, err = r.PubKLineIndex(OP_UNSUBSCRIBE, period, args) 621 | if res { 622 | usedTime := time.Since(start) 623 | fmt.Println("取消订阅成功!", usedTime.String()) 624 | } else { 625 | fmt.Println("取消订阅失败!", err) 626 | t.Fatal("取消订阅失败!", err) 627 | } 628 | 629 | } 630 | 631 | // 指数行情频道 测试 632 | func TestIndexMarket(t *testing.T) { 633 | r := prework() 634 | var err error 635 | var res bool 636 | 637 | var args []map[string]string 638 | arg := make(map[string]string) 639 | arg["instId"] = "BTC-USDT" 640 | args = append(args, arg) 641 | 642 | start := time.Now() 643 | res, _, err = r.PubIndexTickers(OP_SUBSCRIBE, args) 644 | if err != nil { 645 | fmt.Println("订阅失败!", err) 646 | } 647 | usedTime := time.Since(start) 648 | if res { 649 | fmt.Println("订阅成功!", usedTime.String()) 650 | } else { 651 | fmt.Println("订阅失败!", usedTime.String()) 652 | //return 653 | } 654 | 655 | time.Sleep(600 * time.Second) 656 | //等待推送 657 | 658 | start = time.Now() 659 | res, _, err = r.PubIndexTickers(OP_UNSUBSCRIBE, args) 660 | if res { 661 | usedTime := time.Since(start) 662 | fmt.Println("取消订阅成功!", usedTime.String()) 663 | } else { 664 | fmt.Println("取消订阅失败!", err) 665 | t.Fatal("取消订阅失败!", err) 666 | } 667 | 668 | } 669 | -------------------------------------------------------------------------------- /ws/ws_test.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | "time" 8 | . "v5sdk_go/ws/wImpl" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func init() { 14 | log.SetFlags(log.LstdFlags | log.Llongfile) 15 | 16 | } 17 | 18 | func TestPing(t *testing.T) { 19 | r := prework_pri(CROSS_ACCOUNT) 20 | 21 | res, _, _ := r.Ping() 22 | assert.True(t, res, true) 23 | } 24 | 25 | func TestWsClient_SubscribeAndUnSubscribe(t *testing.T) { 26 | r := prework() 27 | var err error 28 | var res bool 29 | 30 | param := map[string]string{} 31 | param["channel"] = "opt-summary" 32 | param["uly"] = "BTC-USD" 33 | 34 | start := time.Now() 35 | res, _, err = r.Subscribe(param) 36 | if res { 37 | usedTime := time.Since(start) 38 | fmt.Println("订阅成功!", usedTime.String()) 39 | } else { 40 | fmt.Println("订阅失败!", err) 41 | t.Fatal("订阅失败!", err) 42 | //return 43 | } 44 | 45 | time.Sleep(60 * time.Second) 46 | //等待推送 47 | 48 | start = time.Now() 49 | res, _, err = r.UnSubscribe(param) 50 | if res { 51 | usedTime := time.Since(start) 52 | fmt.Println("取消订阅成功!", usedTime.String()) 53 | } else { 54 | fmt.Println("取消订阅失败!", err) 55 | t.Fatal("取消订阅失败!", err) 56 | } 57 | 58 | } 59 | 60 | func TestWsClient_SubscribeAndUnSubscribe_priv(t *testing.T) { 61 | r := prework_pri(ISOLATE_ACCOUNT) 62 | var err error 63 | var res bool 64 | 65 | var params []map[string]string 66 | params = append(params, map[string]string{"channel": "orders", "instType": SPOT, "instId": "BTC-USDT"}) 67 | //一个失败的订阅用例 68 | params = append(params, map[string]string{"channel": "positions", "instType": "any"}) 69 | 70 | for _, v := range params { 71 | start := time.Now() 72 | var data *ProcessDetail 73 | res, data, err = r.Subscribe(v) 74 | if res { 75 | usedTime := time.Since(start) 76 | fmt.Println("订阅成功!", usedTime.String()) 77 | PrintDetail(data) 78 | } else { 79 | fmt.Println("订阅失败!", err) 80 | //return 81 | } 82 | time.Sleep(60 * time.Second) 83 | //等待推送 84 | 85 | start = time.Now() 86 | res, _, err = r.UnSubscribe(v) 87 | if res { 88 | usedTime := time.Since(start) 89 | fmt.Println("取消订阅成功!", usedTime.String()) 90 | } else { 91 | fmt.Println("取消订阅失败!", err) 92 | } 93 | 94 | } 95 | 96 | } 97 | 98 | func TestWsClient_Jrpc(t *testing.T) { 99 | //r := prework_pri(ISOLATE_ACCOUNT) 100 | r := prework_pri(CROSS_ACCOUNT) 101 | var res bool 102 | var err error 103 | var data *ProcessDetail 104 | 105 | start := time.Now() 106 | var args []map[string]interface{} 107 | 108 | param := map[string]interface{}{} 109 | param["instId"] = "BTC-USDT" 110 | param["clOrdId"] = "SIM0dcopy16069997808063455" 111 | param["tdMode"] = "cross" 112 | param["side"] = "sell" 113 | param["ordType"] = "limit" 114 | param["px"] = "19333.3" 115 | param["sz"] = "0.18605445" 116 | 117 | param1 := map[string]interface{}{} 118 | param1["instId"] = "BTC-USDT" 119 | param1["clOrdId"] = "SIM0dcopy16069997808063456" 120 | param1["tdMode"] = "cross" 121 | param1["side"] = "sell" 122 | param1["ordType"] = "limit" 123 | param1["px"] = "19334.2" 124 | param1["sz"] = "0.03508913" 125 | 126 | param2 := map[string]interface{}{} 127 | param2["instId"] = "BTC-USDT" 128 | param2["clOrdId"] = "SIM0dcopy16069997808063457" 129 | param2["tdMode"] = "cross" 130 | param2["side"] = "sell" 131 | param2["ordType"] = "limit" 132 | param2["px"] = "19334.8" 133 | param2["sz"] = "0.03658186" 134 | 135 | param3 := map[string]interface{}{} 136 | param3["instId"] = "BTC-USDT" 137 | param3["clOrdId"] = "SIM0dcopy16069997808063458" 138 | param3["tdMode"] = "cross" 139 | param3["side"] = "sell" 140 | param3["ordType"] = "limit" 141 | param3["px"] = "19334.9" 142 | param3["sz"] = "0.5" 143 | 144 | param4 := map[string]interface{}{} 145 | param4["instId"] = "BTC-USDT" 146 | param4["clOrdId"] = "SIM0dcopy16069997808063459" 147 | param4["tdMode"] = "cross" 148 | param4["side"] = "sell" 149 | param4["ordType"] = "limit" 150 | param4["px"] = "19335.2" 151 | param4["sz"] = "0.3" 152 | 153 | param5 := map[string]interface{}{} 154 | param5["instId"] = "BTC-USDT" 155 | param5["clOrdId"] = "SIM0dcopy16069997808063460" 156 | param5["tdMode"] = "cross" 157 | param5["side"] = "sell" 158 | param5["ordType"] = "limit" 159 | param5["px"] = "19335.9" 160 | param5["sz"] = "0.051" 161 | 162 | param6 := map[string]interface{}{} 163 | param6["instId"] = "BTC-USDT" 164 | param6["clOrdId"] = "SIM0dcopy16069997808063461" 165 | param6["tdMode"] = "cross" 166 | param6["side"] = "sell" 167 | param6["ordType"] = "limit" 168 | param6["px"] = "19336.4" 169 | param6["sz"] = "1" 170 | 171 | param7 := map[string]interface{}{} 172 | param7["instId"] = "BTC-USDT" 173 | param7["clOrdId"] = "SIM0dcopy16069997808063462" 174 | param7["tdMode"] = "cross" 175 | param7["side"] = "sell" 176 | param7["ordType"] = "limit" 177 | param7["px"] = "19336.8" 178 | param7["sz"] = "0.475" 179 | 180 | param8 := map[string]interface{}{} 181 | param8["instId"] = "BTC-USDT" 182 | param8["clOrdId"] = "SIM0dcopy16069997808063463" 183 | param8["tdMode"] = "cross" 184 | param8["side"] = "sell" 185 | param8["ordType"] = "limit" 186 | param8["px"] = "19337.3" 187 | param8["sz"] = "0.21299357" 188 | 189 | param9 := map[string]interface{}{} 190 | param9["instId"] = "BTC-USDT" 191 | param9["clOrdId"] = "SIM0dcopy16069997808063464" 192 | param9["tdMode"] = "cross" 193 | param9["side"] = "sell" 194 | param9["ordType"] = "limit" 195 | param9["px"] = "19337.5" 196 | param9["sz"] = "0.5" 197 | 198 | args = append(args, param) 199 | args = append(args, param1) 200 | args = append(args, param2) 201 | args = append(args, param3) 202 | args = append(args, param4) 203 | args = append(args, param5) 204 | args = append(args, param6) 205 | args = append(args, param7) 206 | args = append(args, param8) 207 | args = append(args, param9) 208 | 209 | res, data, err = r.Jrpc("okexv5wsapi001", "order", args) 210 | if res { 211 | usedTime := time.Since(start) 212 | fmt.Println("下单成功!", usedTime.String()) 213 | PrintDetail(data) 214 | } else { 215 | usedTime := time.Since(start) 216 | fmt.Println("下单失败!", usedTime.String(), err) 217 | } 218 | } 219 | 220 | /* 221 | 测试 添加全局消息回调函数 222 | */ 223 | func TestAddMessageHook(t *testing.T) { 224 | 225 | r := prework_pri(CROSS_ACCOUNT) 226 | 227 | r.AddMessageHook(func(msg *Msg) error { 228 | // 添加你的方法 229 | fmt.Println("这是自定义MessageHook") 230 | fmt.Println("当前数据是", msg) 231 | return nil 232 | }) 233 | 234 | select {} 235 | } 236 | 237 | /* 238 | 普通推送数据回调函数 239 | */ 240 | func TestAddBookedDataHook(t *testing.T) { 241 | var r *WsClient 242 | 243 | /*订阅私有频道*/ 244 | { 245 | r = prework_pri(CROSS_ACCOUNT) 246 | var res bool 247 | var err error 248 | 249 | r.AddBookMsgHook(func(ts time.Time, data MsgData) error { 250 | // 添加你的方法 251 | fmt.Println("这是自定义AddBookMsgHook") 252 | fmt.Println("当前数据是", data) 253 | return nil 254 | }) 255 | 256 | param := map[string]string{} 257 | param["channel"] = "account" 258 | param["ccy"] = "BTC" 259 | 260 | res, _, err = r.Subscribe(param) 261 | if res { 262 | fmt.Println("订阅成功!") 263 | } else { 264 | fmt.Println("订阅失败!", err) 265 | t.Fatal("订阅失败!", err) 266 | //return 267 | } 268 | 269 | time.Sleep(100 * time.Second) 270 | } 271 | 272 | //订阅公共频道 273 | { 274 | r = prework() 275 | var res bool 276 | var err error 277 | 278 | r.AddBookMsgHook(func(ts time.Time, data MsgData) error { 279 | // 添加你的方法 280 | fmt.Println("这是自定义AddBookMsgHook") 281 | fmt.Println("当前数据是", data) 282 | return nil 283 | }) 284 | 285 | param := map[string]string{} 286 | param["channel"] = "instruments" 287 | param["instType"] = "FUTURES" 288 | 289 | res, _, err = r.Subscribe(param) 290 | if res { 291 | fmt.Println("订阅成功!") 292 | } else { 293 | fmt.Println("订阅失败!", err) 294 | t.Fatal("订阅失败!", err) 295 | //return 296 | } 297 | 298 | select {} 299 | } 300 | 301 | } 302 | 303 | func TestGetInfoFromErrMsg(t *testing.T) { 304 | a := assert.New(t) 305 | buf := ` 306 | "channel:index-tickers,instId:BTC-USDT1 doesn't exist" 307 | ` 308 | ch := GetInfoFromErrMsg(buf) 309 | //t.Log(ch) 310 | a.Equal("index-tickers", ch) 311 | 312 | //assert.True(t,ch == "index-tickers") 313 | } 314 | 315 | /* 316 | 317 | */ 318 | func TestParseMessage(t *testing.T) { 319 | r := prework() 320 | var evt Event 321 | msg := `{"event":"error","msg":"Contract does not exist.","code":"51001"}` 322 | 323 | evt, _, _ = r.parseMessage([]byte(msg)) 324 | assert.True(t, EVENT_ERROR == evt) 325 | 326 | msg = `{"event":"error","msg":"channel:positions,ccy:BTC doesn't exist","code":"60018"}` 327 | evt, _, _ = r.parseMessage([]byte(msg)) 328 | assert.True(t, EVENT_BOOK_POSTION == evt) 329 | } 330 | 331 | /* 332 | 原始方式 深度订阅 测试 333 | */ 334 | func TestSubscribeTBT(t *testing.T) { 335 | r := prework() 336 | var res bool 337 | var err error 338 | 339 | // 添加你的方法 340 | r.AddDepthHook(func(ts time.Time, data DepthData) error { 341 | //fmt.Println("这是自定义AddBookMsgHook") 342 | fmt.Println("当前数据是:", data) 343 | return nil 344 | }) 345 | 346 | param := map[string]string{} 347 | param["channel"] = "books-l2-tbt" 348 | //param["channel"] = "books" 349 | param["instId"] = "BTC-USD-SWAP" 350 | 351 | res, _, err = r.Subscribe(param) 352 | if res { 353 | fmt.Println("订阅成功!") 354 | } else { 355 | fmt.Println("订阅失败!", err) 356 | t.Fatal("订阅失败!", err) 357 | //return 358 | } 359 | 360 | time.Sleep(60 * time.Second) 361 | } 362 | 363 | /* 364 | 365 | */ 366 | func TestSubscribeBalAndPos(t *testing.T) { 367 | r := prework_pri(CROSS_ACCOUNT) 368 | var res bool 369 | var err error 370 | 371 | param := map[string]string{} 372 | 373 | // 产品信息 374 | param["channel"] = "balance_and_position" 375 | 376 | res, _, err = r.Subscribe(param) 377 | if res { 378 | fmt.Println("订阅成功!") 379 | } else { 380 | fmt.Println("订阅失败!", err) 381 | t.Fatal("订阅失败!", err) 382 | //return 383 | } 384 | 385 | time.Sleep(60 * time.Second) 386 | } 387 | --------------------------------------------------------------------------------