├── .gitignore ├── LICENSE ├── README.md ├── examples ├── hbdm-demo │ └── main.go ├── hbdm-ws-demo │ └── main.go ├── hbdmswap-demo │ └── main.go └── hbdmswap-ws-demo │ └── main.go ├── go.mod ├── go.sum ├── hbdm ├── base.go ├── hbdm.go ├── hbdm_account.go ├── hbdm_account_test.go ├── hbdm_market.go ├── hbdm_market_test.go ├── hbdm_test.go ├── hbdm_trade.go ├── hbdm_trade_test.go ├── models.go ├── nws.go ├── nws_test.go ├── ws.go └── ws_test.go ├── hbdmswap ├── base.go ├── hbdmswap.go ├── hbdmswap_account.go ├── hbdmswap_account_test.go ├── hbdmswap_market.go ├── hbdmswap_market_test.go ├── hbdmswap_test.go ├── hbdmswap_trade.go ├── hbdmswap_trade_test.go ├── models.go ├── nws.go ├── nws_test.go ├── ws.go └── ws_test.go ├── spot ├── base.go ├── models.go ├── spot.go ├── spot_market.go └── spot_market_test.go ├── test_config.example.yaml └── utils ├── gzip.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .vscode/ 18 | .idea/ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Frank Zhou 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # huobi-api 2 | Huobi api for Golang. 火币交割合约和永续合约接口. 3 | 4 | [![](https://img.shields.io/badge/api-huobi-blue.svg)](https://huobiapi.github.io/docs/dm/v1/cn/) 5 | 6 | An implementation of [Huobi-DM API](https://huobiapi.github.io/docs/dm/v1/cn/) and [Huobi-DM-Swap API](https://docs.huobigroup.com/docs/coin_margined_swap/v1/cn/). 7 | 8 | ## Installation 9 | ``` 10 | go get github.com/frankrap/huobi-api 11 | ``` 12 | 13 | ## Usage 14 | ```go 15 | package main 16 | 17 | import ( 18 | "github.com/frankrap/huobi-api/hbdm" 19 | "log" 20 | ) 21 | 22 | func main() { 23 | accessKey := "[Access Key]" 24 | secretKey := "[Secret Key]" 25 | 26 | baseURL := "https://api.hbdm.com" 27 | //baseURL := "https://api.btcgateway.pro" 28 | apiParams := &hbdm.ApiParameter{ 29 | Debug: true, 30 | AccessKey: accessKey, 31 | SecretKey: secretKey, 32 | EnablePrivateSign: false, 33 | Url: baseURL, 34 | PrivateKeyPrime256: "", 35 | } 36 | client := hbdm.NewClient(apiParams) 37 | 38 | client.GetAccountInfo("BTC") 39 | orderResult, err := client.Order("BTC", 40 | "this_week", 41 | "", 42 | 0, 43 | 3000.0, 44 | 1, 45 | "buy", 46 | "open", 47 | 10, 48 | "limit") 49 | if err != nil { 50 | log.Fatal(err) 51 | return 52 | } 53 | log.Printf("%#v", orderResult) 54 | 55 | orders, err := client.GetOpenOrders( 56 | "BTC", 57 | 0, 58 | 0, 59 | ) 60 | if err != nil { 61 | log.Fatal(err) 62 | return 63 | } 64 | log.Printf("%#v", orders) 65 | } 66 | ``` 67 | 68 | ## Usage WebSocket 69 | ```go 70 | package main 71 | 72 | import ( 73 | "github.com/frankrap/huobi-api/hbdm" 74 | "log" 75 | ) 76 | 77 | func main() { 78 | wsURL := "wss://api.hbdm.com/ws" 79 | //wsURL := "wss://api.btcgateway.pro/ws" 80 | ws := hbdm.NewWS(wsURL, "", "") 81 | 82 | // 设置Ticker回调 83 | ws.SetTickerCallback(func(ticker *hbdm.WSTicker) { 84 | log.Printf("ticker: %#v", ticker) 85 | }) 86 | // 设置Depth回调 87 | ws.SetDepthCallback(func(depth *hbdm.WSDepth) { 88 | log.Printf("depth: %#v", depth) 89 | }) 90 | // 设置Trade回调 91 | ws.SetTradeCallback(func(trade *hbdm.WSTrade) { 92 | log.Printf("trade: %#v", trade) 93 | }) 94 | 95 | // 订阅Ticker 96 | ws.SubscribeTicker("ticker_1", "BTC_CQ") 97 | // 订阅Depth 98 | ws.SubscribeDepth("depth_1", "BTC_CQ") 99 | // 订阅Trade 100 | ws.SubscribeTrade("trade_1", "BTC_CQ") 101 | // 启动WS 102 | ws.Start() 103 | 104 | select {} 105 | } 106 | ``` -------------------------------------------------------------------------------- /examples/hbdm-demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/frankrap/huobi-api/hbdm" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | accessKey := "[Access Key]" 10 | secretKey := "[Secret Key]" 11 | 12 | baseURL := "https://api.hbdm.com" 13 | //baseURL := "https://api.btcgateway.pro" 14 | apiParams := &hbdm.ApiParameter{ 15 | Debug: true, 16 | AccessKey: accessKey, 17 | SecretKey: secretKey, 18 | EnablePrivateSign: false, 19 | BaseURL: baseURL, 20 | PrivateKeyPrime256: "", 21 | } 22 | client := hbdm.NewClient(apiParams) 23 | 24 | client.GetAccountInfo("BTC") 25 | orderResult, err := client.Order("BTC", 26 | "this_week", 27 | "", 28 | 0, 29 | 3000.0, 30 | 1, 31 | "buy", 32 | "open", 33 | 10, 34 | "limit") 35 | if err != nil { 36 | log.Fatal(err) 37 | return 38 | } 39 | log.Printf("%#v", orderResult) 40 | 41 | orders, err := client.GetOpenOrders( 42 | "BTC", 43 | 0, 44 | 0, 45 | ) 46 | if err != nil { 47 | log.Fatal(err) 48 | return 49 | } 50 | log.Printf("%#v", orders) 51 | } 52 | -------------------------------------------------------------------------------- /examples/hbdm-ws-demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/frankrap/huobi-api/hbdm" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | wsURL := "wss://api.hbdm.com/ws" 10 | //wsURL := "wss://api.btcgateway.pro/ws" 11 | ws := hbdm.NewWS(wsURL, "", "") 12 | 13 | // 设置Ticker回调 14 | ws.SetTickerCallback(func(ticker *hbdm.WSTicker) { 15 | log.Printf("ticker: %#v", ticker) 16 | }) 17 | // 设置Depth回调 18 | ws.SetDepthCallback(func(depth *hbdm.WSDepth) { 19 | log.Printf("depth: %#v", depth) 20 | }) 21 | // 设置Trade回调 22 | ws.SetTradeCallback(func(trade *hbdm.WSTrade) { 23 | log.Printf("trade: %#v", trade) 24 | }) 25 | 26 | // 订阅Ticker 27 | ws.SubscribeTicker("ticker_1", "BTC_CQ") 28 | // 订阅Depth 29 | ws.SubscribeDepth("depth_1", "BTC_CQ") 30 | // 订阅Trade 31 | ws.SubscribeTrade("trade_1", "BTC_CQ") 32 | // 启动WS 33 | ws.Start() 34 | 35 | select {} 36 | } 37 | -------------------------------------------------------------------------------- /examples/hbdmswap-demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/frankrap/huobi-api/hbdmswap" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | accessKey := "[Access Key]" 10 | secretKey := "[Secret Key]" 11 | 12 | baseURL := "https://api.hbdm.com" 13 | //baseURL := "https://api.btcgateway.pro" 14 | apiParams := &hbdmswap.ApiParameter{ 15 | Debug: true, 16 | AccessKey: accessKey, 17 | SecretKey: secretKey, 18 | EnablePrivateSign: false, 19 | BaseURL: baseURL, 20 | PrivateKeyPrime256: "", 21 | } 22 | client := hbdmswap.NewClient(apiParams) 23 | 24 | symbol := "BTC-USD" 25 | client.GetAccountInfo(symbol) 26 | orderResult, err := client.Order(symbol, 27 | 0, 28 | 3000, 29 | 1, 30 | "buy", 31 | "open", 32 | 125, 33 | "limit") 34 | if err != nil { 35 | log.Fatal(err) 36 | return 37 | } 38 | log.Printf("%#v", orderResult) 39 | 40 | orders, err := client.GetOpenOrders( 41 | symbol, 42 | 0, 43 | 0, 44 | ) 45 | if err != nil { 46 | log.Fatal(err) 47 | return 48 | } 49 | log.Printf("%#v", orders) 50 | } 51 | -------------------------------------------------------------------------------- /examples/hbdmswap-ws-demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/frankrap/huobi-api/hbdmswap" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | //wsURL := "wss://api.hbdm.com/swap-ws" 10 | wsURL := "wss://api.btcgateway.pro/swap-ws" 11 | ws := hbdmswap.NewWS(wsURL, "", "") 12 | 13 | // 设置Ticker回调 14 | ws.SetTickerCallback(func(ticker *hbdmswap.WSTicker) { 15 | log.Printf("ticker: %#v", ticker) 16 | }) 17 | // 设置Depth回调 18 | ws.SetDepthCallback(func(depth *hbdmswap.WSDepth) { 19 | log.Printf("depth: %#v", depth) 20 | }) 21 | // 设置Trade回调 22 | ws.SetTradeCallback(func(trade *hbdmswap.WSTrade) { 23 | log.Printf("trade: %#v", trade) 24 | }) 25 | 26 | // 订阅Ticker 27 | ws.SubscribeTicker("ticker_1", "BTC-USD") 28 | // 订阅Depth 29 | ws.SubscribeDepth("depth_1", "BTC-USD") 30 | // 订阅Trade 31 | ws.SubscribeTrade("trade_1", "BTC-USD") 32 | // 启动WS 33 | ws.Start() 34 | 35 | select {} 36 | } 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/frankrap/huobi-api 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/json-iterator/go v1.1.9 7 | github.com/lithammer/shortuuid/v3 v3.0.4 8 | github.com/recws-org/recws v1.2.1 9 | github.com/spf13/viper v1.6.3 10 | github.com/tidwall/gjson v1.6.0 11 | gopkg.in/yaml.v2 v2.2.8 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 10 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 13 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 14 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 15 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 16 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 21 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 22 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 23 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 24 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 25 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 26 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 27 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 28 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 29 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 30 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 31 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 32 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 33 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 34 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 36 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 37 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 38 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 39 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 40 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 41 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 42 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 43 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 44 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 45 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 46 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 47 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 48 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 49 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 50 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 51 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 52 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME= 53 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 54 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 55 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 56 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 57 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 58 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 59 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 60 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 61 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 62 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 63 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 64 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 65 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 66 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 67 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 68 | github.com/lithammer/shortuuid/v3 v3.0.4 h1:uj4xhotfY92Y1Oa6n6HUiFn87CdoEHYUlTy0+IgbLrs= 69 | github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw= 70 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 71 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 72 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 73 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 74 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 75 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 76 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 77 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 78 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 79 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 80 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 81 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 82 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 83 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 84 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 85 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 86 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 87 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 88 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 89 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 90 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 91 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 92 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 93 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 94 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 95 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 96 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 97 | github.com/recws-org/recws v1.2.1 h1:bYocRkAsS71hlQ9AMCVS+hYXHEgEyQsAbYKXf394gZ8= 98 | github.com/recws-org/recws v1.2.1/go.mod h1:SxTgwQU/jqYSzEgUh4ifDxq/7enApS150f8nZ5Sczk8= 99 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 100 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 101 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 102 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 103 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 104 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 105 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 106 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 107 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 108 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 109 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 110 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 111 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 112 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 113 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 114 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 115 | github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= 116 | github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= 117 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 118 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 119 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 120 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 121 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 122 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 123 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 124 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 125 | github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= 126 | github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= 127 | github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= 128 | github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= 129 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 130 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 131 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 132 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 133 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 134 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 135 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 136 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 137 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 138 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 139 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 140 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 141 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 142 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 143 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 144 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 145 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 146 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 147 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 148 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 149 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 150 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 151 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 152 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 153 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 154 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 155 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 156 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 157 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 158 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 159 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 160 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 161 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 162 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 163 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 164 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 165 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 166 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 167 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 168 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 169 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 170 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 171 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 172 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 173 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 174 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 175 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 176 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 177 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 178 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 179 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 180 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 181 | -------------------------------------------------------------------------------- /hbdm/base.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import "github.com/json-iterator/go" 4 | 5 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 6 | -------------------------------------------------------------------------------- /hbdm/hbdm.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import ( 4 | "fmt" 5 | "github.com/frankrap/huobi-api/utils" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var ( 14 | defaultPostHeaders = map[string]string{"Content-Type": "application/json", "Accept-Language": "zh-cn"} 15 | ) 16 | 17 | type ApiParameter struct { 18 | Debug bool 19 | AccessKey string 20 | SecretKey string 21 | EnablePrivateSign bool 22 | BaseURL string 23 | PrivateKeyPrime256 string 24 | HttpClient *http.Client 25 | ProxyURL string 26 | } 27 | 28 | type Client struct { 29 | params *ApiParameter 30 | domain string 31 | httpClient *http.Client 32 | } 33 | 34 | func (c *Client) Heartbeat() (result HeartbeatResult, err error) { 35 | var resp []byte 36 | resp, err = utils.HttpGet(c.httpClient, "https://www.hbdm.com/heartbeat/", "", nil) 37 | if err != nil { 38 | return 39 | } 40 | err = json.Unmarshal(resp, &result) 41 | return 42 | } 43 | 44 | func (c *Client) doGet(path string, params *url.Values, result interface{}) (resp []byte, err error) { 45 | url := c.params.BaseURL + path + "?" + params.Encode() 46 | resp, err = utils.HttpGet( 47 | c.httpClient, 48 | url, 49 | "", 50 | nil, 51 | ) 52 | if err != nil { 53 | return 54 | } 55 | 56 | if c.params.Debug { 57 | log.Println(string(resp)) 58 | } 59 | 60 | if result == nil { 61 | return 62 | } 63 | 64 | err = json.Unmarshal(resp, result) 65 | return 66 | } 67 | 68 | func (c *Client) doPost(path string, params *url.Values, result interface{}) (resp []byte, err error) { 69 | c.sign("POST", path, params) 70 | jsonD, _ := utils.ValuesToJson(*params) 71 | 72 | url := c.params.BaseURL + path + "?" + params.Encode() 73 | resp, err = utils.HttpPost( 74 | c.httpClient, 75 | url, 76 | string(jsonD), 77 | defaultPostHeaders, 78 | ) 79 | if err != nil { 80 | return 81 | } 82 | 83 | if c.params.Debug { 84 | log.Println(string(resp)) 85 | } 86 | 87 | if result == nil { 88 | return 89 | } 90 | 91 | err = json.Unmarshal(resp, result) 92 | return 93 | } 94 | 95 | func (c *Client) sign(reqMethod, path string, postForm *url.Values) error { 96 | postForm.Set("AccessKeyId", c.params.AccessKey) 97 | postForm.Set("SignatureMethod", "HmacSHA256") 98 | postForm.Set("SignatureVersion", "2") 99 | postForm.Set("Timestamp", time.Now().UTC().Format("2006-01-02T15:04:05")) 100 | payload := fmt.Sprintf("%s\n%s\n%s\n%s", 101 | reqMethod, 102 | c.domain, 103 | path, 104 | postForm.Encode()) 105 | signature, _ := utils.GetParamHmacSHA256Base64Sign(c.params.SecretKey, payload) 106 | postForm.Set("Signature", signature) 107 | return nil 108 | } 109 | 110 | func NewClient(params *ApiParameter) *Client { 111 | domain := strings.Replace(params.BaseURL, "https://", "", -1) 112 | httpClient := params.HttpClient 113 | if httpClient == nil { 114 | httpClient = utils.DefaultHttpClient(params.ProxyURL) 115 | } 116 | return &Client{ 117 | params: params, 118 | domain: domain, 119 | httpClient: httpClient, 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /hbdm/hbdm_account.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | // GetAccountInfo 获取用户账户信息 8 | func (c *Client) GetAccountInfo(symbol string) (result AccountInfoResult, err error) { 9 | path := "/api/v1/contract_account_info" 10 | params := &url.Values{} 11 | if symbol != "" { 12 | params.Add("symbol", symbol) 13 | } 14 | _, err = c.doPost(path, params, &result) 15 | return 16 | } 17 | 18 | // GetPositionInfo 用户持仓信息 19 | func (c *Client) GetPositionInfo(symbol string) (result PositionInfoResult, err error) { 20 | path := "/api/v1/contract_position_info" 21 | params := &url.Values{} 22 | if symbol != "" { 23 | params.Add("symbol", symbol) 24 | } 25 | _, err = c.doPost(path, params, &result) 26 | return 27 | } 28 | 29 | // /api/v1/contract_sub_account_list 30 | // /api/v1/contract_sub_account_info 31 | // /api/v1/contract_sub_position_info 32 | // /api/v1/contract_financial_record 33 | // /api/v1/contract_order_limit 34 | // /api/v1/contract_fee 35 | // /api/v1/contract_transfer_limit 36 | // /api/v1/contract_position_limit 37 | // /api/v1/contract_account_position_info 38 | // /api/v1/contract_master_sub_transfer 39 | // /api/v1/contract_master_sub_transfer_record 40 | // /api/v1/contract_api_trading_status 41 | -------------------------------------------------------------------------------- /hbdm/hbdm_account_test.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import "testing" 4 | 5 | func TestClient_GetAccountInfo(t *testing.T) { 6 | c := newTestClient() 7 | info, err := c.GetAccountInfo("BTC") 8 | if err != nil { 9 | t.Error(err) 10 | return 11 | } 12 | t.Logf("%v", info) 13 | } 14 | 15 | func TestClient_GetPositionInfo(t *testing.T) { 16 | c := newTestClient() 17 | info, err := c.GetPositionInfo("BTC") 18 | if err != nil { 19 | t.Error(err) 20 | return 21 | } 22 | t.Logf("%v", info) 23 | } 24 | -------------------------------------------------------------------------------- /hbdm/hbdm_market.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | /** 10 | * 期货行情 https://huobiapi.github.io/docs/dm/v1/cn/#0737c93bf7 11 | * 12 | * @param symbol 13 | * "BTC","ETH"... 14 | * @param contractType 15 | * 合约类型: this_week:当周 next_week:下周 quarter:季度 16 | * @param contract_code 17 | * 合约code: BTC200320 18 | * @return 19 | */ 20 | func (c *Client) GetContractInfo(symbol, contractType, contractCode string) (result ContractInfoResult, err error) { 21 | path := "/api/v1/contract_contract_info" 22 | params := &url.Values{} 23 | if symbol != "" { 24 | params.Add("symbol", symbol) 25 | } 26 | if contractType != "" { 27 | params.Add("contract_type", contractType) 28 | } 29 | if contractCode != "" { 30 | params.Add("contractCode", contractCode) 31 | } 32 | _, err = c.doGet(path, params, &result) 33 | return 34 | } 35 | 36 | /** 37 | * 获取合约指数信息 https://huobiapi.github.io/docs/dm/v1/cn/#1028ab8392 38 | */ 39 | func (c *Client) GetContractIndex(symbol string) (result ContractIndexResult, err error) { 40 | path := "/api/v1/contract_index" 41 | params := &url.Values{} 42 | params.Add("symbol", symbol) 43 | _, err = c.doGet(path, params, &result) 44 | return 45 | } 46 | 47 | // api/v1/contract_price_limit 48 | // api/v1/contract_open_interest 49 | // api/v1/contract_delivery_price 50 | // api/v1/contract_api_state 51 | 52 | // GetMarketDepth 获取行情深度数据 53 | // 如"BTC_CW"表示BTC当周合约,"BTC_NW"表示BTC次周合约,"BTC_CQ"表示BTC季度合约 54 | // 获得150档深度数据,使用step0, step1, step2, step3, step4, step5(step1至step5是进行了深度合并后的深度),使用step0时,不合并深度获取150档数据;获得20档深度数据,使用 step6, step7, step8, step9, step10, step11(step7至step11是进行了深度合并后的深度),使用step6时,不合并深度获取20档数据 55 | // https://api.hbdm.com/market/depth?symbol=BTC_CQ&type=step5 56 | func (c *Client) GetMarketDepth(symbol string, _type string) (result MarketDepthResult, err error) { 57 | path := "/market/depth" 58 | params := &url.Values{} 59 | params.Add("symbol", symbol) 60 | params.Add("type", _type) 61 | _, err = c.doGet(path, params, &result) 62 | return 63 | } 64 | 65 | // 获取K线数据 https://huobiapi.github.io/docs/dm/v1/cn/#k 66 | // symbol true string 合约名称 如"BTC_CW"表示BTC当周合约,"BTC_NW"表示BTC次周合约,"BTC_CQ"表示BTC季度合约 67 | // period true string K线类型 1min, 5min, 15min, 30min, 60min,4hour,1day, 1mon 68 | // size false integer 获取数量 150 [1,2000] 69 | // from false integer 开始时间戳 10位 单位S 70 | // to false integer 结束时间戳 10位 单位S 71 | func (c *Client) GetKLine(symbol string, period string, size int, from int64, to int64) (result KLineResult, err error) { 72 | path := "/market/history/kline" 73 | params := &url.Values{} 74 | params.Add("symbol", symbol) 75 | params.Add("period", period) 76 | if size <= 0 { 77 | size = 150 78 | } 79 | params.Add("size", strconv.Itoa(size)) 80 | if from != 0 { 81 | params.Add("from", fmt.Sprintf("%v", from)) 82 | } 83 | if to != 0 { 84 | params.Add("to", fmt.Sprintf("%v", to)) 85 | } 86 | _, err = c.doGet(path, params, &result) 87 | //log.Printf("%v", string(resp)) 88 | return 89 | } 90 | 91 | // /market/detail/merged 92 | // /market/trade 93 | // /market/history/trade 94 | // /api/v1/contract_risk_info 95 | // /api/v1/contract_insurance_fund 96 | // /api/v1/contract_adjustfactor 97 | // /api/v1/contract_his_open_interest 98 | // /api/v1/contract_elite_account_ratio 99 | // /api/v1/contract_elite_position_ratio 100 | // /api/v1/contract_liquidation_orders 101 | // /api/v1/index/market/history/index 102 | // /api/v1/index/market/history/basis 103 | -------------------------------------------------------------------------------- /hbdm/hbdm_market_test.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import "testing" 4 | 5 | func TestClient_ContractInfo(t *testing.T) { 6 | c := newTestClient() 7 | contractInfo, err := c.GetContractInfo( 8 | "BTC", 9 | "this_week", 10 | "", 11 | ) 12 | if err != nil { 13 | t.Error(err) 14 | return 15 | } 16 | t.Logf("%#v", contractInfo) 17 | } 18 | 19 | func TestClient_GetContractIndex(t *testing.T) { 20 | c := newTestClient() 21 | info, err := c.GetContractIndex("BTC") 22 | if err != nil { 23 | t.Error(err) 24 | return 25 | } 26 | t.Logf("%#v", info) 27 | } 28 | 29 | func TestClient_GetMarketDepth(t *testing.T) { 30 | c := newTestClient() 31 | depth, err := c.GetMarketDepth("BTC_CQ", 32 | "step5") 33 | if err != nil { 34 | t.Error(err) 35 | return 36 | } 37 | t.Logf("%#v", depth) 38 | } 39 | 40 | func TestClient_GetKLine(t *testing.T) { 41 | c := newTestClient() 42 | klineResult, err := c.GetKLine( 43 | "BTC_CQ", 44 | "1min", 45 | 0, 46 | 0, 47 | 0, 48 | ) 49 | if err != nil { 50 | t.Error(err) 51 | return 52 | } 53 | t.Logf("%#v", klineResult) 54 | } 55 | -------------------------------------------------------------------------------- /hbdm/hbdm_test.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func newTestClient() *Client { 10 | viper.SetConfigName("test_config") 11 | viper.AddConfigPath("..") 12 | err := viper.ReadInConfig() 13 | if err != nil { 14 | log.Panic(err) 15 | } 16 | 17 | accessKey := viper.GetString("access_key") 18 | secretKey := viper.GetString("secret_key") 19 | 20 | baseURL := "https://api.btcgateway.pro" 21 | // baseURL := https://api.hbdm.com 22 | apiParams := &ApiParameter{ 23 | Debug: true, 24 | AccessKey: accessKey, 25 | SecretKey: secretKey, 26 | EnablePrivateSign: false, 27 | BaseURL: baseURL, 28 | PrivateKeyPrime256: "", 29 | } 30 | c := NewClient(apiParams) 31 | return c 32 | } 33 | 34 | func TestClient_Heartbeat(t *testing.T) { 35 | c := newTestClient() 36 | ret, err := c.Heartbeat() 37 | if err != nil { 38 | t.Error(err) 39 | return 40 | } 41 | t.Logf("%#v", ret) 42 | } 43 | -------------------------------------------------------------------------------- /hbdm/hbdm_trade.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // Order 合约下单 9 | // https://huobiapi.github.io/docs/dm/v1/cn/#9dc85ffb46 10 | /* 11 | symbol string true "BTC","ETH"... 12 | contract_type string true 合约类型 ("this_week":当周 "next_week":下周 "quarter":季度) 13 | contract_code string true BTC180914 14 | client_order_id long false 客户自己填写和维护,必须为数字 15 | price decimal false 价格 16 | volume long true 委托数量(张) 17 | direction string true "buy":买 "sell":卖 18 | offset string true "open":开 "close":平 19 | lever_rate int true 杠杆倍数[“开仓”若有10倍多单,就不能再下20倍多单] 20 | order_price_type string true 订单报价类型 "limit":限价 "opponent":对手价 "post_only":只做maker单,post only下单只受用户持仓数量限制,optimal_5:最优5档、optimal_10:最优10档、optimal_20:最优20档,ioc:IOC订单,fok:FOK订单, "opponent_ioc": 对手价-IOC下单,"optimal_5_ioc":最优5档-IOC下单,"optimal_10_ioc":最优10档-IOC下单,"optimal_20_ioc":最优20档-IOC下单,"opponent_fok": 对手价-FOK下单,"optimal_5_fok":最优5档-FOK下单,"optimal_10_fok":最优10档-FOK下单,"optimal_20_fok":最优20档-FOK下单 21 | */ 22 | func (c *Client) Order(symbol string, contractType string, contractCode string, clientOrderID int64, price float64, 23 | volume float64, direction string, offset string, leverRate int, orderPriceType string) (result OrderResult, err error) { 24 | path := "/api/v1/contract_order" 25 | params := &url.Values{} 26 | if contractCode != "" { 27 | params.Add("contract_code", contractCode) 28 | } else { 29 | params.Add("symbol", symbol) 30 | params.Add("contract_type", contractType) 31 | } 32 | if clientOrderID > 0 { 33 | params.Add("client_order_id", fmt.Sprint(clientOrderID)) 34 | } 35 | if price > 0 { 36 | params.Add("price", fmt.Sprint(price)) 37 | } 38 | params.Add("volume", fmt.Sprint(volume)) 39 | params.Add("direction", direction) 40 | params.Add("offset", offset) 41 | params.Add("lever_rate", fmt.Sprint(leverRate)) 42 | params.Add("order_price_type", orderPriceType) 43 | _, err = c.doPost(path, params, &result) 44 | return 45 | } 46 | 47 | // /api/v1/contract_batchorder 48 | 49 | /* 50 | * Cancel 撤销订单 51 | */ 52 | func (c *Client) Cancel(symbol string, orderID int64, clientOrderID int64) (result CancelResult, err error) { 53 | path := "/api/v1/contract_cancel" 54 | params := &url.Values{} 55 | params.Add("symbol", symbol) 56 | if orderID > 0 { 57 | params.Add("order_id", fmt.Sprint(orderID)) 58 | } 59 | if clientOrderID > 0 { 60 | params.Add("client_order_id", fmt.Sprint(clientOrderID)) 61 | } 62 | _, err = c.doPost(path, params, &result) 63 | return 64 | } 65 | 66 | // /api/v1/contract_cancelall 67 | 68 | /* 69 | * OrderInfo 获取合约订单信息 70 | */ 71 | func (c *Client) OrderInfo(symbol string, orderID int64, clientOrderID int64) (result OrderInfoResult, err error) { 72 | path := "/api/v1/contract_order_info" 73 | params := &url.Values{} 74 | params.Add("symbol", symbol) 75 | if orderID > 0 { 76 | params.Add("order_id", fmt.Sprint(orderID)) 77 | } 78 | if clientOrderID > 0 { 79 | params.Add("client_order_id", fmt.Sprint(clientOrderID)) 80 | } 81 | _, err = c.doPost(path, params, &result) 82 | return 83 | } 84 | 85 | // TODO: OrderDetail 86 | func (c *Client) OrderDetail(symbol string, orderID int64, createdAt int64, orderType int, pageIndex int, pageSize int) (err error) { 87 | path := "/api/v1/contract_order_detail" 88 | params := &url.Values{} 89 | params.Add("symbol", symbol) 90 | params.Add("order_id", fmt.Sprint(orderID)) 91 | if createdAt > 0 { 92 | params.Add("created_at", fmt.Sprint(createdAt)) 93 | } 94 | if orderType > 0 { 95 | params.Add("order_type", fmt.Sprint(orderType)) 96 | } 97 | if pageIndex > 0 { 98 | params.Add("page_index", fmt.Sprint(pageIndex)) 99 | } 100 | if pageSize > 0 { 101 | params.Add("page_size", fmt.Sprint(pageSize)) 102 | } 103 | _, err = c.doPost(path, params, nil) 104 | return 105 | } 106 | 107 | // GetOpenOrders 获取合约当前未成交委托单 108 | // page_index: 1,2,3... 109 | func (c *Client) GetOpenOrders(symbol string, pageIndex int, pageSize int) (result OpenOrdersResult, err error) { 110 | path := "/api/v1/contract_openorders" 111 | params := &url.Values{} 112 | params.Add("symbol", symbol) 113 | if pageIndex > 0 { 114 | params.Add("page_index", fmt.Sprint(pageIndex)) 115 | } 116 | if pageSize > 0 { 117 | params.Add("page_size", fmt.Sprint(pageSize)) 118 | } 119 | _, err = c.doPost(path, params, &result) 120 | return 121 | } 122 | 123 | // GetHisOrders 获取合约历史委托 124 | func (c *Client) GetHisOrders(symbol string, tradeType int, _type int, status int, createDate int, 125 | pageIndex int, pageSize int, contractCode string, orderType string) (result HisOrdersResult, err error) { 126 | path := "/api/v1/contract_hisorders" 127 | params := &url.Values{} 128 | params.Add("symbol", symbol) 129 | params.Add("trade_type", fmt.Sprint(tradeType)) 130 | params.Add("type", fmt.Sprint(_type)) 131 | params.Add("status", fmt.Sprint(status)) 132 | params.Add("create_date", fmt.Sprint(createDate)) 133 | if pageIndex > 0 { 134 | params.Add("page_index", fmt.Sprint(pageIndex)) 135 | } 136 | if pageSize > 0 { 137 | params.Add("page_size", fmt.Sprint(pageSize)) 138 | } 139 | if contractCode != "" { 140 | params.Add("contract_code", contractCode) 141 | } 142 | if orderType != "" { 143 | params.Add("order_type", orderType) 144 | } 145 | _, err = c.doPost(path, params, &result) 146 | return 147 | } 148 | 149 | // /api/v1/contract_matchresults 150 | 151 | // LightningClosePosition 闪电平仓下单 152 | func (c *Client) LightningClosePosition(symbol string, contractType string, contractCode string, volume int, 153 | direction string, clientOrderID int64, orderPriceType string) (result LightningClosePositionResult, err error) { 154 | path := "/api/v1/lightning_close_position" 155 | params := &url.Values{} 156 | if contractCode != "" { 157 | params.Add("contract_code", contractCode) 158 | } else { 159 | params.Add("symbol", symbol) 160 | params.Add("contract_type", contractType) 161 | } 162 | params.Add("volume", fmt.Sprint(volume)) 163 | params.Add("direction", direction) 164 | if clientOrderID > 0 { 165 | params.Add("client_order_id", fmt.Sprint(clientOrderID)) 166 | } 167 | if orderPriceType != "" { 168 | params.Add("order_price_type", orderPriceType) 169 | } 170 | _, err = c.doPost(path, params, &result) 171 | return 172 | } 173 | 174 | // /api/v1/contract_trigger_order 175 | // /api/v1/contract_trigger_cancel 176 | // /api/v1/contract_trigger_cancelall 177 | // /api/v1/contract_trigger_openorders 178 | // /api/v1/contract_trigger_hisorders 179 | -------------------------------------------------------------------------------- /hbdm/hbdm_trade_test.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import "testing" 4 | 5 | func TestClient_Order(t *testing.T) { 6 | c := newTestClient() 7 | orderResult, err := c.Order("BTC", 8 | "this_week", 9 | "", 10 | 0, 11 | 3000.0, 12 | 1, 13 | "buy", 14 | "open", 15 | 5, 16 | "limit") 17 | if err != nil { 18 | t.Error(err) 19 | return 20 | } 21 | t.Logf("%#v", orderResult) 22 | } 23 | 24 | func TestClosePosition(t *testing.T) { 25 | c := newTestClient() 26 | orderResult, err := c.Order("BTC", 27 | "this_week", 28 | "", 29 | 0, 30 | 6000.0, // Buy price must be lower than 6795.94 USD. Sell price must exceed 6294.48 USD. 31 | 1, 32 | "buy", 33 | "close", 34 | 5, 35 | "limit") 36 | if err != nil { 37 | t.Error(err) 38 | return 39 | } 40 | t.Logf("%#v", orderResult) 41 | } 42 | 43 | func TestClient_Cancel(t *testing.T) { 44 | c := newTestClient() 45 | orderID := int64(690494908993323008) // 690495528999559168 46 | cancelResult, err := c.Cancel("BTC", 47 | orderID, 0) 48 | if err != nil { 49 | t.Error(err) 50 | return 51 | } 52 | t.Logf("%#v", cancelResult) 53 | } 54 | 55 | func TestClient_OrderInfo(t *testing.T) { 56 | c := newTestClient() 57 | info, err := c.OrderInfo("BTC", 58 | 696107957142069248, 59 | 0) 60 | if err != nil { 61 | t.Error(err) 62 | return 63 | } 64 | t.Logf("%#v", info) 65 | } 66 | 67 | func TestClient_GetOpenOrders(t *testing.T) { 68 | c := newTestClient() 69 | ordersResult, err := c.GetOpenOrders("BTC", 70 | 0, 71 | 0) 72 | if err != nil { 73 | t.Error(err) 74 | return 75 | } 76 | t.Logf("%#v", ordersResult) 77 | } 78 | 79 | func TestClient_GetHisOrders(t *testing.T) { 80 | c := newTestClient() 81 | ordersResult, err := c.GetHisOrders("BTC", 82 | 0, 83 | 1, 84 | 0, 85 | 90, 86 | 1, 87 | 50, 88 | "", 89 | "1") 90 | if err != nil { 91 | t.Error(err) 92 | return 93 | } 94 | //t.Logf("%#v", ordersResult) 95 | 96 | for _, v := range ordersResult.Data.Orders { 97 | t.Logf("%#v", v) 98 | } 99 | } 100 | 101 | func TestClient_LightningClosePosition(t *testing.T) { 102 | c := newTestClient() 103 | orderResult, err := c.LightningClosePosition("BTC", 104 | "this_week", 105 | "", 106 | 1, 107 | "sell", 108 | 0, 109 | "") 110 | if err != nil { 111 | t.Error(err) 112 | return 113 | } 114 | t.Logf("%#v", orderResult) 115 | } 116 | -------------------------------------------------------------------------------- /hbdm/models.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import ( 4 | sjson "encoding/json" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | type Heartbeat struct { 10 | Heartbeat int `json:"heartbeat"` 11 | EstimatedRecoveryTime interface{} `json:"estimated_recovery_time"` 12 | SwapHeartbeat int `json:"swap_heartbeat"` 13 | SwapEstimatedRecoveryTime interface{} `json:"swap_estimated_recovery_time"` 14 | } 15 | 16 | type HeartbeatResult struct { 17 | Status string `json:"status"` 18 | Data Heartbeat `json:"data"` 19 | Ts int64 `json:"ts"` 20 | } 21 | 22 | type Order struct { 23 | Symbol string `json:"symbol"` 24 | ContractCode string `json:"contract_code"` 25 | ContractType string `json:"contract_type"` 26 | Volume float64 `json:"volume"` 27 | Price float64 `json:"price"` 28 | OrderPriceTypeRaw sjson.RawMessage `json:"order_price_type"` // 1限价单,3对手价,4闪电平仓,5计划委托,6post_only 29 | OrderType int `json:"order_type"` 30 | Direction string `json:"direction"` 31 | Offset string `json:"offset"` 32 | LeverRate int `json:"lever_rate"` 33 | OrderID int64 `json:"order_id"` 34 | ClientOrderID string `json:"client_order_id"` 35 | CreatedAt int64 `json:"created_at"` 36 | TradeVolume float64 `json:"trade_volume"` 37 | TradeTurnover float64 `json:"trade_turnover"` 38 | Fee float64 `json:"fee"` 39 | TradeAvgPrice float64 `json:"trade_avg_price"` 40 | MarginFrozen float64 `json:"margin_frozen"` 41 | Profit float64 `json:"profit"` 42 | Status int `json:"status"` 43 | OrderSource string `json:"order_source"` 44 | OrderIDStr string `json:"order_id_str"` 45 | FeeAsset string `json:"fee_asset"` 46 | LiquidationType string `json:"liquidation_type"` 47 | CreateDate int64 `json:"create_date"` 48 | } 49 | 50 | func (o *Order) OrderPriceType() string { 51 | d, err := o.OrderPriceTypeRaw.MarshalJSON() 52 | if err != nil { 53 | return "" 54 | } 55 | s := string(d) 56 | i, err := strconv.ParseInt(s, 10, 64) 57 | if err == nil { 58 | switch i { 59 | case 1: 60 | return "limit" 61 | case 3: 62 | return "opponent" 63 | case 4: 64 | return "lightning" 65 | case 5: 66 | return "trigger_order" 67 | case 6: 68 | return "post_only" 69 | case 7: 70 | return "optimal_5" 71 | case 8: 72 | return "optimal_10" 73 | case 9: 74 | return "optimal_20" 75 | case 10: 76 | return "fok" 77 | case 11: 78 | return "ioc" 79 | default: 80 | return fmt.Sprint(i) 81 | } 82 | } 83 | // 1限价单,3对手价,4闪电平仓,5计划委托,6post_only 84 | // 1:限价单、3:对手价、4:闪电平仓、5:计划委托、6:post_only、7:最优5档、8:最优10档、9:最优20档、10:fok、11:ioc 85 | // 订单报价类型 "limit":限价,"optimal_5":最优5档,"optimal_10":最优10档,"optimal_20":最优20档 86 | // 订单报价类型 "limit":限价 "opponent":对手价 "post_only":只做maker单,post only下单只受用户持仓数量限制 87 | // "limit":限价,"opponent":对手价,"lightning":闪电平仓,"optimal_5":最优5档,"optimal_10":最优10档,"optimal_20":最优20档,"fok":FOK订单,"ioc":IOC订单,"opponent_ioc": 对手价-IOC下单,"lightning_ioc":闪电平仓-IOC下单,"optimal_5_ioc":最优5档-IOC下单,"optimal_10_ioc":最优10档-IOC下单,"optimal_20_ioc":最优20档-IOC下单,"opponent_fok": 对手价-FOK下单,"lightning_fok":闪电平仓-FOK下单,"optimal_5_fok":最优5档-FOK下单,"optimal_10_fok":最优10档-FOK下单,"optimal_20_fok":最优20档-FOK下单 88 | return s 89 | } 90 | 91 | type ContractInfo struct { 92 | Symbol string `json:"symbol"` 93 | ContractCode string `json:"contract_code"` 94 | ContractType string `json:"contract_type"` 95 | ContractSize float64 `json:"contract_size"` 96 | PriceTick float64 `json:"price_tick"` 97 | DeliveryDate string `json:"delivery_date"` 98 | CreateDate string `json:"create_date"` 99 | ContractStatus int `json:"contract_status"` 100 | } 101 | 102 | type ContractInfoResult struct { 103 | Status string `json:"status"` 104 | ErrCode int `json:"err_code"` 105 | ErrMsg string `json:"err_msg"` 106 | Data []ContractInfo `json:"data"` 107 | Ts int64 `json:"ts"` 108 | } 109 | 110 | type ContractIndex struct { 111 | Symbol string `json:"symbol"` 112 | IndexPrice float64 `json:"index_price"` 113 | IndexTs int64 `json:"index_ts"` 114 | } 115 | 116 | type ContractIndexResult struct { 117 | Status string `json:"status"` 118 | ErrCode int `json:"err_code"` 119 | ErrMsg string `json:"err_msg"` 120 | Data []ContractIndex `json:"data"` 121 | Ts int64 `json:"ts"` 122 | } 123 | 124 | type Tick struct { 125 | Asks [][]float64 `json:"asks"` 126 | Bids [][]float64 `json:"bids"` 127 | Ch string `json:"ch"` 128 | ID int `json:"id"` 129 | MrID int64 `json:"mrid"` 130 | Ts int64 `json:"ts"` 131 | Version int `json:"version"` 132 | } 133 | 134 | type MarketDepthResult struct { 135 | Ch string `json:"ch"` 136 | Status string `json:"status"` 137 | ErrCode int `json:"err_code"` 138 | ErrMsg string `json:"err_msg"` 139 | Tick Tick `json:"tick"` 140 | Ts int64 `json:"ts"` 141 | } 142 | 143 | type KLine struct { 144 | Amount float64 `json:"amount"` 145 | Close float64 `json:"close"` 146 | Count int `json:"count"` 147 | High float64 `json:"high"` 148 | ID int `json:"id"` 149 | Low float64 `json:"low"` 150 | Open float64 `json:"open"` 151 | Vol int `json:"vol"` 152 | } 153 | 154 | type KLineResult struct { 155 | Ch string `json:"ch"` 156 | Data []KLine `json:"data"` 157 | Status string `json:"status"` 158 | ErrCode int `json:"err_code"` 159 | ErrMsg string `json:"err_msg"` 160 | Ts int64 `json:"ts"` 161 | } 162 | 163 | type AccountInfo struct { 164 | Symbol string `json:"symbol"` 165 | MarginBalance float64 `json:"margin_balance"` 166 | MarginPosition float64 `json:"margin_position"` 167 | MarginFrozen float64 `json:"margin_frozen"` 168 | MarginAvailable float64 `json:"margin_available"` 169 | ProfitReal float64 `json:"profit_real"` 170 | ProfitUnreal float64 `json:"profit_unreal"` 171 | RiskRate interface{} `json:"risk_rate"` 172 | WithdrawAvailable float64 `json:"withdraw_available"` 173 | LiquidationPrice interface{} `json:"liquidation_price"` 174 | LeverRate float64 `json:"lever_rate"` 175 | AdjustFactor float64 `json:"adjust_factor"` 176 | MarginStatic float64 `json:"margin_static"` 177 | IsDebit int `json:"is_debit"` 178 | } 179 | 180 | type AccountInfoResult struct { 181 | Status string `json:"status"` 182 | ErrCode int `json:"err_code"` 183 | ErrMsg string `json:"err_msg"` 184 | Data []AccountInfo `json:"data"` 185 | Ts int64 `json:"ts"` 186 | } 187 | 188 | type Position struct { 189 | Symbol string `json:"symbol"` 190 | ContractCode string `json:"contract_code"` 191 | ContractType string `json:"contract_type"` 192 | Volume float64 `json:"volume"` 193 | Available float64 `json:"available"` 194 | Frozen float64 `json:"frozen"` 195 | CostOpen float64 `json:"cost_open"` 196 | CostHold float64 `json:"cost_hold"` 197 | ProfitUnreal float64 `json:"profit_unreal"` 198 | ProfitRate float64 `json:"profit_rate"` 199 | LeverRate float64 `json:"lever_rate"` 200 | PositionMargin float64 `json:"position_margin"` 201 | Direction string `json:"direction"` 202 | Profit float64 `json:"profit"` 203 | LastPrice float64 `json:"last_price"` 204 | } 205 | 206 | type PositionInfoResult struct { 207 | Status string `json:"status"` 208 | ErrCode int `json:"err_code"` 209 | ErrMsg string `json:"err_msg"` 210 | Data []Position `json:"data"` 211 | Ts int64 `json:"ts"` 212 | } 213 | 214 | type OrderData struct { 215 | OrderID int64 `json:"order_id"` 216 | OrderIDStr string `json:"order_id_str"` 217 | ClientOrderID int64 `json:"client_order_id"` 218 | } 219 | 220 | type OrderResult struct { 221 | Status string `json:"status"` 222 | ErrCode int `json:"err_code"` 223 | ErrMsg string `json:"err_msg"` 224 | Data OrderData `json:"data"` 225 | Ts int64 `json:"ts"` 226 | } 227 | 228 | type CancelError struct { 229 | OrderID string `json:"order_id"` 230 | ErrCode int `json:"err_code"` 231 | ErrMsg string `json:"err_msg"` 232 | } 233 | 234 | type CancelData struct { 235 | Errors []CancelError `json:"errors"` 236 | Successes string `json:"successes"` 237 | } 238 | 239 | type CancelResult struct { 240 | Status string `json:"status"` 241 | ErrCode int `json:"err_code"` 242 | ErrMsg string `json:"err_msg"` 243 | Data CancelData `json:"data"` 244 | Ts int64 `json:"ts"` 245 | } 246 | 247 | type OrderInfoResult struct { 248 | Status string `json:"status"` 249 | ErrCode int `json:"err_code"` 250 | ErrMsg string `json:"err_msg"` 251 | Data []Order `json:"data"` 252 | Ts int64 `json:"ts"` 253 | } 254 | 255 | type OpenOrdersData struct { 256 | Orders []Order `json:"orders"` 257 | TotalPage int `json:"total_page"` 258 | CurrentPage int `json:"current_page"` 259 | TotalSize int `json:"total_size"` 260 | } 261 | 262 | type OpenOrdersResult struct { 263 | Status string `json:"status"` 264 | ErrCode int `json:"err_code"` 265 | ErrMsg string `json:"err_msg"` 266 | Data OpenOrdersData `json:"data"` 267 | Ts int64 `json:"ts"` 268 | } 269 | 270 | type HisOrdersData struct { 271 | Orders []Order `json:"orders"` 272 | TotalPage int `json:"total_page"` 273 | CurrentPage int `json:"current_page"` 274 | TotalSize int `json:"total_size"` 275 | } 276 | 277 | type HisOrdersResult struct { 278 | Status string `json:"status"` 279 | ErrCode int `json:"err_code"` 280 | ErrMsg string `json:"err_msg"` 281 | Data HisOrdersData `json:"data"` 282 | Ts int64 `json:"ts"` 283 | } 284 | 285 | type LightningClosePositionResult struct { 286 | Status string `json:"status"` 287 | ErrCode int `json:"err_code"` 288 | ErrMsg string `json:"err_msg"` 289 | Data OrderData `json:"data"` 290 | Ts int64 `json:"ts"` 291 | } 292 | 293 | type WSTickerTick struct { 294 | ID int64 `json:"id"` 295 | MrID int64 `json:"mrid"` 296 | Open float64 `json:"open"` 297 | Close float64 `json:"close"` 298 | High float64 `json:"high"` 299 | Low float64 `json:"low"` 300 | Amount float64 `json:"amount"` 301 | Vol float64 `json:"vol"` 302 | Count int64 `json:"count"` 303 | } 304 | 305 | type WSTicker struct { 306 | Ch string `json:"ch"` 307 | Ts int64 `json:"ts"` 308 | Tick WSTickerTick `json:"tick"` 309 | } 310 | 311 | type WSTick struct { 312 | MrID int64 `json:"mrid"` 313 | ID int `json:"id"` 314 | Bids [][]float64 `json:"bids"` 315 | Asks [][]float64 `json:"asks"` 316 | Ts int64 `json:"ts"` 317 | Version int `json:"version"` 318 | Ch string `json:"ch"` 319 | } 320 | 321 | type WSDepth struct { 322 | Ch string `json:"ch"` 323 | Ts int64 `json:"ts"` 324 | Tick Tick `json:"tick"` 325 | } 326 | 327 | type WSTickHF struct { 328 | Asks [][]float64 `json:"asks"` 329 | Bids [][]float64 `json:"bids"` 330 | Ch string `json:"ch"` 331 | Event string `json:"event"` 332 | ID int64 `json:"id"` 333 | Mrid int64 `json:"mrid"` 334 | Ts int64 `json:"ts"` 335 | Version int `json:"version"` 336 | } 337 | 338 | type WSDepthHF struct { 339 | Ch string `json:"ch"` 340 | Tick WSTickHF `json:"tick"` 341 | Ts int64 `json:"ts"` 342 | } 343 | 344 | type WSTradeItem struct { 345 | Amount int `json:"amount"` 346 | Ts int64 `json:"ts"` 347 | ID int64 `json:"id"` 348 | Price float64 `json:"price"` 349 | Direction string `json:"direction"` 350 | } 351 | 352 | type WSTradeTick struct { 353 | ID int64 `json:"id"` 354 | Ts int64 `json:"ts"` 355 | Data []WSTradeItem `json:"data"` 356 | } 357 | 358 | type WSTrade struct { 359 | Ch string `json:"ch"` 360 | Ts int64 `json:"ts"` 361 | Tick WSTradeTick `json:"tick"` 362 | } 363 | 364 | type WSOrder struct { 365 | Op string `json:"op"` 366 | Topic string `json:"topic"` 367 | Ts int64 `json:"ts"` 368 | Symbol string `json:"symbol"` 369 | ContractType string `json:"contract_type"` 370 | ContractCode string `json:"contract_code"` 371 | Volume float64 `json:"volume"` 372 | Price float64 `json:"price"` 373 | OrderPriceType string `json:"order_price_type"` 374 | Direction string `json:"direction"` 375 | Offset string `json:"offset"` 376 | Status int `json:"status"` 377 | LeverRate float64 `json:"lever_rate"` 378 | OrderID int64 `json:"order_id"` 379 | OrderIDStr string `json:"order_id_str"` 380 | ClientOrderID int64 `json:"client_order_id"` 381 | OrderSource string `json:"order_source"` 382 | OrderType int `json:"order_type"` 383 | CreatedAt int64 `json:"created_at"` 384 | TradeVolume float64 `json:"trade_volume"` 385 | TradeTurnover float64 `json:"trade_turnover"` 386 | Fee float64 `json:"fee"` 387 | TradeAvgPrice float64 `json:"trade_avg_price"` 388 | MarginFrozen float64 `json:"margin_frozen"` 389 | Profit float64 `json:"profit"` 390 | Trade []WSMyTrade `json:"trade"` 391 | LiquidationType string `json:"liquidation_type"` 392 | } 393 | 394 | type WSMyTrade struct { 395 | ID string `json:"id"` 396 | TradeID int64 `json:"trade_id"` 397 | TradeVolume float64 `json:"trade_volume"` 398 | TradePrice float64 `json:"trade_price"` 399 | TradeFee float64 `json:"trade_fee"` 400 | TradeTurnover float64 `json:"trade_turnover"` 401 | CreatedAt int64 `json:"created_at"` 402 | Role string `json:"role"` 403 | } 404 | 405 | type WSMatchOrder struct { 406 | Op string `json:"op"` 407 | Topic string `json:"topic"` 408 | Ts int64 `json:"ts"` 409 | Symbol string `json:"symbol"` 410 | ContractType string `json:"contract_type"` 411 | ContractCode string `json:"contract_code"` 412 | Status int `json:"status"` 413 | OrderID int64 `json:"order_id"` 414 | OrderIDStr string `json:"order_id_str"` 415 | OrderType int `json:"order_type"` 416 | Trade []WSMyTrade `json:"trade"` 417 | } 418 | 419 | type WSAccountData struct { 420 | Symbol string `json:"symbol"` 421 | MarginBalance float64 `json:"margin_balance"` 422 | MarginStatic float64 `json:"margin_static"` 423 | MarginPosition float64 `json:"margin_position"` 424 | MarginFrozen float64 `json:"margin_frozen"` 425 | MarginAvailable float64 `json:"margin_available"` 426 | ProfitReal float64 `json:"profit_real"` 427 | ProfitUnreal float64 `json:"profit_unreal"` 428 | WithdrawAvailable float64 `json:"withdraw_available"` 429 | RiskRate float64 `json:"risk_rate"` 430 | LiquidationPrice float64 `json:"liquidation_price"` 431 | LeverRate float64 `json:"lever_rate"` 432 | AdjustFactor float64 `json:"adjust_factor"` 433 | } 434 | 435 | type WSAccounts struct { 436 | Op string `json:"op"` 437 | Topic string `json:"topic"` 438 | Ts int64 `json:"ts"` 439 | Event string `json:"event"` 440 | Data []WSAccountData `json:"data"` 441 | } 442 | 443 | type WSPositionData struct { 444 | Symbol string `json:"symbol"` 445 | ContractCode string `json:"contract_code"` 446 | ContractType string `json:"contract_type"` 447 | Volume float64 `json:"volume"` 448 | Available float64 `json:"available"` 449 | Frozen float64 `json:"frozen"` 450 | CostOpen float64 `json:"cost_open"` 451 | CostHold float64 `json:"cost_hold"` 452 | ProfitUnreal float64 `json:"profit_unreal"` 453 | ProfitRate float64 `json:"profit_rate"` 454 | Profit float64 `json:"profit"` 455 | PositionMargin float64 `json:"position_margin"` 456 | LeverRate float64 `json:"lever_rate"` 457 | Direction string `json:"direction"` 458 | LastPrice float64 `json:"last_price"` 459 | } 460 | 461 | type WSPositions struct { 462 | Op string `json:"op"` 463 | Topic string `json:"topic"` 464 | Ts int64 `json:"ts"` 465 | Event string `json:"event"` 466 | Data []WSPositionData `json:"data"` 467 | } 468 | 469 | type WSLiquidationOrders struct { 470 | Op string `json:"op"` 471 | Topic string `json:"topic"` 472 | Ts int64 `json:"ts"` 473 | Symbol string `json:"symbol"` 474 | ContractCode string `json:"contract_code"` 475 | Direction string `json:"direction"` 476 | Offset string `json:"offset"` 477 | Volume float64 `json:"volume"` 478 | Price float64 `json:"price"` 479 | CreatedAt int64 `json:"created_at"` 480 | } 481 | -------------------------------------------------------------------------------- /hbdm/nws.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | // 接口文档 4 | // https://huobiapi.github.io/docs/dm/v1/cn/#websocket 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "github.com/frankrap/huobi-api/utils" 10 | "github.com/lithammer/shortuuid/v3" 11 | "github.com/recws-org/recws" 12 | "github.com/tidwall/gjson" 13 | "log" 14 | "net/http" 15 | "net/url" 16 | "strings" 17 | "sync" 18 | "time" 19 | ) 20 | 21 | // NWS WebSocket 订单和用户数据接口 22 | type NWS struct { 23 | sync.RWMutex 24 | 25 | ctx context.Context 26 | cancel context.CancelFunc 27 | conn recws.RecConn 28 | 29 | wsURL string 30 | cid string // 客户端请求唯一ID 31 | accessKey string 32 | secretKey string 33 | subscriptions map[string]interface{} 34 | 35 | ordersCallback func(order *WSOrder) 36 | matchOrdersCallback func(order *WSMatchOrder) 37 | accountsCallback func(accounts *WSAccounts) 38 | positionsCallback func(positions *WSPositions) 39 | liquidationOrdersCallback func(liquidationOrders *WSLiquidationOrders) 40 | } 41 | 42 | // SetProxy 设置代理地址 43 | // porxyURL: 44 | // socks5://127.0.0.1:1080 45 | // https://127.0.0.1:1080 46 | func (ws *NWS) SetProxy(proxyURL string) (err error) { 47 | var purl *url.URL 48 | purl, err = url.Parse(proxyURL) 49 | if err != nil { 50 | return 51 | } 52 | log.Printf("[ws][%s] proxy url:%s", proxyURL, purl) 53 | ws.conn.Proxy = http.ProxyURL(purl) 54 | return 55 | } 56 | 57 | // SubscribeOrders 订阅订单成交数据 58 | // symbol: BTC 59 | func (ws *NWS) SubscribeOrders(id string, symbol string) { 60 | ws.SubscribeTopic(id, 61 | fmt.Sprintf("orders.%s", strings.ToLower(symbol))) 62 | } 63 | 64 | // SubscribeMatchOrders 订阅撮合订单成交数据 65 | // symbol: BTC 66 | func (ws *NWS) SubscribeMatchOrders(id string, symbol string) { 67 | ws.SubscribeTopic(id, 68 | fmt.Sprintf("matchOrders.%s", strings.ToLower(symbol))) 69 | } 70 | 71 | // SubscribeAccounts 订阅资产变动数据 72 | // symbol: BTC 73 | func (ws *NWS) SubscribeAccounts(id string, symbol string) { 74 | ws.SubscribeTopic(id, 75 | fmt.Sprintf("accounts.%s", strings.ToLower(symbol))) 76 | } 77 | 78 | // SubscribePositions 订阅持仓变动更新数据 79 | // symbol: BTC 80 | func (ws *NWS) SubscribePositions(id string, symbol string) { 81 | ws.SubscribeTopic(id, 82 | fmt.Sprintf("positions.%s", strings.ToLower(symbol))) 83 | } 84 | 85 | // SubscribeLiquidationOrders 订阅强平订单数据 86 | // symbol: BTC 87 | func (ws *NWS) SubscribeLiquidationOrders(id string, symbol string) { 88 | ws.SubscribeTopic(id, 89 | fmt.Sprintf("liquidationOrders.%s", strings.ToLower(symbol))) 90 | } 91 | 92 | // SubscribeTopic 订阅 93 | func (ws *NWS) SubscribeTopic(id string, topic string) { 94 | ch := map[string]interface{}{ 95 | "op": "sub", 96 | "cid": ws.cid, 97 | "topic": topic} 98 | err := ws.Subscribe(id, ch) 99 | if err != nil { 100 | log.Printf("%v", err) 101 | } 102 | } 103 | 104 | // Subscribe 订阅 105 | func (ws *NWS) Subscribe(id string, ch map[string]interface{}) error { 106 | ws.Lock() 107 | defer ws.Unlock() 108 | 109 | ws.subscriptions[id] = ch 110 | return ws.sendWSMessage(ch) 111 | } 112 | 113 | // Login 授权接口 114 | func (ws *NWS) Login() error { 115 | log.Printf("Login") 116 | if ws.accessKey == "" || ws.secretKey == "" { 117 | return fmt.Errorf("missing accessKey or secretKey") 118 | } 119 | opReq := map[string]string{ 120 | "op": "auth", 121 | "type": "api", 122 | } 123 | err := ws.setSignatureData(opReq, ws.accessKey, ws.secretKey) 124 | if err != nil { 125 | return err 126 | } 127 | log.Printf("opReq: %#v", opReq) 128 | return ws.sendWSMessage(opReq) 129 | } 130 | 131 | func (ws *NWS) setSignatureData(data map[string]string, apiKey, apiSecretKey string) error { 132 | data["AccessKeyId"] = apiKey 133 | data["SignatureMethod"] = "HmacSHA256" 134 | data["SignatureVersion"] = "2" 135 | data["Timestamp"] = time.Now().UTC().Format("2006-01-02T15:04:05") 136 | postForm := url.Values{} 137 | // 当type为api时,参数op,type,cid,Signature不参加签名计算 138 | isApi := data["type"] == "api" 139 | for k, v := range data { 140 | if isApi && (k == "op" || k == "cid" || k == "type") { 141 | continue 142 | } 143 | postForm.Set(k, v) 144 | } 145 | u, err := url.Parse(ws.wsURL) 146 | if err != nil { 147 | return err 148 | } 149 | payload := fmt.Sprintf("%s\n%s\n%s\n%s", "GET", u.Host, u.Path, postForm.Encode()) 150 | sign, _ := utils.GetParamHmacSHA256Base64Sign(apiSecretKey, payload) 151 | data["Signature"] = sign 152 | return nil 153 | } 154 | 155 | func (ws *NWS) SetOrdersCallback(callback func(order *WSOrder)) { 156 | ws.ordersCallback = callback 157 | } 158 | 159 | func (ws *NWS) SetMatchOrdersCallback(callback func(order *WSMatchOrder)) { 160 | ws.matchOrdersCallback = callback 161 | } 162 | 163 | func (ws *NWS) SetAccountsCallback(callback func(accounts *WSAccounts)) { 164 | ws.accountsCallback = callback 165 | } 166 | 167 | func (ws *NWS) SetPositionsCallback(callback func(positions *WSPositions)) { 168 | ws.positionsCallback = callback 169 | } 170 | 171 | func (ws *NWS) SetLiquidationOrdersCallback(callback func(liquidationOrders *WSLiquidationOrders)) { 172 | ws.liquidationOrdersCallback = callback 173 | } 174 | 175 | // Unsubscribe 取消订阅 176 | func (ws *NWS) Unsubscribe(id string) error { 177 | ws.Lock() 178 | defer ws.Unlock() 179 | 180 | if v, ok := ws.subscriptions[id]; ok { 181 | ch, ok := v.(map[string]interface{}) 182 | if ok { 183 | ch["op"] = "unsub" 184 | log.Printf("取消订阅: %#v", ch) 185 | ws.sendWSMessage(ch) 186 | } 187 | delete(ws.subscriptions, id) 188 | } 189 | return nil 190 | } 191 | 192 | func (ws *NWS) unsubscribe(symbol string) { 193 | ch := map[string]interface{}{ 194 | "op": "unsub", 195 | "cid": ws.cid, 196 | "topic": fmt.Sprintf("matchOrders.%s", strings.ToLower(symbol))} 197 | ws.sendWSMessage(ch) 198 | } 199 | 200 | func (ws *NWS) subscribeHandler() error { 201 | //log.Printf("subscribeHandler") 202 | ws.Lock() 203 | defer ws.Unlock() 204 | 205 | // 授权 206 | if ws.accessKey != "" && ws.secretKey != "" { 207 | err := ws.Login() 208 | if err != nil { 209 | log.Printf("%v", err) 210 | return err 211 | } 212 | //time.Sleep(1*time.Second) 213 | } 214 | 215 | for _, v := range ws.subscriptions { 216 | log.Printf("sub: %#v", v) 217 | err := ws.sendWSMessage(v) 218 | if err != nil { 219 | log.Printf("%v", err) 220 | } 221 | } 222 | return nil 223 | } 224 | 225 | func (ws *NWS) sendWSMessage(msg interface{}) error { 226 | return ws.conn.WriteJSON(msg) 227 | } 228 | 229 | func (ws *NWS) Start() { 230 | ws.conn.Dial(ws.wsURL, nil) 231 | go ws.run() 232 | } 233 | 234 | func (ws *NWS) run() { 235 | ctx := context.Background() 236 | for { 237 | select { 238 | case <-ctx.Done(): 239 | go ws.conn.Close() 240 | log.Printf("Websocket closed %s", ws.conn.GetURL()) 241 | return 242 | default: 243 | messageType, msg, err := ws.conn.ReadMessage() 244 | if err != nil { 245 | log.Printf("Read error: %v", err) 246 | time.Sleep(100 * time.Millisecond) 247 | continue 248 | } 249 | 250 | msg, err = utils.GzipUncompress(msg) 251 | if err != nil { 252 | continue 253 | } 254 | 255 | ws.handleMsg(messageType, msg) 256 | } 257 | } 258 | } 259 | 260 | func (ws *NWS) handleMsg(messageType int, msg []byte) { 261 | ret := gjson.ParseBytes(msg) 262 | 263 | // 认证成功 264 | // {"op":"auth","type":"api","err-code":0,"ts":1586579120653,"data":{"user-id":"100726"}} 265 | 266 | // 订阅成功 267 | // {"op":"sub","cid":"40sG903yz80oDFWr","topic":"matchOrders.btc","ts":1586579121646,"err-code":0} 268 | 269 | // 下单事件 270 | // {"op":"notify","topic":"matchOrders.btc","ts":1586579194161,"symbol":"BTC","contract_type":"this_week","contract_code":"BTC200417","status":3,"order_id":698509296681648128,"order_id_str":"698509296681648128","order_type":1,"trade":[]} 271 | 272 | // 撤单事件 273 | // {"op":"notify","topic":"matchOrders.btc","ts":1586579226100,"symbol":"BTC","contract_type":"this_week","contract_code":"BTC200417","status":7,"order_id":698509296681648128,"order_id_str":"698509296681648128","order_type":2,"trade":[]} 274 | 275 | // 下单事件 276 | // {"op":"notify","topic":"orders.btc","ts":1586579446522,"symbol":"BTC","contract_type":"this_week","contract_code":"BTC200417","volume":2,"price":6200,"order_price_type":"limit","direction":"buy","offset":"open","status":3,"lever_rate":10,"order_id":698510354724319232,"order_id_str":"698510354724319232","client_order_id":null,"order_source":"web","order_type":1,"created_at":1586579446261,"trade_volume":0,"trade_turnover":0,"fee":0,"trade_avg_price":0.00,"margin_frozen":0.003225806451612903,"profit":0,"trade":[],"liquidation_type":"0"} 277 | 278 | // 撤单 279 | // {"op":"notify","topic":"orders.btc","ts":1586579526850,"symbol":"BTC","contract_type":"this_week","contract_code":"BTC200417","volume":2,"price":6200,"order_price_type":"limit","direction":"buy","offset":"open","status":7,"lever_rate":10,"order_id":698510354724319232,"order_id_str":"698510354724319232","client_order_id":null,"order_source":"web","order_type":2,"created_at":1586579446261,"trade_volume":0,"trade_turnover":0,"fee":0,"trade_avg_price":0.00,"margin_frozen":0E-18,"profit":0,"trade":[],"liquidation_type":"0"} 280 | 281 | if opValue := ret.Get("op"); opValue.Exists() { 282 | op := opValue.String() 283 | if op == "ping" { 284 | ts := ret.Get("ts").Int() 285 | ws.handlePing(ts) 286 | return 287 | } else if op == "notify" { 288 | topicValue := ret.Get("topic") 289 | if !topicValue.Exists() { 290 | log.Printf("err") 291 | return 292 | } 293 | topic := topicValue.String() 294 | ws.handleNotify(topic, msg...) 295 | } 296 | } 297 | 298 | //log.Printf("%v", string(msg)) 299 | } 300 | 301 | func (ws *NWS) handleNotify(topic string, msg ...byte) { 302 | if strings.HasPrefix(topic, "orders.") { 303 | var value WSOrder 304 | err := json.Unmarshal(msg, &value) 305 | if err != nil { 306 | log.Printf("%v", err) 307 | return 308 | } 309 | 310 | if ws.ordersCallback != nil { 311 | ws.ordersCallback(&value) 312 | } 313 | return 314 | } else if strings.HasPrefix(topic, "matchOrders.") { 315 | var value WSMatchOrder 316 | err := json.Unmarshal(msg, &value) 317 | if err != nil { 318 | log.Printf("%v", err) 319 | return 320 | } 321 | 322 | if ws.matchOrdersCallback != nil { 323 | ws.matchOrdersCallback(&value) 324 | } 325 | return 326 | } else if strings.HasPrefix(topic, "accounts.") { 327 | var value WSAccounts 328 | err := json.Unmarshal(msg, &value) 329 | if err != nil { 330 | log.Printf("%v", err) 331 | return 332 | } 333 | 334 | if ws.accountsCallback != nil { 335 | ws.accountsCallback(&value) 336 | } 337 | return 338 | } else if strings.HasPrefix(topic, "positions.") { 339 | var value WSPositions 340 | err := json.Unmarshal(msg, &value) 341 | if err != nil { 342 | log.Printf("%v", err) 343 | return 344 | } 345 | 346 | if ws.positionsCallback != nil { 347 | ws.positionsCallback(&value) 348 | } 349 | return 350 | } else if strings.HasPrefix(topic, "liquidationOrders.") { 351 | var value WSLiquidationOrders 352 | err := json.Unmarshal(msg, &value) 353 | if err != nil { 354 | log.Printf("%v", err) 355 | return 356 | } 357 | 358 | if ws.liquidationOrdersCallback != nil { 359 | ws.liquidationOrdersCallback(&value) 360 | } 361 | return 362 | } 363 | } 364 | 365 | func (ws *NWS) handlePing(ts int64) { 366 | pong := struct { 367 | Op string `json:"op"` 368 | Ts int64 `json:"ts"` 369 | }{"pong", ts} 370 | 371 | err := ws.sendWSMessage(pong) 372 | if err != nil { 373 | log.Printf("Send pong error: %v", err) 374 | } 375 | } 376 | 377 | // NewNWS 创建 NWS 378 | // wsURL: 379 | // 正式地址 wss://api.hbdm.com/notification 380 | // 开发地址 wss://api.btcgateway.pro/notification 381 | func NewNWS(wsURL string, accessKey string, secretKey string) *NWS { 382 | ws := &NWS{ 383 | wsURL: wsURL, 384 | cid: shortuuid.New(), 385 | accessKey: accessKey, 386 | secretKey: secretKey, 387 | subscriptions: make(map[string]interface{}), 388 | } 389 | ws.ctx, ws.cancel = context.WithCancel(context.Background()) 390 | ws.conn = recws.RecConn{ 391 | KeepAliveTimeout: 10 * time.Second, 392 | } 393 | ws.conn.SubscribeHandler = ws.subscribeHandler 394 | return ws 395 | } 396 | -------------------------------------------------------------------------------- /hbdm/nws_test.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func newNWSTest() *NWS { 10 | viper.SetConfigName("test_config") 11 | viper.AddConfigPath("..") 12 | err := viper.ReadInConfig() 13 | if err != nil { 14 | log.Panic(err) 15 | } 16 | 17 | accessKey := viper.GetString("access_key") 18 | secretKey := viper.GetString("secret_key") 19 | 20 | wsURL := "wss://api.btcgateway.pro/notification" 21 | ws := NewNWS(wsURL, accessKey, secretKey) 22 | return ws 23 | } 24 | 25 | func TestNWS_SubscribeOrders(t *testing.T) { 26 | ws := newNWSTest() 27 | 28 | // 设置回调 29 | ws.SetOrdersCallback(func(order *WSOrder) { 30 | log.Printf("order: %#v", order) 31 | }) 32 | 33 | // 订阅 34 | ws.SubscribeOrders("orders_1", "BTC") 35 | // 取消订阅 36 | //ws.Unsubscribe("orders_1") 37 | 38 | //go func() { 39 | // time.Sleep(30*time.Second) 40 | // ws.Unsubscribe("orders_1") 41 | //}() 42 | 43 | ws.Start() 44 | 45 | select {} 46 | } 47 | -------------------------------------------------------------------------------- /hbdm/ws.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | // 接口文档 4 | // https://huobiapi.github.io/docs/dm/v1/cn/#websocket 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "github.com/frankrap/huobi-api/utils" 10 | "github.com/lithammer/shortuuid/v3" 11 | "github.com/recws-org/recws" 12 | "github.com/tidwall/gjson" 13 | "log" 14 | "net/http" 15 | "net/url" 16 | "strings" 17 | "sync" 18 | "time" 19 | ) 20 | 21 | // WS WebSocket 市场行情接口 22 | type WS struct { 23 | sync.RWMutex 24 | 25 | ctx context.Context 26 | cancel context.CancelFunc 27 | conn recws.RecConn 28 | 29 | wsURL string // Public 30 | cid string // 客户端请求唯一ID 31 | accessKey string 32 | secretKey string 33 | debugMode bool 34 | subscriptions map[string]interface{} 35 | 36 | tickerCallback func(trade *WSTicker) 37 | depthCallback func(depth *WSDepth) 38 | depthHFCallback func(depth *WSDepthHF) 39 | tradeCallback func(trade *WSTrade) 40 | } 41 | 42 | // SetProxy 设置代理地址 43 | // porxyURL: 44 | // socks5://127.0.0.1:1080 45 | // https://127.0.0.1:1080 46 | func (ws *WS) SetProxy(proxyURL string) (err error) { 47 | var purl *url.URL 48 | purl, err = url.Parse(proxyURL) 49 | if err != nil { 50 | return 51 | } 52 | log.Printf("[ws][%s] proxy url:%s", proxyURL, purl) 53 | ws.conn.Proxy = http.ProxyURL(purl) 54 | return 55 | } 56 | 57 | // SubscribeTicker 订阅 Market Ticker 数据 58 | // id: 订阅的编号 59 | // symbol: BTC_CQ 60 | func (ws *WS) SubscribeTicker(id string, symbol string) { 61 | ch := map[string]interface{}{ 62 | "id": id, 63 | "sub": fmt.Sprintf("market.%s.detail", symbol)} 64 | ws.Subscribe(id, ch) 65 | } 66 | 67 | // SubscribeDepth 订阅 Market Depth 数据 68 | // id: 订阅的编号 69 | // symbol: BTC_CQ 70 | func (ws *WS) SubscribeDepth(id string, symbol string) { 71 | ch := map[string]interface{}{ 72 | "id": id, 73 | "sub": fmt.Sprintf("market.%s.depth.step0", symbol), 74 | } 75 | ws.Subscribe(id, ch) 76 | } 77 | 78 | // SubscribeDepthHF 订阅增量深度 79 | // size: 20/150 档位数,20:表示20档不合并的深度,150:表示150档不合并的深度 80 | // dateType: 数据类型,不填默认为全量数据,"incremental":增量数据,"snapshot":全量数据 81 | func (ws *WS) SubscribeDepthHF(id string, symbol string, size int, dateType string) { 82 | ch := map[string]interface{}{ 83 | "id": id, 84 | "sub": fmt.Sprintf("market.%v.depth.size_%v.high_freq", symbol, size), 85 | "data_type": dateType, 86 | } 87 | ws.Subscribe(id, ch) 88 | } 89 | 90 | // SubscribeTrade 订阅 Market Trade 数据 91 | // id: 订阅的编号 92 | // symbol: BTC_CQ 93 | func (ws *WS) SubscribeTrade(id string, symbol string) { 94 | ch := map[string]interface{}{ 95 | "id": id, 96 | "sub": fmt.Sprintf("market.%s.trade.detail", symbol)} 97 | ws.Subscribe(id, ch) 98 | } 99 | 100 | // Subscribe 订阅 101 | func (ws *WS) Subscribe(id string, ch map[string]interface{}) error { 102 | ws.Lock() 103 | defer ws.Unlock() 104 | 105 | ws.subscriptions[id] = ch 106 | return ws.sendWSMessage(ch) 107 | } 108 | 109 | func (ws *WS) SetTickerCallback(callback func(ticker *WSTicker)) { 110 | ws.tickerCallback = callback 111 | } 112 | 113 | func (ws *WS) SetDepthCallback(callback func(depth *WSDepth)) { 114 | ws.depthCallback = callback 115 | } 116 | 117 | func (ws *WS) SetDepthHFCallback(callback func(depth *WSDepthHF)) { 118 | ws.depthHFCallback = callback 119 | } 120 | 121 | func (ws *WS) SetTradeCallback(callback func(trade *WSTrade)) { 122 | ws.tradeCallback = callback 123 | } 124 | 125 | // Unsubscribe 取消订阅 126 | func (ws *WS) Unsubscribe(id string) error { 127 | ws.Lock() 128 | defer ws.Unlock() 129 | 130 | if _, ok := ws.subscriptions[id]; ok { 131 | delete(ws.subscriptions, id) 132 | } 133 | return nil 134 | } 135 | 136 | func (ws *WS) subscribeHandler() error { 137 | //log.Printf("subscribeHandler") 138 | ws.Lock() 139 | defer ws.Unlock() 140 | 141 | for _, v := range ws.subscriptions { 142 | //log.Printf("sub: %#v", v) 143 | err := ws.sendWSMessage(v) 144 | if err != nil { 145 | log.Printf("%v", err) 146 | } 147 | } 148 | return nil 149 | } 150 | 151 | func (ws *WS) sendWSMessage(msg interface{}) error { 152 | return ws.conn.WriteJSON(msg) 153 | } 154 | 155 | func (ws *WS) Start() { 156 | ws.conn.Dial(ws.wsURL, nil) 157 | go ws.run() 158 | } 159 | 160 | func (ws *WS) run() { 161 | ctx := context.Background() 162 | for { 163 | select { 164 | case <-ctx.Done(): 165 | go ws.conn.Close() 166 | log.Printf("Websocket closed %s", ws.conn.GetURL()) 167 | return 168 | default: 169 | messageType, msg, err := ws.conn.ReadMessage() 170 | if err != nil { 171 | log.Printf("Read error: %v", err) 172 | time.Sleep(100 * time.Millisecond) 173 | continue 174 | } 175 | 176 | msg, err = utils.GzipUncompress(msg) 177 | if err != nil { 178 | continue 179 | } 180 | 181 | ws.handleMsg(messageType, msg) 182 | } 183 | } 184 | } 185 | 186 | func (ws *WS) handleMsg(messageType int, msg []byte) { 187 | ret := gjson.ParseBytes(msg) 188 | 189 | if ws.debugMode { 190 | log.Printf("%v", string(msg)) 191 | } 192 | 193 | if pingValue := ret.Get("ping"); pingValue.Exists() { 194 | // 心跳 195 | ping := pingValue.Int() 196 | ws.handlePing(ping) 197 | return 198 | } 199 | 200 | // 订阅成功返回消息 201 | // {"id":"depth_1","subbed":"market.BTC_CQ.depth.step0","ts":1586498957314,"status":"ok"} 202 | 203 | if chValue := ret.Get("ch"); chValue.Exists() { 204 | ch := chValue.String() 205 | if strings.HasPrefix(ch, "market") { 206 | if strings.HasSuffix(ch, ".high_freq") { 207 | // market.BTC_CQ.depth.size_20.high_freq 208 | var depth WSDepthHF 209 | err := json.Unmarshal(msg, &depth) 210 | if err != nil { 211 | log.Printf("%v", err) 212 | return 213 | } 214 | 215 | if ws.depthHFCallback != nil { 216 | ws.depthHFCallback(&depth) 217 | } 218 | } else if strings.Contains(ch, ".depth") { 219 | // market.BTC_CQ.depth.step0 220 | var depth WSDepth 221 | err := json.Unmarshal(msg, &depth) 222 | if err != nil { 223 | log.Printf("%v", err) 224 | return 225 | } 226 | 227 | if ws.depthCallback != nil { 228 | ws.depthCallback(&depth) 229 | } 230 | } else if strings.HasSuffix(ch, ".trade.detail") { 231 | //log.Printf("%v", string(msg)) 232 | var trade WSTrade 233 | err := json.Unmarshal(msg, &trade) 234 | if err != nil { 235 | log.Printf("%v", err) 236 | return 237 | } 238 | 239 | if ws.tradeCallback != nil { 240 | ws.tradeCallback(&trade) 241 | } 242 | } else if strings.HasSuffix(ch, ".detail") { 243 | var ticker WSTicker 244 | err := json.Unmarshal(msg, &ticker) 245 | if err != nil { 246 | log.Printf("%v", err) 247 | return 248 | } 249 | 250 | if ws.tickerCallback != nil { 251 | ws.tickerCallback(&ticker) 252 | } 253 | } else { 254 | log.Printf("%v", string(msg)) 255 | } 256 | } else { 257 | log.Printf("msg: %v", string(msg)) 258 | } 259 | return 260 | } 261 | } 262 | 263 | func (ws *WS) handlePing(ping int64) { 264 | pong := struct { 265 | Pong int64 `json:"pong"` 266 | }{ping} 267 | 268 | err := ws.sendWSMessage(pong) 269 | if err != nil { 270 | log.Printf("Send pong error: %v", err) 271 | } 272 | } 273 | 274 | // NewWS 创建 WS 275 | // wsURL: 276 | // 正式地址 wss://api.hbdm.com/ws 277 | // 开发地址 wss://api.btcgateway.pro/ws 278 | func NewWS(wsURL string, accessKey string, secretKey string, debugMode bool) *WS { 279 | ws := &WS{ 280 | wsURL: wsURL, 281 | cid: shortuuid.New(), 282 | accessKey: accessKey, 283 | secretKey: secretKey, 284 | debugMode: debugMode, 285 | subscriptions: make(map[string]interface{}), 286 | } 287 | ws.ctx, ws.cancel = context.WithCancel(context.Background()) 288 | ws.conn = recws.RecConn{ 289 | KeepAliveTimeout: 10 * time.Second, 290 | } 291 | ws.conn.SubscribeHandler = ws.subscribeHandler 292 | return ws 293 | } 294 | -------------------------------------------------------------------------------- /hbdm/ws_test.go: -------------------------------------------------------------------------------- 1 | package hbdm 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func newWSTest() *WS { 10 | viper.SetConfigName("test_config") 11 | viper.AddConfigPath("..") 12 | err := viper.ReadInConfig() 13 | if err != nil { 14 | log.Panic(err) 15 | } 16 | 17 | accessKey := viper.GetString("access_key") 18 | secretKey := viper.GetString("secret_key") 19 | 20 | wsURL := "wss://api.btcgateway.pro/ws" 21 | ws := NewWS(wsURL, accessKey, secretKey, true) 22 | return ws 23 | } 24 | 25 | func TestWS_SubscribeTicker(t *testing.T) { 26 | ws := newWSTest() 27 | 28 | ws.SetTickerCallback(func(ticker *WSTicker) { 29 | log.Printf("ticker: %#v", ticker) 30 | }) 31 | ws.SubscribeTicker("ticker_1", "BTC_CQ") 32 | ws.Start() 33 | 34 | select {} 35 | } 36 | 37 | func TestWS_SubscribeDepth(t *testing.T) { 38 | ws := newWSTest() 39 | 40 | ws.SetDepthCallback(func(depth *WSDepth) { 41 | log.Printf("depth: %#v", depth) 42 | }) 43 | ws.SubscribeDepth("depth_1", "BTC_CQ") 44 | ws.Start() 45 | 46 | select {} 47 | } 48 | 49 | func TestWS_SubscribeDepthHighFreq(t *testing.T) { 50 | ws := newWSTest() 51 | 52 | ws.SetDepthHFCallback(func(depth *WSDepthHF) { 53 | log.Printf("depth: %#v", depth) 54 | }) 55 | ws.SubscribeDepthHF("depth_1", "BTC_CQ", 20, "incremental") 56 | ws.Start() 57 | 58 | select {} 59 | } 60 | 61 | func TestWS_SubscribeTrade(t *testing.T) { 62 | ws := newWSTest() 63 | 64 | ws.SetTradeCallback(func(trade *WSTrade) { 65 | log.Printf("trade: %#v", trade) 66 | }) 67 | ws.SubscribeTrade("trade_1", "BTC_CQ") 68 | ws.Start() 69 | 70 | select {} 71 | } 72 | -------------------------------------------------------------------------------- /hbdmswap/base.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import "github.com/json-iterator/go" 4 | 5 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 6 | -------------------------------------------------------------------------------- /hbdmswap/hbdmswap.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import ( 4 | "fmt" 5 | "github.com/frankrap/huobi-api/utils" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var ( 14 | defaultPostHeaders = map[string]string{"Content-Type": "application/json", "Accept-Language": "zh-cn"} 15 | ) 16 | 17 | type ApiParameter struct { 18 | Debug bool 19 | AccessKey string 20 | SecretKey string 21 | EnablePrivateSign bool 22 | BaseURL string 23 | PrivateKeyPrime256 string 24 | HttpClient *http.Client 25 | ProxyURL string 26 | } 27 | 28 | type Client struct { 29 | params *ApiParameter 30 | domain string 31 | httpClient *http.Client 32 | } 33 | 34 | func (c *Client) Heartbeat() (result HeartbeatResult, err error) { 35 | var resp []byte 36 | resp, err = utils.HttpGet(c.httpClient, "https://www.hbdm.com/heartbeat/", "", nil) 37 | if err != nil { 38 | return 39 | } 40 | err = json.Unmarshal(resp, &result) 41 | return 42 | } 43 | 44 | func (c *Client) doGet(path string, params *url.Values, result interface{}) (resp []byte, err error) { 45 | url := c.params.BaseURL + path + "?" + params.Encode() 46 | resp, err = utils.HttpGet( 47 | c.httpClient, 48 | url, 49 | "", 50 | nil, 51 | ) 52 | if err != nil { 53 | return 54 | } 55 | 56 | if c.params.Debug { 57 | log.Println(string(resp)) 58 | } 59 | 60 | if result == nil { 61 | return 62 | } 63 | 64 | err = json.Unmarshal(resp, result) 65 | return 66 | } 67 | 68 | func (c *Client) doPost(path string, params *url.Values, result interface{}) (resp []byte, err error) { 69 | c.sign("POST", path, params) 70 | jsonD, _ := utils.ValuesToJson(*params) 71 | 72 | url := c.params.BaseURL + path + "?" + params.Encode() 73 | resp, err = utils.HttpPost( 74 | c.httpClient, 75 | url, 76 | string(jsonD), 77 | defaultPostHeaders, 78 | ) 79 | if err != nil { 80 | return 81 | } 82 | 83 | if c.params.Debug { 84 | log.Println(string(resp)) 85 | } 86 | 87 | if result == nil { 88 | return 89 | } 90 | 91 | err = json.Unmarshal(resp, result) 92 | return 93 | } 94 | 95 | func (c *Client) sign(reqMethod, path string, postForm *url.Values) error { 96 | postForm.Set("AccessKeyId", c.params.AccessKey) 97 | postForm.Set("SignatureMethod", "HmacSHA256") 98 | postForm.Set("SignatureVersion", "2") 99 | postForm.Set("Timestamp", time.Now().UTC().Format("2006-01-02T15:04:05")) 100 | payload := fmt.Sprintf("%s\n%s\n%s\n%s", 101 | reqMethod, 102 | c.domain, 103 | path, 104 | postForm.Encode(), 105 | ) 106 | signature, _ := utils.GetParamHmacSHA256Base64Sign(c.params.SecretKey, payload) 107 | postForm.Set("Signature", signature) 108 | return nil 109 | } 110 | 111 | func NewClient(params *ApiParameter) *Client { 112 | domain := strings.Replace(params.BaseURL, "https://", "", -1) 113 | httpClient := params.HttpClient 114 | if httpClient == nil { 115 | httpClient = utils.DefaultHttpClient(params.ProxyURL) 116 | } 117 | return &Client{ 118 | params: params, 119 | domain: domain, 120 | httpClient: httpClient, 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /hbdmswap/hbdmswap_account.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import "net/url" 4 | 5 | // GetAccountInfo 获取用户账户信息 6 | func (c *Client) GetAccountInfo(contractCode string) (result AccountInfoResult, err error) { 7 | path := "/swap-api/v1/swap_account_info" 8 | params := &url.Values{} 9 | if contractCode != "" { 10 | params.Add("contract_code", contractCode) 11 | } 12 | _, err = c.doPost(path, params, &result) 13 | return 14 | } 15 | 16 | // GetPositionInfo 用户持仓信息 17 | func (c *Client) GetPositionInfo(contractCode string) (result PositionInfoResult, err error) { 18 | path := "/swap-api/v1/swap_position_info" 19 | params := &url.Values{} 20 | if contractCode != "" { 21 | params.Add("contract_code", contractCode) 22 | } 23 | _, err = c.doPost(path, params, &result) 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /hbdmswap/hbdmswap_account_test.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import "testing" 4 | 5 | func TestClient_GetAccountInfo(t *testing.T) { 6 | c := newTestClient() 7 | accountInfo, err := c.GetAccountInfo("BTC-USD") 8 | if err != nil { 9 | t.Error(err) 10 | return 11 | } 12 | 13 | for _, v := range accountInfo.Data { 14 | t.Logf("%#v", v) 15 | } 16 | } 17 | 18 | func TestClient_GetPositionInfo(t *testing.T) { 19 | c := newTestClient() 20 | positions, err := c.GetPositionInfo("BTC-USD") 21 | if err != nil { 22 | t.Error(err) 23 | return 24 | } 25 | for _, v := range positions.Data { 26 | t.Logf("%#v", v) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /hbdmswap/hbdmswap_market.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | // GetMarketDepth 获取行情深度数据 10 | // step0-150档 step6-20档 11 | // (150档数据) step0, step1, step2, step3, step4, step5(合并深度1-5);step0时,不合并深度, (20档数据) step6, step7, step8, step9, step10, step11(合并深度7-11);step6时,不合并深度 12 | func (c *Client) GetMarketDepth(contractCode string, _type string) (result MarketDepthResult, err error) { 13 | path := "/swap-ex/market/depth" 14 | params := &url.Values{} 15 | params.Add("contract_code", contractCode) 16 | params.Add("type", _type) 17 | //var resp []byte 18 | _, err = c.doGet(path, params, &result) 19 | //log.Printf("%v", string(resp)) 20 | return 21 | } 22 | 23 | // 获取K线数据 24 | // symbol: BTC-USD 25 | // period: 1min, 5min, 15min, 30min, 60min, 4hour, 1day, 1mon 26 | 27 | func (c *Client) GetKLine(symbol string, period string, size int, from int64, to int64) (result KLineResult, err error) { 28 | path := "/swap-ex/market/history/kline" 29 | params := &url.Values{} 30 | params.Add("contract_code", symbol) 31 | params.Add("period", period) 32 | if size <= 0 { 33 | size = 150 34 | } 35 | params.Add("size", strconv.Itoa(size)) 36 | if from != 0 { 37 | params.Add("from", fmt.Sprintf("%v", from)) 38 | } 39 | if to != 0 { 40 | params.Add("to", fmt.Sprintf("%v", to)) 41 | } 42 | //var resp []byte 43 | _, err = c.doGet(path, params, &result) 44 | //log.Printf("%v", string(resp)) 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /hbdmswap/hbdmswap_market_test.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import "testing" 4 | 5 | func TestClient_GetMarketDepth(t *testing.T) { 6 | b := newTestClient() 7 | depth, err := b.GetMarketDepth("BTC-USD", 8 | "step6") 9 | if err != nil { 10 | t.Error(err) 11 | return 12 | } 13 | 14 | //t.Logf("%#v", depth) 15 | 16 | for _, v := range depth.Tick.Asks { 17 | t.Logf("%#v", v) 18 | } 19 | } 20 | 21 | func TestClient_GetKLine(t *testing.T) { 22 | b := newTestClient() 23 | kLine, err := b.GetKLine("BTC-USD", 24 | "1min", 10, 0, 0) 25 | if err != nil { 26 | t.Error(err) 27 | return 28 | } 29 | t.Logf("%#v", kLine) 30 | } 31 | -------------------------------------------------------------------------------- /hbdmswap/hbdmswap_test.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func newTestClient() *Client { 10 | viper.SetConfigName("test_config") 11 | viper.AddConfigPath("..") 12 | err := viper.ReadInConfig() 13 | if err != nil { 14 | log.Panic(err) 15 | } 16 | 17 | accessKey := viper.GetString("access_key") 18 | secretKey := viper.GetString("secret_key") 19 | 20 | baseURL := "https://api.btcgateway.pro" 21 | // baseURL := https://api.hbdm.com 22 | apiParams := &ApiParameter{ 23 | Debug: true, 24 | AccessKey: accessKey, 25 | SecretKey: secretKey, 26 | EnablePrivateSign: false, 27 | BaseURL: baseURL, 28 | PrivateKeyPrime256: "", 29 | } 30 | c := NewClient(apiParams) 31 | return c 32 | } 33 | 34 | func TestClient_Heartbeat(t *testing.T) { 35 | c := newTestClient() 36 | ret, err := c.Heartbeat() 37 | if err != nil { 38 | t.Error(err) 39 | return 40 | } 41 | t.Logf("%#v", ret) 42 | } 43 | -------------------------------------------------------------------------------- /hbdmswap/hbdmswap_trade.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // Order 合约下单 9 | /* 10 | contract_code: BTC-USD 11 | direction: "buy":买 "sell":卖 12 | offset string true "open":开 "close":平 13 | orderPriceType: 订单报价类型 "limit":限价 "opponent":对手价 "post_only":只做maker单,post only下单只受用户持仓数量限制,optimal_5:最优5档、optimal_10:最优10档、optimal_20:最优20档,"fok":FOK订单,"ioc":IOC订单, opponent_ioc": 对手价-IOC下单,"optimal_5_ioc":最优5档-IOC下单,"optimal_10_ioc":最优10档-IOC下单,"optimal_20_ioc":最优20档-IOC下单,"opponent_fok": 对手价-FOK下单,"optimal_5_fok":最优5档-FOK下单,"optimal_10_fok":最优10档-FOK下单,"optimal_20_fok":最优20档-FOK下单 14 | */ 15 | func (c *Client) Order(contractCode string, clientOrderID int64, price float64, 16 | volume float64, direction string, offset string, leverRate int, orderPriceType string) (result OrderResult, err error) { 17 | path := "/swap-api/v1/swap_order" 18 | params := &url.Values{} 19 | params.Add("contract_code", contractCode) 20 | if clientOrderID > 0 { 21 | params.Add("client_order_id", fmt.Sprint(clientOrderID)) 22 | } 23 | if price > 0 { 24 | params.Add("price", fmt.Sprint(price)) 25 | } 26 | params.Add("volume", fmt.Sprint(volume)) 27 | params.Add("direction", direction) 28 | params.Add("offset", offset) 29 | params.Add("lever_rate", fmt.Sprint(leverRate)) 30 | params.Add("order_price_type", orderPriceType) 31 | _, err = c.doPost(path, params, &result) 32 | return 33 | } 34 | 35 | /* 36 | * Cancel 撤销订单 37 | */ 38 | func (c *Client) Cancel(contractCode string, orderID int64, clientOrderID int64) (result CancelResult, err error) { 39 | path := "/swap-api/v1/swap_cancel" 40 | params := &url.Values{} 41 | params.Add("contract_code", contractCode) 42 | if orderID > 0 { 43 | params.Add("order_id", fmt.Sprint(orderID)) 44 | } 45 | if clientOrderID > 0 { 46 | params.Add("client_order_id", fmt.Sprint(clientOrderID)) 47 | } 48 | _, err = c.doPost(path, params, &result) 49 | return 50 | } 51 | 52 | /* 53 | * OrderInfo 获取合约订单信息 54 | */ 55 | func (c *Client) OrderInfo(contractCode string, orderID int64, clientOrderID int64) (result OrderInfoResult, err error) { 56 | path := "/swap-api/v1/swap_order_info" 57 | params := &url.Values{} 58 | params.Add("contract_code", contractCode) 59 | if orderID > 0 { 60 | params.Add("order_id", fmt.Sprint(orderID)) 61 | } 62 | if clientOrderID > 0 { 63 | params.Add("client_order_id", fmt.Sprint(clientOrderID)) 64 | } 65 | _, err = c.doPost(path, params, &result) 66 | return 67 | } 68 | 69 | // GetOpenOrders 获取合约当前未成交委托单 70 | // page_index: 1,2,3... 71 | func (c *Client) GetOpenOrders(contractCode string, pageIndex int, pageSize int) (result OpenOrdersResult, err error) { 72 | path := "/swap-api/v1/swap_openorders" 73 | params := &url.Values{} 74 | params.Add("contract_code", contractCode) 75 | if pageIndex > 0 { 76 | params.Add("page_index", fmt.Sprint(pageIndex)) 77 | } 78 | if pageSize > 0 { 79 | params.Add("page_size", fmt.Sprint(pageSize)) 80 | } 81 | _, err = c.doPost(path, params, &result) 82 | return 83 | } 84 | 85 | // GetHisOrders 获取合约历史委托 86 | func (c *Client) GetHisOrders(contractCode string, tradeType int, _type int, status int, createDate int, 87 | pageIndex int, pageSize int) (result HisOrdersResult, err error) { 88 | path := "/swap-api/v1/swap_hisorders" 89 | params := &url.Values{} 90 | params.Add("contract_code", contractCode) 91 | params.Add("trade_type", fmt.Sprint(tradeType)) 92 | params.Add("type", fmt.Sprint(_type)) 93 | params.Add("status", fmt.Sprint(status)) 94 | params.Add("create_date", fmt.Sprint(createDate)) 95 | if pageIndex > 0 { 96 | params.Add("page_index", fmt.Sprint(pageIndex)) 97 | } 98 | if pageSize > 0 { 99 | params.Add("page_size", fmt.Sprint(pageSize)) 100 | } 101 | _, err = c.doPost(path, params, &result) 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /hbdmswap/hbdmswap_trade_test.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import "testing" 4 | 5 | func TestClient_Order(t *testing.T) { 6 | c := newTestClient() 7 | orderResult, err := c.Order("BTC-USD", 8 | 0, 9 | 3000, 10 | 1, 11 | "buy", 12 | "open", 13 | 125, 14 | "limit") 15 | if err != nil { 16 | t.Error(err) 17 | return 18 | } 19 | t.Logf("%#v", orderResult) 20 | } 21 | 22 | func TestClient_Cancel(t *testing.T) { 23 | c := newTestClient() 24 | r, err := c.Cancel( 25 | "BTC-USD", 26 | 696382271304871936, 27 | 0, 28 | ) 29 | if err != nil { 30 | t.Error(err) 31 | return 32 | } 33 | t.Logf("%#v", r) 34 | } 35 | 36 | func TestClient_OrderInfo(t *testing.T) { 37 | c := newTestClient() 38 | info, err := c.OrderInfo( 39 | "BTC-USD", 40 | 696382271304871936, 41 | 0, 42 | ) 43 | if err != nil { 44 | t.Error(err) 45 | return 46 | } 47 | t.Logf("%#v", info) 48 | } 49 | 50 | func TestClient_GetOpenOrders(t *testing.T) { 51 | c := newTestClient() 52 | orders, err := c.GetOpenOrders( 53 | "BTC-USD", 54 | 1, 55 | 0, 56 | ) 57 | if err != nil { 58 | t.Error(err) 59 | return 60 | } 61 | t.Logf("%#v", orders) 62 | } 63 | 64 | func TestClient_GetHisOrders(t *testing.T) { 65 | c := newTestClient() 66 | orders, err := c.GetHisOrders( 67 | "BTC-USD", 68 | 0, 69 | 1, 70 | 0, 71 | 90, 72 | 1, 73 | 0, 74 | ) 75 | if err != nil { 76 | t.Error(err) 77 | return 78 | } 79 | 80 | for _, v := range orders.Data.Orders { 81 | t.Logf("%#v", v) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /hbdmswap/models.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import ( 4 | sjson "encoding/json" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | type Heartbeat struct { 10 | Heartbeat int `json:"heartbeat"` 11 | EstimatedRecoveryTime interface{} `json:"estimated_recovery_time"` 12 | SwapHeartbeat int `json:"swap_heartbeat"` 13 | SwapEstimatedRecoveryTime interface{} `json:"swap_estimated_recovery_time"` 14 | } 15 | 16 | type HeartbeatResult struct { 17 | Status string `json:"status"` 18 | Data Heartbeat `json:"data"` 19 | Ts int64 `json:"ts"` 20 | } 21 | 22 | type AccountInfo struct { 23 | Symbol string `json:"symbol"` 24 | MarginBalance float64 `json:"margin_balance"` 25 | MarginPosition float64 `json:"margin_position"` 26 | MarginFrozen float64 `json:"margin_frozen"` 27 | MarginAvailable float64 `json:"margin_available"` 28 | ProfitReal float64 `json:"profit_real"` 29 | ProfitUnreal float64 `json:"profit_unreal"` 30 | RiskRate float64 `json:"risk_rate"` //*interface{} 31 | WithdrawAvailable float64 `json:"withdraw_available"` 32 | LiquidationPrice float64 `json:"liquidation_price"` //*interface{} 33 | LeverRate float64 `json:"lever_rate"` 34 | AdjustFactor float64 `json:"adjust_factor"` 35 | MarginStatic float64 `json:"margin_static"` 36 | ContractCode string `json:"contract_code"` 37 | } 38 | 39 | type AccountInfoResult struct { 40 | Status string `json:"status"` // "ok" , "error" 41 | ErrCode int `json:"err_code"` 42 | ErrMsg string `json:"err_msg"` 43 | Data []AccountInfo `json:"data"` 44 | Ts int64 `json:"ts"` 45 | } 46 | 47 | type Position struct { 48 | Symbol string `json:"symbol"` 49 | ContractCode string `json:"contract_code"` 50 | Volume float64 `json:"volume"` 51 | Available float64 `json:"available"` 52 | Frozen float64 `json:"frozen"` 53 | CostOpen float64 `json:"cost_open"` 54 | CostHold float64 `json:"cost_hold"` 55 | ProfitUnreal float64 `json:"profit_unreal"` 56 | ProfitRate float64 `json:"profit_rate"` 57 | Profit float64 `json:"profit"` 58 | PositionMargin float64 `json:"position_margin"` 59 | LeverRate int `json:"lever_rate"` 60 | Direction string `json:"direction"` // "buy":买 "sell":卖 61 | LastPrice float64 `json:"last_price"` 62 | } 63 | 64 | type PositionInfoResult struct { 65 | Status string `json:"status"` 66 | ErrCode int `json:"err_code"` 67 | ErrMsg string `json:"err_msg"` 68 | Data []Position `json:"data"` 69 | Ts int64 `json:"ts"` 70 | } 71 | 72 | type Tick struct { 73 | Asks [][]float64 `json:"asks"` 74 | Bids [][]float64 `json:"bids"` 75 | Ch string `json:"ch"` 76 | ID int64 `json:"id"` 77 | MrID int64 `json:"mrid"` 78 | Ts int64 `json:"ts"` 79 | Version int64 `json:"version"` 80 | } 81 | 82 | type MarketDepthResult struct { 83 | Ch string `json:"ch"` 84 | Status string `json:"status"` 85 | ErrCode int `json:"err_code"` 86 | ErrMsg string `json:"err_msg"` 87 | Tick Tick `json:"tick"` 88 | Ts int64 `json:"ts"` 89 | } 90 | 91 | type KLine struct { 92 | Amount float64 `json:"amount"` 93 | Close float64 `json:"close"` 94 | Count int `json:"count"` 95 | High float64 `json:"high"` 96 | ID int `json:"id"` 97 | Low float64 `json:"low"` 98 | Open float64 `json:"open"` 99 | Vol int `json:"vol"` 100 | } 101 | 102 | type KLineResult struct { 103 | Ch string `json:"ch"` 104 | Data []KLine `json:"data"` 105 | Status string `json:"status"` 106 | ErrCode int `json:"err_code"` 107 | ErrMsg string `json:"err_msg"` 108 | Ts int64 `json:"ts"` 109 | } 110 | 111 | type OrderData struct { 112 | OrderID int64 `json:"order_id"` 113 | OrderIDStr string `json:"order_id_str"` 114 | ClientOrderID int64 `json:"client_order_id"` 115 | } 116 | 117 | type OrderResult struct { 118 | Status string `json:"status"` 119 | ErrCode int `json:"err_code"` 120 | ErrMsg string `json:"err_msg"` 121 | Data OrderData `json:"data"` 122 | Ts int64 `json:"ts"` 123 | } 124 | 125 | type CancelError struct { 126 | OrderID string `json:"order_id"` 127 | ErrCode int `json:"err_code"` 128 | ErrMsg string `json:"err_msg"` 129 | } 130 | 131 | type CancelData struct { 132 | Errors []CancelError `json:"errors"` 133 | Successes string `json:"successes"` 134 | } 135 | 136 | type CancelResult struct { 137 | Status string `json:"status"` 138 | ErrCode int `json:"err_code"` 139 | ErrMsg string `json:"err_msg"` 140 | Data CancelData `json:"data"` 141 | Ts int64 `json:"ts"` 142 | } 143 | 144 | type Order struct { 145 | Symbol string `json:"symbol"` 146 | ContractCode string `json:"contract_code"` 147 | Volume float64 `json:"volume"` 148 | Price float64 `json:"price"` 149 | OrderPriceTypeRaw sjson.RawMessage `json:"order_price_type"` // 1限价单,3对手价,4闪电平仓,5计划委托,6post_only 150 | OrderType int `json:"order_type"` 151 | Direction string `json:"direction"` 152 | Offset string `json:"offset"` 153 | LeverRate int `json:"lever_rate"` 154 | OrderID int64 `json:"order_id"` 155 | ClientOrderID string `json:"client_order_id"` 156 | CreatedAt int64 `json:"created_at"` 157 | TradeVolume float64 `json:"trade_volume"` 158 | TradeTurnover float64 `json:"trade_turnover"` 159 | Fee float64 `json:"fee"` 160 | TradeAvgPrice float64 `json:"trade_avg_price"` 161 | MarginFrozen float64 `json:"margin_frozen"` 162 | Profit float64 `json:"profit"` 163 | Status int `json:"status"` 164 | OrderSource string `json:"order_source"` 165 | OrderIDStr string `json:"order_id_str"` 166 | FeeAsset string `json:"fee_asset"` 167 | } 168 | 169 | func (o *Order) OrderPriceType() string { 170 | // 1:限价单,3:对手价,4:闪电平仓,5:计划委托,6:post_only 171 | d, err := o.OrderPriceTypeRaw.MarshalJSON() 172 | if err != nil { 173 | return "" 174 | } 175 | s := string(d) 176 | i, err := strconv.ParseInt(s, 10, 64) 177 | if err == nil { 178 | switch i { 179 | case 1: 180 | return "limit" 181 | case 3: 182 | return "opponent" 183 | case 4: 184 | return "lightning" 185 | case 5: 186 | return "trigger_order" 187 | case 6: 188 | return "post_only" 189 | default: 190 | return fmt.Sprint(i) 191 | } 192 | } 193 | return s 194 | } 195 | 196 | type OrderInfoResult struct { 197 | Status string `json:"status"` 198 | ErrCode int `json:"err_code"` 199 | ErrMsg string `json:"err_msg"` 200 | Data []Order `json:"data"` 201 | Ts int64 `json:"ts"` 202 | } 203 | 204 | type OpenOrdersData struct { 205 | Orders []Order `json:"orders"` 206 | TotalPage int `json:"total_page"` 207 | CurrentPage int `json:"current_page"` 208 | TotalSize int `json:"total_size"` 209 | } 210 | 211 | type OpenOrdersResult struct { 212 | Status string `json:"status"` 213 | ErrCode int `json:"err_code"` 214 | ErrMsg string `json:"err_msg"` 215 | Data OpenOrdersData `json:"data"` 216 | Ts int64 `json:"ts"` 217 | } 218 | 219 | type HisOrdersData struct { 220 | Orders []Order `json:"orders"` 221 | TotalPage int `json:"total_page"` 222 | CurrentPage int `json:"current_page"` 223 | TotalSize int `json:"total_size"` 224 | } 225 | 226 | type HisOrdersResult struct { 227 | Status string `json:"status"` 228 | ErrCode int `json:"err_code"` 229 | ErrMsg string `json:"err_msg"` 230 | Data HisOrdersData `json:"data"` 231 | Ts int64 `json:"ts"` 232 | } 233 | 234 | type WSTickerTick struct { 235 | ID int64 `json:"id"` 236 | MrID int64 `json:"mrid"` 237 | Open float64 `json:"open"` 238 | Close float64 `json:"close"` 239 | High float64 `json:"high"` 240 | Low float64 `json:"low"` 241 | Amount float64 `json:"amount"` 242 | Vol float64 `json:"vol"` 243 | Count int64 `json:"count"` 244 | } 245 | 246 | type WSTicker struct { 247 | Ch string `json:"ch"` 248 | Ts int64 `json:"ts"` 249 | Tick WSTickerTick `json:"tick"` 250 | } 251 | 252 | type WSTick struct { 253 | MrID int64 `json:"mrid"` 254 | ID int `json:"id"` 255 | Bids [][]float64 `json:"bids"` 256 | Asks [][]float64 `json:"asks"` 257 | Ts int64 `json:"ts"` 258 | Version int `json:"version"` 259 | Ch string `json:"ch"` 260 | } 261 | 262 | type WSDepth struct { 263 | Ch string `json:"ch"` 264 | Ts int64 `json:"ts"` 265 | Tick Tick `json:"tick"` 266 | } 267 | 268 | type WSTickHF struct { 269 | Asks [][]float64 `json:"asks"` 270 | Bids [][]float64 `json:"bids"` 271 | Ch string `json:"ch"` 272 | Event string `json:"event"` 273 | ID int64 `json:"id"` 274 | Mrid int64 `json:"mrid"` 275 | Ts int64 `json:"ts"` 276 | Version int `json:"version"` 277 | } 278 | 279 | type WSDepthHF struct { 280 | Ch string `json:"ch"` 281 | Tick WSTickHF `json:"tick"` 282 | Ts int64 `json:"ts"` 283 | } 284 | 285 | type WSTradeItem struct { 286 | Amount int `json:"amount"` 287 | Ts int64 `json:"ts"` 288 | ID int64 `json:"id"` 289 | Price float64 `json:"price"` 290 | Direction string `json:"direction"` 291 | } 292 | 293 | type WSTradeTick struct { 294 | ID int64 `json:"id"` 295 | Ts int64 `json:"ts"` 296 | Data []WSTradeItem `json:"data"` 297 | } 298 | 299 | type WSTrade struct { 300 | Ch string `json:"ch"` 301 | Ts int64 `json:"ts"` 302 | Tick WSTradeTick `json:"tick"` 303 | } 304 | 305 | type WSMyTrade struct { 306 | TradeID int64 `json:"trade_id"` 307 | ID string `json:"id"` 308 | TradeVolume float64 `json:"trade_volume"` 309 | TradePrice float64 `json:"trade_price"` 310 | TradeFee float64 `json:"trade_fee"` 311 | FeeAsset string `json:"fee_asset"` 312 | TradeTurnover float64 `json:"trade_turnover"` 313 | CreatedAt int64 `json:"created_at"` 314 | Role string `json:"role"` 315 | } 316 | 317 | type WSOrder struct { 318 | Op string `json:"op"` 319 | Topic string `json:"topic"` 320 | Ts int64 `json:"ts"` 321 | Symbol string `json:"symbol"` 322 | ContractCode string `json:"contract_code"` 323 | Volume float64 `json:"volume"` 324 | Price float64 `json:"price"` 325 | OrderPriceType string `json:"order_price_type"` 326 | Direction string `json:"direction"` 327 | Offset string `json:"offset"` 328 | Status int `json:"status"` 329 | LeverRate float64 `json:"lever_rate"` 330 | OrderID int64 `json:"order_id"` 331 | OrderIDStr string `json:"order_id_str"` 332 | ClientOrderID int64 `json:"client_order_id"` 333 | OrderSource string `json:"order_source"` 334 | OrderType int `json:"order_type"` 335 | CreatedAt int64 `json:"created_at"` 336 | TradeVolume float64 `json:"trade_volume"` 337 | TradeTurnover float64 `json:"trade_turnover"` 338 | Fee float64 `json:"fee"` 339 | TradeAvgPrice float64 `json:"trade_avg_price"` 340 | MarginFrozen float64 `json:"margin_frozen"` 341 | Profit float64 `json:"profit"` 342 | Trade []WSMyTrade `json:"trade"` 343 | } 344 | 345 | type WSAccountData struct { 346 | Symbol string `json:"symbol"` 347 | ContractCode string `json:"contract_code"` 348 | MarginBalance float64 `json:"margin_balance"` 349 | MarginStatic float64 `json:"margin_static"` 350 | MarginPosition float64 `json:"margin_position"` 351 | MarginFrozen float64 `json:"margin_frozen"` 352 | MarginAvailable float64 `json:"margin_available"` 353 | ProfitReal float64 `json:"profit_real"` 354 | ProfitUnreal float64 `json:"profit_unreal"` 355 | WithdrawAvailable float64 `json:"withdraw_available"` 356 | RiskRate float64 `json:"risk_rate"` 357 | LiquidationPrice float64 `json:"liquidation_price"` 358 | LeverRate float64 `json:"lever_rate"` 359 | AdjustFactor float64 `json:"adjust_factor"` 360 | } 361 | 362 | type WSAccounts struct { 363 | Op string `json:"op"` 364 | Topic string `json:"topic"` 365 | Ts int64 `json:"ts"` 366 | Event string `json:"event"` 367 | Data []WSAccountData `json:"data"` 368 | } 369 | 370 | type WSPositionData struct { 371 | Symbol string `json:"symbol"` 372 | ContractCode string `json:"contract_code"` 373 | Volume float64 `json:"volume"` 374 | Available float64 `json:"available"` 375 | Frozen float64 `json:"frozen"` 376 | CostOpen float64 `json:"cost_open"` 377 | CostHold float64 `json:"cost_hold"` 378 | ProfitUnreal float64 `json:"profit_unreal"` 379 | ProfitRate float64 `json:"profit_rate"` 380 | Profit float64 `json:"profit"` 381 | PositionMargin float64 `json:"position_margin"` 382 | LeverRate float64 `json:"lever_rate"` 383 | Direction string `json:"direction"` 384 | LastPrice float64 `json:"last_price"` 385 | } 386 | 387 | type WSPositions struct { 388 | Op string `json:"op"` 389 | Topic string `json:"topic"` 390 | Ts int64 `json:"ts"` 391 | Event string `json:"event"` 392 | Data []WSPositionData `json:"data"` 393 | } 394 | 395 | type WSLiquidationOrderData struct { 396 | Symbol string `json:"symbol"` 397 | ContractCode string `json:"contract_code"` 398 | Direction string `json:"direction"` 399 | Offset string `json:"offset"` 400 | Volume float64 `json:"volume"` 401 | Price float64 `json:"price"` 402 | CreatedAt int64 `json:"created_at"` 403 | } 404 | 405 | type WSLiquidationOrders struct { 406 | Op string `json:"op"` 407 | Topic string `json:"topic"` 408 | Ts int64 `json:"ts"` 409 | Data []WSLiquidationOrderData `json:"data"` 410 | } 411 | -------------------------------------------------------------------------------- /hbdmswap/nws.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | // 接口文档 4 | // https://docs.huobigroup.com/docs/coin_margined_swap/v1/cn/#websocket 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "github.com/frankrap/huobi-api/utils" 10 | "github.com/lithammer/shortuuid/v3" 11 | "github.com/recws-org/recws" 12 | "github.com/tidwall/gjson" 13 | "log" 14 | "net/http" 15 | "net/url" 16 | "strings" 17 | "sync" 18 | "time" 19 | ) 20 | 21 | // NWS WebSocket 订单和用户数据接口 22 | type NWS struct { 23 | sync.RWMutex 24 | 25 | ctx context.Context 26 | cancel context.CancelFunc 27 | conn recws.RecConn 28 | 29 | wsURL string 30 | cid string // 客户端请求唯一ID 31 | accessKey string 32 | secretKey string 33 | subscriptions map[string]interface{} 34 | 35 | ordersCallback func(order *WSOrder) 36 | accountsCallback func(accounts *WSAccounts) 37 | positionsCallback func(positions *WSPositions) 38 | liquidationOrdersCallback func(liquidationOrders *WSLiquidationOrders) 39 | } 40 | 41 | // SetProxy 设置代理地址 42 | // porxyURL: 43 | // socks5://127.0.0.1:1080 44 | // https://127.0.0.1:1080 45 | func (ws *NWS) SetProxy(proxyURL string) (err error) { 46 | var purl *url.URL 47 | purl, err = url.Parse(proxyURL) 48 | if err != nil { 49 | return 50 | } 51 | log.Printf("[ws][%s] proxy url:%s", proxyURL, purl) 52 | ws.conn.Proxy = http.ProxyURL(purl) 53 | return 54 | } 55 | 56 | // SubscribeOrders 订阅订单成交数据 57 | // symbol: BTC-USD 58 | func (ws *NWS) SubscribeOrders(id string, symbol string) { 59 | ws.SubscribeTopic(id, 60 | fmt.Sprintf("orders.%s", symbol)) 61 | } 62 | 63 | // SubscribeAccounts 订阅资产变动数据 64 | // symbol: BTC-USD 65 | func (ws *NWS) SubscribeAccounts(id string, symbol string) { 66 | ws.SubscribeTopic(id, 67 | fmt.Sprintf("accounts.%s", symbol)) 68 | } 69 | 70 | // SubscribePositions 订阅持仓变动更新数据 71 | // symbol: BTC-USD 72 | func (ws *NWS) SubscribePositions(id string, symbol string) { 73 | ws.SubscribeTopic(id, 74 | fmt.Sprintf("positions.%s", strings.ToLower(symbol))) 75 | } 76 | 77 | // SubscribeLiquidationOrders 订阅强平订单数据 78 | // symbol: BTC-USD 79 | func (ws *NWS) SubscribeLiquidationOrders(id string, symbol string) { 80 | ws.SubscribeTopic(id, 81 | fmt.Sprintf("liquidationOrders.%s", strings.ToLower(symbol))) 82 | } 83 | 84 | // SubscribeTopic 订阅 85 | func (ws *NWS) SubscribeTopic(id string, topic string) { 86 | ch := map[string]interface{}{ 87 | "op": "sub", 88 | "cid": ws.cid, 89 | "topic": topic} 90 | err := ws.Subscribe(id, ch) 91 | if err != nil { 92 | log.Printf("%v", err) 93 | } 94 | } 95 | 96 | // Subscribe 订阅 97 | func (ws *NWS) Subscribe(id string, ch map[string]interface{}) error { 98 | ws.Lock() 99 | defer ws.Unlock() 100 | 101 | ws.subscriptions[id] = ch 102 | return ws.sendWSMessage(ch) 103 | } 104 | 105 | // Login 授权接口 106 | func (ws *NWS) Login() error { 107 | log.Printf("Login") 108 | if ws.accessKey == "" || ws.secretKey == "" { 109 | return fmt.Errorf("missing accessKey or secretKey") 110 | } 111 | opReq := map[string]string{ 112 | "op": "auth", 113 | "type": "api", 114 | } 115 | err := ws.setSignatureData(opReq, ws.accessKey, ws.secretKey) 116 | if err != nil { 117 | return err 118 | } 119 | log.Printf("opReq: %#v", opReq) 120 | return ws.sendWSMessage(opReq) 121 | } 122 | 123 | func (ws *NWS) setSignatureData(data map[string]string, apiKey, apiSecretKey string) error { 124 | data["AccessKeyId"] = apiKey 125 | data["SignatureMethod"] = "HmacSHA256" 126 | data["SignatureVersion"] = "2" 127 | data["Timestamp"] = time.Now().UTC().Format("2006-01-02T15:04:05") 128 | postForm := url.Values{} 129 | // 当type为api时,参数op,type,cid,Signature不参加签名计算 130 | isApi := data["type"] == "api" 131 | for k, v := range data { 132 | if isApi && (k == "op" || k == "cid" || k == "type") { 133 | continue 134 | } 135 | postForm.Set(k, v) 136 | } 137 | u, err := url.Parse(ws.wsURL) 138 | if err != nil { 139 | return err 140 | } 141 | payload := fmt.Sprintf("%s\n%s\n%s\n%s", "GET", u.Host, u.Path, postForm.Encode()) 142 | sign, _ := utils.GetParamHmacSHA256Base64Sign(apiSecretKey, payload) 143 | data["Signature"] = sign 144 | return nil 145 | } 146 | 147 | func (ws *NWS) SetOrdersCallback(callback func(order *WSOrder)) { 148 | ws.ordersCallback = callback 149 | } 150 | 151 | func (ws *NWS) SetAccountsCallback(callback func(accounts *WSAccounts)) { 152 | ws.accountsCallback = callback 153 | } 154 | 155 | func (ws *NWS) SetPositionsCallback(callback func(positions *WSPositions)) { 156 | ws.positionsCallback = callback 157 | } 158 | 159 | func (ws *NWS) SetLiquidationOrdersCallback(callback func(liquidationOrders *WSLiquidationOrders)) { 160 | ws.liquidationOrdersCallback = callback 161 | } 162 | 163 | // Unsubscribe 取消订阅 164 | func (ws *NWS) Unsubscribe(id string) error { 165 | ws.Lock() 166 | defer ws.Unlock() 167 | 168 | if v, ok := ws.subscriptions[id]; ok { 169 | ch, ok := v.(map[string]interface{}) 170 | if ok { 171 | ch["op"] = "unsub" 172 | log.Printf("取消订阅: %#v", ch) 173 | ws.sendWSMessage(ch) 174 | } 175 | delete(ws.subscriptions, id) 176 | } 177 | return nil 178 | } 179 | 180 | func (ws *NWS) unsubscribe(symbol string) { 181 | ch := map[string]interface{}{ 182 | "op": "unsub", 183 | "cid": ws.cid, 184 | "topic": fmt.Sprintf("matchOrders.%s", strings.ToLower(symbol))} 185 | ws.sendWSMessage(ch) 186 | } 187 | 188 | func (ws *NWS) subscribeHandler() error { 189 | //log.Printf("subscribeHandler") 190 | ws.Lock() 191 | defer ws.Unlock() 192 | 193 | // 授权 194 | if ws.accessKey != "" && ws.secretKey != "" { 195 | err := ws.Login() 196 | if err != nil { 197 | log.Printf("%v", err) 198 | return err 199 | } 200 | //time.Sleep(1*time.Second) 201 | } 202 | 203 | for _, v := range ws.subscriptions { 204 | log.Printf("sub: %#v", v) 205 | err := ws.sendWSMessage(v) 206 | if err != nil { 207 | log.Printf("%v", err) 208 | } 209 | } 210 | return nil 211 | } 212 | 213 | func (ws *NWS) sendWSMessage(msg interface{}) error { 214 | return ws.conn.WriteJSON(msg) 215 | } 216 | 217 | func (ws *NWS) Start() { 218 | ws.conn.Dial(ws.wsURL, nil) 219 | go ws.run() 220 | } 221 | 222 | func (ws *NWS) run() { 223 | ctx := context.Background() 224 | for { 225 | select { 226 | case <-ctx.Done(): 227 | go ws.conn.Close() 228 | log.Printf("Websocket closed %s", ws.conn.GetURL()) 229 | return 230 | default: 231 | messageType, msg, err := ws.conn.ReadMessage() 232 | if err != nil { 233 | log.Printf("Read error: %v", err) 234 | time.Sleep(100 * time.Millisecond) 235 | continue 236 | } 237 | 238 | msg, err = utils.GzipUncompress(msg) 239 | if err != nil { 240 | continue 241 | } 242 | 243 | ws.handleMsg(messageType, msg) 244 | } 245 | } 246 | } 247 | 248 | func (ws *NWS) handleMsg(messageType int, msg []byte) { 249 | ret := gjson.ParseBytes(msg) 250 | 251 | if opValue := ret.Get("op"); opValue.Exists() { 252 | op := opValue.String() 253 | if op == "ping" { 254 | ts := ret.Get("ts").Int() 255 | ws.handlePing(ts) 256 | return 257 | } else if op == "notify" { 258 | topicValue := ret.Get("topic") 259 | if !topicValue.Exists() { 260 | log.Printf("err") 261 | return 262 | } 263 | topic := topicValue.String() 264 | ws.handleNotify(topic, msg...) 265 | } 266 | } 267 | 268 | //log.Printf("%v", string(msg)) 269 | } 270 | 271 | func (ws *NWS) handleNotify(topic string, msg ...byte) { 272 | if strings.HasPrefix(topic, "orders.") { 273 | var value WSOrder 274 | err := json.Unmarshal(msg, &value) 275 | if err != nil { 276 | log.Printf("%v", err) 277 | return 278 | } 279 | 280 | if ws.ordersCallback != nil { 281 | ws.ordersCallback(&value) 282 | } 283 | return 284 | //} else if strings.HasPrefix(topic, "matchOrders.") { 285 | // var value WSUserMatchOrder 286 | // err := json.Unmarshal(msg, &value) 287 | // if err != nil { 288 | // log.Printf("%v", err) 289 | // return 290 | // } 291 | // 292 | // if ws.matchOrdersCallback != nil { 293 | // ws.matchOrdersCallback(&value) 294 | // } 295 | // return 296 | } else if strings.HasPrefix(topic, "accounts.") { 297 | var value WSAccounts 298 | err := json.Unmarshal(msg, &value) 299 | if err != nil { 300 | log.Printf("%v", err) 301 | return 302 | } 303 | 304 | if ws.accountsCallback != nil { 305 | ws.accountsCallback(&value) 306 | } 307 | return 308 | } else if strings.HasPrefix(topic, "positions.") { 309 | var value WSPositions 310 | err := json.Unmarshal(msg, &value) 311 | if err != nil { 312 | log.Printf("%v", err) 313 | return 314 | } 315 | 316 | if ws.positionsCallback != nil { 317 | ws.positionsCallback(&value) 318 | } 319 | return 320 | } else if strings.HasPrefix(topic, "liquidationOrders.") { 321 | var value WSLiquidationOrders 322 | err := json.Unmarshal(msg, &value) 323 | if err != nil { 324 | log.Printf("%v", err) 325 | return 326 | } 327 | 328 | if ws.liquidationOrdersCallback != nil { 329 | ws.liquidationOrdersCallback(&value) 330 | } 331 | return 332 | } 333 | } 334 | 335 | func (ws *NWS) handlePing(ts int64) { 336 | pong := struct { 337 | Op string `json:"op"` 338 | Ts int64 `json:"ts"` 339 | }{"pong", ts} 340 | 341 | err := ws.sendWSMessage(pong) 342 | if err != nil { 343 | log.Printf("Send pong error: %v", err) 344 | } 345 | } 346 | 347 | // NewNWS 创建 NWS 348 | // wsURL: 349 | // 正式地址 wss://api.hbdm.com/swap-notification 350 | // 开发地址 wss://api.btcgateway.pro/swap-notification 351 | func NewNWS(wsURL string, accessKey string, secretKey string) *NWS { 352 | ws := &NWS{ 353 | wsURL: wsURL, 354 | cid: shortuuid.New(), 355 | accessKey: accessKey, 356 | secretKey: secretKey, 357 | subscriptions: make(map[string]interface{}), 358 | } 359 | ws.ctx, ws.cancel = context.WithCancel(context.Background()) 360 | ws.conn = recws.RecConn{ 361 | KeepAliveTimeout: 10 * time.Second, 362 | } 363 | ws.conn.SubscribeHandler = ws.subscribeHandler 364 | return ws 365 | } 366 | -------------------------------------------------------------------------------- /hbdmswap/nws_test.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func newNWSTest() *NWS { 10 | viper.SetConfigName("test_config") 11 | viper.AddConfigPath("..") 12 | err := viper.ReadInConfig() 13 | if err != nil { 14 | log.Panic(err) 15 | } 16 | 17 | accessKey := viper.GetString("access_key") 18 | secretKey := viper.GetString("secret_key") 19 | 20 | wsURL := "wss://api.btcgateway.pro/swap-notification" 21 | ws := NewNWS(wsURL, accessKey, secretKey) 22 | return ws 23 | } 24 | 25 | func TestNWS_SubscribeOrders(t *testing.T) { 26 | ws := newNWSTest() 27 | 28 | // 设置回调 29 | ws.SetOrdersCallback(func(order *WSOrder) { 30 | log.Printf("order: %#v", order) 31 | }) 32 | 33 | // 订阅 34 | ws.SubscribeOrders("orders_1", "BTC-USD") 35 | // 取消订阅 36 | //ws.Unsubscribe("orders_1") 37 | 38 | //go func() { 39 | // time.Sleep(30*time.Second) 40 | // ws.Unsubscribe("orders_1") 41 | //}() 42 | 43 | ws.Start() 44 | 45 | select {} 46 | } 47 | -------------------------------------------------------------------------------- /hbdmswap/ws.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | // 接口文档 4 | // https://huobiapi.github.io/docs/coin_margined_swap/v1/cn/#websocket 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "github.com/frankrap/huobi-api/utils" 10 | "github.com/recws-org/recws" 11 | "github.com/tidwall/gjson" 12 | "log" 13 | "net/http" 14 | "net/url" 15 | "strings" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | // WS WebSocket 市场行情接口 21 | type WS struct { 22 | sync.RWMutex 23 | 24 | ctx context.Context 25 | cancel context.CancelFunc 26 | conn recws.RecConn 27 | 28 | wsURL string 29 | accessKey string 30 | secretKey string 31 | debugMode bool 32 | subscriptions map[string]interface{} 33 | 34 | tickerCallback func(trade *WSTicker) 35 | depthCallback func(depth *WSDepth) 36 | depthHFCallback func(depth *WSDepthHF) 37 | tradeCallback func(trade *WSTrade) 38 | } 39 | 40 | // SetProxy 设置代理地址 41 | // porxyURL: 42 | // socks5://127.0.0.1:1080 43 | // https://127.0.0.1:1080 44 | func (ws *WS) SetProxy(proxyURL string) (err error) { 45 | var purl *url.URL 46 | purl, err = url.Parse(proxyURL) 47 | if err != nil { 48 | return 49 | } 50 | log.Printf("[ws][%s] proxy url:%s", proxyURL, purl) 51 | ws.conn.Proxy = http.ProxyURL(purl) 52 | return 53 | } 54 | 55 | // SubscribeTicker 订阅 Market Ticker 数据 56 | // id: 订阅的编号 57 | // symbol: BTC_CQ 58 | func (ws *WS) SubscribeTicker(id string, symbol string) { 59 | ch := map[string]interface{}{ 60 | "id": id, 61 | "sub": fmt.Sprintf("market.%s.detail", symbol)} 62 | ws.Subscribe(id, ch) 63 | } 64 | 65 | // SubscribeDepth 订阅 Market Depth 数据 66 | // id: 订阅的编号 67 | // symbol: BTC_CQ 68 | func (ws *WS) SubscribeDepth(id string, symbol string) { 69 | ch := map[string]interface{}{ 70 | "id": id, 71 | "sub": fmt.Sprintf("market.%s.depth.step0", symbol)} 72 | ws.Subscribe(id, ch) 73 | } 74 | 75 | // SubscribeDepthHF 订阅增量深度 76 | // size: 20/150 档位数,20:表示20档不合并的深度,150:表示150档不合并的深度 77 | // dateType: 数据类型,不填默认为全量数据,"incremental":增量数据,"snapshot":全量数据 78 | func (ws *WS) SubscribeDepthHF(id string, symbol string, size int, dateType string) { 79 | ch := map[string]interface{}{ 80 | "id": id, 81 | "sub": fmt.Sprintf("market.%v.depth.size_%v.high_freq", symbol, size), 82 | "data_type": dateType, 83 | } 84 | ws.Subscribe(id, ch) 85 | } 86 | 87 | // SubscribeTrade 订阅 Market Trade 数据 88 | // id: 订阅的编号 89 | // symbol: BTC_CQ 90 | func (ws *WS) SubscribeTrade(id string, symbol string) { 91 | ch := map[string]interface{}{ 92 | "id": id, 93 | "sub": fmt.Sprintf("market.%s.trade.detail", symbol)} 94 | ws.Subscribe(id, ch) 95 | } 96 | 97 | // Subscribe 订阅 98 | func (ws *WS) Subscribe(id string, ch map[string]interface{}) error { 99 | ws.Lock() 100 | defer ws.Unlock() 101 | 102 | ws.subscriptions[id] = ch 103 | ws.sendWSMessage(ch) 104 | return nil 105 | } 106 | 107 | func (ws *WS) SetTickerCallback(callback func(ticker *WSTicker)) { 108 | ws.tickerCallback = callback 109 | } 110 | 111 | func (ws *WS) SetDepthCallback(callback func(depth *WSDepth)) { 112 | ws.depthCallback = callback 113 | } 114 | 115 | func (ws *WS) SetDepthHFCallback(callback func(depth *WSDepthHF)) { 116 | ws.depthHFCallback = callback 117 | } 118 | 119 | func (ws *WS) SetTradeCallback(callback func(trade *WSTrade)) { 120 | ws.tradeCallback = callback 121 | } 122 | 123 | func (ws *WS) Unsubscribe(id string) error { 124 | ws.Lock() 125 | defer ws.Unlock() 126 | 127 | if _, ok := ws.subscriptions[id]; ok { 128 | delete(ws.subscriptions, id) 129 | } 130 | return nil 131 | } 132 | 133 | func (ws *WS) subscribeHandler() error { 134 | log.Printf("subscribeHandler") 135 | ws.Lock() 136 | defer ws.Unlock() 137 | 138 | for _, v := range ws.subscriptions { 139 | ws.sendWSMessage(v) 140 | } 141 | return nil 142 | } 143 | 144 | func (ws *WS) sendWSMessage(msg interface{}) error { 145 | return ws.conn.WriteJSON(msg) 146 | } 147 | 148 | func (ws *WS) Start() { 149 | ws.conn.Dial(ws.wsURL, nil) 150 | go ws.run() 151 | } 152 | 153 | func (ws *WS) run() { 154 | ctx := context.Background() 155 | for { 156 | select { 157 | case <-ctx.Done(): 158 | go ws.conn.Close() 159 | log.Printf("Websocket closed %s", ws.conn.GetURL()) 160 | return 161 | default: 162 | messageType, msg, err := ws.conn.ReadMessage() 163 | if err != nil { 164 | log.Printf("Read error: %v", err) 165 | time.Sleep(100 * time.Millisecond) 166 | continue 167 | } 168 | 169 | msg, err = utils.GzipUncompress(msg) 170 | if err != nil { 171 | continue 172 | } 173 | 174 | ws.handleMsg(messageType, msg) 175 | } 176 | } 177 | } 178 | 179 | func (ws *WS) handleMsg(messageType int, msg []byte) { 180 | ret := gjson.ParseBytes(msg) 181 | 182 | if ws.debugMode { 183 | log.Printf("%v", string(msg)) 184 | } 185 | 186 | if pingValue := ret.Get("ping"); pingValue.Exists() { 187 | // 心跳 188 | ping := pingValue.Int() 189 | ws.handlePing(ping) 190 | return 191 | } 192 | 193 | // 订阅成功返回消息 194 | // {"id":"depth_1","subbed":"market.BTC_CQ.depth.step0","ts":1586498957314,"status":"ok"} 195 | 196 | if chValue := ret.Get("ch"); chValue.Exists() { 197 | // market.BTC_CQ.depth.step0 198 | ch := chValue.String() 199 | if strings.HasPrefix(ch, "market") { 200 | if strings.HasSuffix(ch, ".high_freq") { 201 | var depth WSDepthHF 202 | err := json.Unmarshal(msg, &depth) 203 | if err != nil { 204 | log.Printf("%v", err) 205 | return 206 | } 207 | 208 | if ws.depthHFCallback != nil { 209 | ws.depthHFCallback(&depth) 210 | } 211 | } else if strings.Contains(ch, ".depth") { 212 | var depth WSDepth 213 | err := json.Unmarshal(msg, &depth) 214 | if err != nil { 215 | log.Printf("%v", err) 216 | return 217 | } 218 | 219 | if ws.depthCallback != nil { 220 | ws.depthCallback(&depth) 221 | } 222 | } else if strings.HasSuffix(ch, ".trade.detail") { 223 | //log.Printf("%v", string(msg)) 224 | var trade WSTrade 225 | err := json.Unmarshal(msg, &trade) 226 | if err != nil { 227 | log.Printf("%v", err) 228 | return 229 | } 230 | 231 | if ws.tradeCallback != nil { 232 | ws.tradeCallback(&trade) 233 | } 234 | } else if strings.HasSuffix(ch, ".detail") { 235 | var ticker WSTicker 236 | err := json.Unmarshal(msg, &ticker) 237 | if err != nil { 238 | log.Printf("%v", err) 239 | return 240 | } 241 | 242 | if ws.tickerCallback != nil { 243 | ws.tickerCallback(&ticker) 244 | } 245 | } else { 246 | log.Printf("%v", string(msg)) 247 | } 248 | } 249 | return 250 | } 251 | } 252 | 253 | func (ws *WS) handlePing(ping int64) { 254 | pong := struct { 255 | Pong int64 `json:"pong"` 256 | }{ping} 257 | 258 | err := ws.sendWSMessage(pong) 259 | if err != nil { 260 | log.Printf("Send pong error: %v", err) 261 | } 262 | } 263 | 264 | // NewWS 创建 WS 265 | // wsURL: 266 | // 正式地址 wss://api.hbdm.com/swap-ws 267 | // 开发地址 wss://api.btcgateway.pro/swap-ws 268 | func NewWS(wsURL string, accessKey string, secretKey string, debugMode bool) *WS { 269 | ws := &WS{ 270 | wsURL: wsURL, 271 | accessKey: accessKey, 272 | secretKey: secretKey, 273 | debugMode: debugMode, 274 | subscriptions: make(map[string]interface{}), 275 | } 276 | ws.ctx, ws.cancel = context.WithCancel(context.Background()) 277 | ws.conn = recws.RecConn{ 278 | KeepAliveTimeout: 10 * time.Second, 279 | } 280 | ws.conn.SubscribeHandler = ws.subscribeHandler 281 | return ws 282 | } 283 | -------------------------------------------------------------------------------- /hbdmswap/ws_test.go: -------------------------------------------------------------------------------- 1 | package hbdmswap 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "log" 6 | "testing" 7 | ) 8 | 9 | func newWSTest() *WS { 10 | viper.SetConfigName("test_config") 11 | viper.AddConfigPath("..") 12 | err := viper.ReadInConfig() 13 | if err != nil { 14 | log.Panic(err) 15 | } 16 | 17 | accessKey := viper.GetString("access_key") 18 | secretKey := viper.GetString("secret_key") 19 | 20 | wsURL := "wss://api.btcgateway.pro/swap-ws" 21 | ws := NewWS(wsURL, accessKey, secretKey, true) 22 | return ws 23 | } 24 | 25 | func TestWS_SubscribeTicker(t *testing.T) { 26 | ws := newWSTest() 27 | ws.SetTickerCallback(func(ticker *WSTicker) { 28 | log.Printf("ticker: %#v", ticker) 29 | }) 30 | ws.SubscribeTicker("ticker_1", "BTC-USD") 31 | ws.Start() 32 | 33 | select {} 34 | } 35 | 36 | func TestWS_SubscribeDepth(t *testing.T) { 37 | ws := newWSTest() 38 | 39 | ws.SetDepthCallback(func(depth *WSDepth) { 40 | log.Printf("depth: %#v", depth) 41 | }) 42 | ws.SubscribeDepth("depth_1", "BTC-USD") 43 | ws.Start() 44 | 45 | select {} 46 | } 47 | 48 | func TestWS_SubscribeDepthHF(t *testing.T) { 49 | ws := newWSTest() 50 | 51 | ws.SetDepthHFCallback(func(depth *WSDepthHF) { 52 | log.Printf("depth: %#v", depth) 53 | }) 54 | ws.SubscribeDepthHF("depth_1", "BTC-USD", 20, "incremental") 55 | ws.Start() 56 | 57 | select {} 58 | } 59 | 60 | func TestWS_SubscribeTrade(t *testing.T) { 61 | ws := newWSTest() 62 | 63 | ws.SetTradeCallback(func(trade *WSTrade) { 64 | log.Printf("trade: %#v", trade) 65 | }) 66 | ws.SubscribeTrade("trade_1", "BTC-USD") 67 | ws.Start() 68 | 69 | select {} 70 | } 71 | -------------------------------------------------------------------------------- /spot/base.go: -------------------------------------------------------------------------------- 1 | package spot 2 | 3 | import "github.com/json-iterator/go" 4 | 5 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 6 | -------------------------------------------------------------------------------- /spot/models.go: -------------------------------------------------------------------------------- 1 | package spot 2 | 3 | type Tick struct { 4 | Bids [][]float64 `json:"bids"` // [8956.46,0.225618] 5 | Asks [][]float64 `json:"asks"` 6 | } 7 | 8 | type MarketDepthResult struct { 9 | Ch string `json:"ch"` // market.btcusdt.depth.step0 10 | Status string `json:"status"` // ok 11 | Ts int64 `json:"ts"` // 1590479896449 12 | Tick Tick `json:"tick"` 13 | } 14 | -------------------------------------------------------------------------------- /spot/spot.go: -------------------------------------------------------------------------------- 1 | package spot 2 | 3 | import ( 4 | "github.com/frankrap/huobi-api/utils" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | type ApiParameter struct { 11 | Debug bool 12 | AccessKey string 13 | SecretKey string 14 | EnablePrivateSign bool 15 | BaseURL string 16 | PrivateKeyPrime256 string 17 | HttpClient *http.Client 18 | ProxyURL string 19 | } 20 | 21 | type Client struct { 22 | params *ApiParameter 23 | httpClient *http.Client 24 | } 25 | 26 | func (c *Client) doGet(path string, params *url.Values, result interface{}) (resp []byte, err error) { 27 | url := c.params.BaseURL + path + "?" + params.Encode() 28 | resp, err = utils.HttpGet( 29 | c.httpClient, 30 | url, 31 | "", 32 | nil, 33 | ) 34 | if err != nil { 35 | return 36 | } 37 | 38 | if c.params.Debug { 39 | log.Println(string(resp)) 40 | } 41 | 42 | if result == nil { 43 | return 44 | } 45 | 46 | err = json.Unmarshal(resp, result) 47 | return 48 | } 49 | 50 | func NewClient(params *ApiParameter) *Client { 51 | httpClient := params.HttpClient 52 | if httpClient == nil { 53 | httpClient = utils.DefaultHttpClient("") 54 | } 55 | return &Client{ 56 | httpClient: httpClient, 57 | params: params, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /spot/spot_market.go: -------------------------------------------------------------------------------- 1 | package spot 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | ) 7 | 8 | // GetMarketDepth 市场深度数据 9 | // depth: 5,10,20 10 | // _type: 深度的价格聚合度,具体说明见下方 step0,step1,step2,step3,step4,step5 11 | func (c *Client) GetMarketDepth(symbol string, depth int, _type string) (result MarketDepthResult, err error) { 12 | if _type == "" { 13 | _type = "step0" 14 | } 15 | path := "/market/depth" 16 | params := &url.Values{} 17 | params.Add("symbol", symbol) 18 | params.Add("depth", strconv.Itoa(depth)) 19 | params.Add("type", _type) 20 | _, err = c.doGet(path, params, &result) 21 | return 22 | } 23 | -------------------------------------------------------------------------------- /spot/spot_market_test.go: -------------------------------------------------------------------------------- 1 | package spot 2 | 3 | import "testing" 4 | 5 | func TestClient_GetMarketDepth(t *testing.T) { 6 | baseURL := "https://api.huobi.pro" 7 | params := &ApiParameter{ 8 | Debug: true, 9 | AccessKey: "", 10 | SecretKey: "", 11 | EnablePrivateSign: false, 12 | BaseURL: baseURL, 13 | PrivateKeyPrime256: "", 14 | HttpClient: nil, 15 | ProxyURL: "", 16 | } 17 | c := NewClient(params) 18 | // {"ch":"market.btcusdt.depth.step0","status":"ok","ts":1590479896449,"tick":{"bids":[[8949.99,3.897438],[8949.98,0.002868],[8949.64,0.003209],[8949.61,0.015694],[8949.53,0.3],[8949.45,0.019441],[8948.9,0.01],[8948.82,0.005331],[8948.78,0.1],[8948.72,0.5],[8948.68,0.055858],[8948.67,0.391585],[8948.47,0.22342],[8948.38,0.111716],[8948.29,0.217027],[8948.06,0.004783],[8948.05,0.301848],[8948.04,0.009797],[8948.03,1.895833],[8948.02,0.446841]],"asks":[[8950.0,35.329777364243526],[8950.36,0.002],[8950.37,0.203653],[8950.51,0.177986],[8950.61,0.002],[8950.85,0.002],[8950.88,0.004],[8951.05,0.011408],[8951.44,0.001904],[8951.65,5.7E-4],[8951.96,0.011832],[8951.97,0.202934],[8951.98,0.015582],[8952.0,0.041264],[8952.14,0.131941],[8952.26,0.2],[8952.27,0.015],[8952.58,0.2445],[8952.6,0.01],[8952.68,0.6555]]}} 19 | depth, err := c.GetMarketDepth("btcusdt", 20, "") 20 | if err != nil { 21 | t.Error(err) 22 | return 23 | } 24 | t.Logf("%#v", depth) 25 | } 26 | -------------------------------------------------------------------------------- /test_config.example.yaml: -------------------------------------------------------------------------------- 1 | access_key: "" 2 | secret_key: "" 3 | -------------------------------------------------------------------------------- /utils/gzip.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "io/ioutil" 7 | ) 8 | 9 | var defaultLevel = gzip.DefaultCompression 10 | 11 | // SetLevel 设置压缩级别 12 | func SetLevel(level int) { 13 | defaultLevel = level 14 | } 15 | 16 | // GzipCompress gzip 压缩 17 | func GzipCompress(in []byte) ([]byte, error) { 18 | var buffer bytes.Buffer 19 | 20 | writer, err := gzip.NewWriterLevel(&buffer, defaultLevel) 21 | if err != nil { 22 | return nil, err 23 | } 24 | defer writer.Close() 25 | 26 | _, err = writer.Write(in) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return buffer.Bytes(), nil 32 | } 33 | 34 | // GzipUncompress gzip 解压 35 | func GzipUncompress(in []byte) ([]byte, error) { 36 | reader, err := gzip.NewReader(bytes.NewReader(in)) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | defer reader.Close() 42 | 43 | return ioutil.ReadAll(reader) 44 | } 45 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | var ( 19 | DefaultTransport *http.Transport 20 | ) 21 | 22 | func init() { 23 | DefaultTransport = &http.Transport{ 24 | DialContext: (&net.Dialer{ 25 | Timeout: 5 * time.Second, 26 | KeepAlive: 30 * time.Second, 27 | DualStack: true, 28 | }).DialContext, 29 | IdleConnTimeout: 1 * time.Minute, 30 | TLSHandshakeTimeout: 10 * time.Second, 31 | ExpectContinueTimeout: 1 * time.Second, 32 | DisableKeepAlives: false, 33 | MaxResponseHeaderBytes: 1 << 15, 34 | } 35 | } 36 | 37 | func DefaultHttpClient(proxyURL string) *http.Client { 38 | transport := CloneDefaultTransport() 39 | if proxyURL != "" { 40 | transport.Proxy, _ = ParseProxy(proxyURL) 41 | } 42 | httpClient := &http.Client{ 43 | Timeout: 10 * time.Second, 44 | Transport: transport, 45 | } 46 | return httpClient 47 | } 48 | 49 | func HttpGet(client *http.Client, reqUrl string, postData string, headers map[string]string) ([]byte, error) { 50 | return NewHttpRequest(client, "GET", reqUrl, postData, headers) 51 | } 52 | 53 | func HttpPost(client *http.Client, reqUrl string, postData string, headers map[string]string) ([]byte, error) { 54 | return NewHttpRequest(client, "POST", reqUrl, postData, headers) 55 | } 56 | 57 | func NewHttpRequest(client *http.Client, method string, reqUrl string, postData string, requestHeaders map[string]string) ([]byte, error) { 58 | req, _ := http.NewRequest(method, reqUrl, strings.NewReader(postData)) 59 | //req.Header.Set("User-Agent", defaultUserAgent) 60 | 61 | if requestHeaders != nil { 62 | for k, v := range requestHeaders { 63 | req.Header.Add(k, v) 64 | } 65 | } 66 | 67 | resp, err := client.Do(req) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | defer resp.Body.Close() 73 | 74 | bodyData, err := ioutil.ReadAll(resp.Body) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | if resp.StatusCode != 200 { 80 | return nil, errors.New(fmt.Sprintf("HttpStatusCode: %d, Desc: %s", resp.StatusCode, string(bodyData))) 81 | } 82 | 83 | return bodyData, nil 84 | } 85 | 86 | func ValuesToJson(v url.Values) ([]byte, error) { 87 | m := make(map[string]interface{}) 88 | for k, vv := range v { 89 | if len(vv) == 1 { 90 | m[k] = vv[0] 91 | } else { 92 | m[k] = vv 93 | } 94 | } 95 | return json.Marshal(m) 96 | } 97 | 98 | func GetParamHmacSHA256Base64Sign(secret, params string) (string, error) { 99 | mac := hmac.New(sha256.New, []byte(secret)) 100 | _, err := mac.Write([]byte(params)) 101 | if err != nil { 102 | return "", err 103 | } 104 | signByte := mac.Sum(nil) 105 | return base64.StdEncoding.EncodeToString(signByte), nil 106 | } 107 | 108 | func CloneDefaultTransport() *http.Transport { 109 | return &http.Transport{DialContext: DefaultTransport.DialContext, 110 | IdleConnTimeout: DefaultTransport.IdleConnTimeout, 111 | TLSHandshakeTimeout: DefaultTransport.TLSHandshakeTimeout, 112 | ExpectContinueTimeout: DefaultTransport.ExpectContinueTimeout, 113 | MaxResponseHeaderBytes: DefaultTransport.MaxResponseHeaderBytes, 114 | Proxy: DefaultTransport.Proxy, 115 | DisableKeepAlives: DefaultTransport.DisableKeepAlives, 116 | } 117 | } 118 | 119 | // "socks5://127.0.0.1:1080" 120 | func ParseProxy(proxyURL string) (res func(*http.Request) (*url.URL, error), err error) { 121 | var purl *url.URL 122 | purl, err = url.Parse(proxyURL) 123 | if err != nil { 124 | return 125 | } 126 | res = http.ProxyURL(purl) 127 | return 128 | } 129 | --------------------------------------------------------------------------------