├── .gitignore ├── go.mod ├── signer_test.go ├── time.go ├── helper.go ├── service_status_test.go ├── service_status.go ├── time_test.go ├── helper_test.go ├── pagination.go ├── LICENSE ├── fill_test.go ├── fill.go ├── go.sum ├── hf_account_test.go ├── signer.go ├── hf_account.go ├── currency_test.go ├── websocket_test.go ├── deposit_test.go ├── deposit.go ├── withdrawal_test.go ├── withdrawal.go ├── examples └── main.go ├── currency.go ├── earn_test.go ├── api.go ├── http.go ├── earn.go ├── symbol_test.go ├── symbol.go ├── websocket.go ├── hf_order_types.go ├── order_test.go ├── account_test.go ├── hf_order.go ├── README.md ├── order.go └── hf_order_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | .github 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Kucoin/kucoin-go-sdk 2 | 3 | require ( 4 | github.com/google/go-querystring v1.1.0 // indirect 5 | github.com/gorilla/websocket v1.4.2 6 | github.com/pkg/errors v0.8.1 7 | github.com/sirupsen/logrus v1.4.1 8 | ) 9 | 10 | go 1.13 11 | -------------------------------------------------------------------------------- /signer_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import "testing" 4 | 5 | func TestKcSigner_Sign(t *testing.T) { 6 | s := NewKcSigner("abc", "efg", "kcs") 7 | b := []byte("GET/api/v1/orders") 8 | if string(s.Sign(b)) != "iOdkcc7K6cyY8Cdr3yMcTgXCof4vhHCaDyrDSG7Qf3w=" { 9 | t.Error("Invalid sign") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type ServerTimeModel int64 9 | 10 | // ServerTime returns the API server time. 11 | func (as *ApiService) ServerTime(ctx context.Context) (*ApiResponse, error) { 12 | req := NewRequest(http.MethodGet, "/api/v1/timestamp", nil) 13 | return as.Call(ctx, req) 14 | } 15 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | ) 7 | 8 | // IntToString converts int64 to string. 9 | func IntToString(i int64) string { 10 | return strconv.FormatInt(i, 10) 11 | } 12 | 13 | // ToJsonString converts any value to JSON string. 14 | func ToJsonString(v interface{}) string { 15 | b, err := json.Marshal(v) 16 | if err != nil { 17 | return "" 18 | } 19 | return string(b) 20 | } 21 | -------------------------------------------------------------------------------- /service_status_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestApiService_ServiceStatus(t *testing.T) { 9 | s := NewApiServiceFromEnv() 10 | 11 | rsp, err := s.ServiceStatus(context.Background()) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | var ss ServiceStatusModel 16 | if err := rsp.ReadData(&ss); err != nil { 17 | t.Fatal(err) 18 | } 19 | if ss.Status == "" { 20 | t.Fatal("empty status") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /service_status.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // A ServiceStatusModel represents the structure of service status. 9 | type ServiceStatusModel struct { 10 | Status string `json:"status"` 11 | Msg string `json:"msg"` 12 | } 13 | 14 | // ServiceStatus returns the service status. 15 | func (as *ApiService) ServiceStatus(ctx context.Context) (*ApiResponse, error) { 16 | req := NewRequest(http.MethodGet, "/api/v1/status", nil) 17 | return as.Call(ctx, req) 18 | } 19 | -------------------------------------------------------------------------------- /time_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestApiService_ServerTime(t *testing.T) { 11 | s := NewApiServiceFromEnv() 12 | rsp, err := s.ServerTime(context.Background()) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | var ts ServerTimeModel 17 | if err := rsp.ReadData(&ts); err != nil { 18 | t.Fatal(err) 19 | } 20 | t.Log(ts) 21 | now := time.Now().UnixNano() / 1000 / 1000 22 | if math.Abs(float64(int64(ts)-now)) > 10000 { 23 | t.Error("Invalid timestamp") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import "testing" 4 | 5 | func TestIntToString(t *testing.T) { 6 | var i int64 = 5200 7 | if IntToString(i) != "5200" { 8 | t.Error("Invalid string") 9 | } 10 | } 11 | 12 | func TestToJsonString(t *testing.T) { 13 | type test struct { 14 | M1 string `json:"m1"` 15 | M2 int64 `json:"m2"` 16 | M3 bool `json:"m3"` 17 | M4 map[string]string `json:"m4"` 18 | m5 chan string 19 | } 20 | var s = test{ 21 | M1: "KuCoin", 22 | M2: 5200, 23 | M3: false, 24 | M4: map[string]string{"KCS": "$300"}, 25 | m5: make(chan string, 10), 26 | } 27 | var a = `{"m1":"KuCoin","m2":5200,"m3":false,"m4":{"KCS":"$300"}}` 28 | if ToJsonString(s) != a { 29 | t.Error("Invalid string") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pagination.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import "encoding/json" 4 | 5 | // A PaginationParam represents the pagination parameters `currentPage` `pageSize` in a request . 6 | type PaginationParam struct { 7 | CurrentPage int64 8 | PageSize int64 9 | } 10 | 11 | // ReadParam read pagination parameters into params. 12 | func (p *PaginationParam) ReadParam(params map[string]string) { 13 | params["currentPage"], params["pageSize"] = IntToString(p.CurrentPage), IntToString(p.PageSize) 14 | } 15 | 16 | // A PaginationModel represents the pagination in a response. 17 | type PaginationModel struct { 18 | CurrentPage int64 `json:"currentPage"` 19 | PageSize int64 `json:"pageSize"` 20 | TotalNum int64 `json:"totalNum"` 21 | TotalPage int64 `json:"totalPage"` 22 | RawItems json.RawMessage `json:"items"` // delay parsing 23 | } 24 | 25 | // ReadItems read the `items` into v. 26 | func (p *PaginationModel) ReadItems(v interface{}) error { 27 | return json.Unmarshal(p.RawItems, v) 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 KuCoin 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 | -------------------------------------------------------------------------------- /fill_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestApiService_Fills(t *testing.T) { 9 | s := NewApiServiceFromEnv() 10 | p := &PaginationParam{CurrentPage: 1, PageSize: 10} 11 | rsp, err := s.Fills(context.Background(), map[string]string{}, p) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | fs := FillsModel{} 17 | if _, err := rsp.ReadPaginationData(&fs); err != nil { 18 | t.Fatal(err) 19 | } 20 | for _, f := range fs { 21 | t.Log(ToJsonString(f)) 22 | switch { 23 | case f.Symbol == "": 24 | t.Error("Empty key 'symbol'") 25 | case f.TradeId == "": 26 | t.Error("Empty key 'tradeId'") 27 | case f.OrderId == "": 28 | t.Error("Empty key 'orderId'") 29 | case f.Type == "": 30 | t.Error("Empty key 'type'") 31 | case f.Side == "": 32 | t.Error("Empty key 'side'") 33 | } 34 | } 35 | } 36 | 37 | func TestApiService_RecentFills(t *testing.T) { 38 | s := NewApiServiceFromEnv() 39 | rsp, err := s.RecentFills(context.Background()) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | fs := FillsModel{} 45 | if err := rsp.ReadData(&fs); err != nil { 46 | t.Fatal(err) 47 | } 48 | for _, f := range fs { 49 | t.Log(ToJsonString(f)) 50 | switch { 51 | case f.Symbol == "": 52 | t.Error("Empty key 'symbol'") 53 | case f.TradeId == "": 54 | t.Error("Empty key 'tradeId'") 55 | case f.OrderId == "": 56 | t.Error("Empty key 'orderId'") 57 | case f.Type == "": 58 | t.Error("Empty key 'type'") 59 | case f.Side == "": 60 | t.Error("Empty key 'side'") 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /fill.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // A FillModel represents the structure of fill. 9 | type FillModel struct { 10 | Symbol string `json:"symbol"` 11 | TradeId string `json:"tradeId"` 12 | OrderId string `json:"orderId"` 13 | CounterOrderId string `json:"counterOrderId"` 14 | Side string `json:"side"` 15 | Liquidity string `json:"liquidity"` 16 | ForceTaker bool `json:"forceTaker"` 17 | Price string `json:"price"` 18 | Size string `json:"size"` 19 | Funds string `json:"funds"` 20 | Fee string `json:"fee"` 21 | FeeRate string `json:"feeRate"` 22 | FeeCurrency string `json:"feeCurrency"` 23 | Stop string `json:"stop"` 24 | Type string `json:"type"` 25 | CreatedAt int64 `json:"createdAt"` 26 | TradeType string `json:"tradeType"` 27 | } 28 | 29 | // A FillsModel is the set of *FillModel. 30 | type FillsModel []*FillModel 31 | 32 | // Fills returns a list of recent fills. 33 | func (as *ApiService) Fills(ctx context.Context, params map[string]string, pagination *PaginationParam) (*ApiResponse, error) { 34 | pagination.ReadParam(params) 35 | req := NewRequest(http.MethodGet, "/api/v1/fills", params) 36 | return as.Call(ctx, req) 37 | } 38 | 39 | // RecentFills returns the recent fills of the latest transactions within 24 hours. 40 | func (as *ApiService) RecentFills(ctx context.Context) (*ApiResponse, error) { 41 | req := NewRequest(http.MethodGet, "/api/v1/limit/fills", nil) 42 | return as.Call(ctx, req) 43 | } 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 4 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 5 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 6 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 7 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 8 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 9 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 10 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 11 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= 15 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 16 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 18 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 19 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= 20 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 21 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 22 | -------------------------------------------------------------------------------- /hf_account_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestApiService_HfAccountInnerTransfer(t *testing.T) { 10 | s := NewApiServiceFromEnv() 11 | clientOid := IntToString(time.Now().Unix()) 12 | p := map[string]string{ 13 | "clientOid": clientOid, 14 | "currency": "USDT", 15 | "from": "trade", 16 | "to": "margin_v2", 17 | "amount": "1", 18 | } 19 | rsp, err := s.HfAccountInnerTransfer(context.Background(), p) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | v := &InnerTransferResultModel{} 24 | if err := rsp.ReadData(v); err != nil { 25 | t.Fatal(err) 26 | } 27 | t.Log(ToJsonString(v)) 28 | if v.OrderId == "" { 29 | t.Error("Empty key 'orderId'") 30 | } 31 | } 32 | 33 | func TestApiService_HfAccounts(t *testing.T) { 34 | s := NewApiServiceFromEnv() 35 | 36 | rsp, err := s.HfAccounts(context.Background(), "", "trade_hf") 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | v := &HfAccountsModel{} 41 | if err := rsp.ReadData(v); err != nil { 42 | t.Fatal(err) 43 | } 44 | t.Log(ToJsonString(v)) 45 | } 46 | 47 | func TestApiService_HfAccount(t *testing.T) { 48 | s := NewApiServiceFromEnv() 49 | rsp, err := s.HfAccount(context.Background(), "2969860516868") 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | v := &HfAccountModel{} 54 | if err := rsp.ReadData(v); err != nil { 55 | t.Fatal(err) 56 | } 57 | t.Log(ToJsonString(v)) 58 | } 59 | 60 | func TestApiService_HfAccountTransferable(t *testing.T) { 61 | s := NewApiServiceFromEnv() 62 | rsp, err := s.HfAccountTransferable(context.Background(), "USDT") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | a := &HfAccountTransferableModel{} 67 | if err := rsp.ReadData(a); err != nil { 68 | t.Fatal(err) 69 | } 70 | t.Log(ToJsonString(a)) 71 | } 72 | 73 | func TestApiService_HfAccountLedgers(t *testing.T) { 74 | s := NewApiServiceFromEnv() 75 | p := map[string]string{} 76 | rsp, err := s.HfAccountLedgers(context.Background(), p) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | a := &HfAccountLedgersModel{} 81 | if err := rsp.ReadData(a); err != nil { 82 | t.Fatal(err) 83 | } 84 | t.Log(ToJsonString(a)) 85 | } 86 | -------------------------------------------------------------------------------- /signer.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "time" 8 | ) 9 | 10 | // Signer interface contains Sign() method. 11 | type Signer interface { 12 | Sign(plain []byte) []byte 13 | } 14 | 15 | // Sha256Signer is the sha256 Signer. 16 | type Sha256Signer struct { 17 | key []byte 18 | } 19 | 20 | // Sign makes a signature by sha256. 21 | func (ss *Sha256Signer) Sign(plain []byte) []byte { 22 | hm := hmac.New(sha256.New, ss.key) 23 | hm.Write(plain) 24 | return hm.Sum(nil) 25 | } 26 | 27 | // KcSigner is the implement of Signer for KuCoin. 28 | type KcSigner struct { 29 | Sha256Signer 30 | apiKey string 31 | apiSecret string 32 | apiPassPhrase string 33 | apiKeyVersion string 34 | } 35 | 36 | // Sign makes a signature by sha256 with `apiKey` `apiSecret` `apiPassPhrase`. 37 | func (ks *KcSigner) Sign(plain []byte) []byte { 38 | s := ks.Sha256Signer.Sign(plain) 39 | return []byte(base64.StdEncoding.EncodeToString(s)) 40 | } 41 | 42 | // Headers returns a map of signature header. 43 | func (ks *KcSigner) Headers(plain string) map[string]string { 44 | t := IntToString(time.Now().UnixNano() / 1000000) 45 | p := []byte(t + plain) 46 | s := string(ks.Sign(p)) 47 | ksHeaders := map[string]string{ 48 | "KC-API-KEY": ks.apiKey, 49 | "KC-API-PASSPHRASE": ks.apiPassPhrase, 50 | "KC-API-TIMESTAMP": t, 51 | "KC-API-SIGN": s, 52 | } 53 | 54 | if ks.apiKeyVersion != "" && ks.apiKeyVersion != ApiKeyVersionV1 { 55 | ksHeaders["KC-API-KEY-VERSION"] = ks.apiKeyVersion 56 | } 57 | 58 | return ksHeaders 59 | } 60 | 61 | // NewKcSigner creates a instance of KcSigner. 62 | func NewKcSigner(key, secret, passPhrase string) *KcSigner { 63 | ks := &KcSigner{ 64 | apiKey: key, 65 | apiSecret: secret, 66 | apiPassPhrase: passPhrase, 67 | apiKeyVersion: ApiKeyVersionV1, 68 | } 69 | ks.key = []byte(secret) 70 | return ks 71 | } 72 | 73 | // NewKcSignerV2 creates a instance of KcSigner. 74 | func NewKcSignerV2(key, secret, passPhrase string) *KcSigner { 75 | ks := &KcSigner{ 76 | apiKey: key, 77 | apiSecret: secret, 78 | apiPassPhrase: passPhraseEncrypt([]byte(secret), []byte(passPhrase)), 79 | apiKeyVersion: ApiKeyVersionV2, 80 | } 81 | ks.key = []byte(secret) 82 | return ks 83 | } 84 | 85 | // passPhraseEncrypt, encrypt passPhrase 86 | func passPhraseEncrypt(key, plain []byte) string { 87 | hm := hmac.New(sha256.New, key) 88 | hm.Write(plain) 89 | return base64.StdEncoding.EncodeToString(hm.Sum(nil)) 90 | } 91 | -------------------------------------------------------------------------------- /hf_account.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // HfAccountInnerTransfer Users can transfer funds between their main account, 9 | // trading account, and high-frequency trading account free of charge. 10 | func (as *ApiService) HfAccountInnerTransfer(ctx context.Context, params map[string]string) (*ApiResponse, error) { 11 | req := NewRequest(http.MethodPost, "/api/v2/accounts/inner-transfer", params) 12 | return as.Call(ctx, req) 13 | } 14 | 15 | type HfAccountInnerTransferRes struct { 16 | OrderId string `json:"orderId"` 17 | } 18 | 19 | // HfAccounts Get a list of high-frequency trading accounts. 20 | func (as *ApiService) HfAccounts(ctx context.Context, currency, accountType string) (*ApiResponse, error) { 21 | p := map[string]string{ 22 | "currency": currency, 23 | "type": accountType, 24 | } 25 | req := NewRequest(http.MethodGet, "/api/v1/accounts", p) 26 | return as.Call(ctx, req) 27 | } 28 | 29 | type HfAccountsModel []HfAccountModel 30 | 31 | // HfAccount Get the details of the high-frequency trading account 32 | func (as *ApiService) HfAccount(ctx context.Context, accountId string) (*ApiResponse, error) { 33 | req := NewRequest(http.MethodGet, "/api/v1/accounts/"+accountId, nil) 34 | return as.Call(ctx, req) 35 | } 36 | 37 | type HfAccountModel struct { 38 | Balance string `json:"balance"` 39 | Available string `json:"available"` 40 | Currency string `json:"currency"` 41 | Holds string `json:"holds"` 42 | Type string `json:"type"` 43 | Id string `json:"id"` 44 | } 45 | 46 | // HfAccountTransferable This API can be used to obtain the amount of transferrable funds 47 | // in high-frequency trading accounts. 48 | func (as *ApiService) HfAccountTransferable(ctx context.Context, currency string) (*ApiResponse, error) { 49 | p := map[string]string{ 50 | "currency": currency, 51 | "type": "TRADE_HF", 52 | } 53 | req := NewRequest(http.MethodGet, "/api/v1/accounts/transferable", p) 54 | return as.Call(ctx, req) 55 | } 56 | 57 | type HfAccountTransferableModel struct { 58 | Balance string `json:"balance"` 59 | Available string `json:"available"` 60 | Currency string `json:"currency"` 61 | Holds string `json:"holds"` 62 | Transferable string `json:"transferable"` 63 | } 64 | 65 | // HfAccountLedgers returns all transfer (in and out) records in high-frequency trading account 66 | // and supports multi-coin queries. The query results are sorted in descending order by createdAt and id. 67 | func (as *ApiService) HfAccountLedgers(ctx context.Context, params map[string]string) (*ApiResponse, error) { 68 | req := NewRequest(http.MethodGet, "/api/v1/hf/accounts/ledgers", params) 69 | return as.Call(ctx, req) 70 | } 71 | 72 | type HfAccountLedgersModel []*HfAccountLedgerModel 73 | 74 | type HfAccountLedgerModel struct { 75 | Id string `json:"id"` 76 | Currency string `json:"currency"` 77 | Amount string `json:"amount"` 78 | Fee string `json:"fee"` 79 | Balance string `json:"balance"` 80 | AccountType string `json:"accountType"` 81 | BizType string `json:"bizType"` 82 | Direction string `json:"direction"` 83 | CreatedAt string `json:"createdAt"` 84 | Context string `json:"context"` 85 | } 86 | -------------------------------------------------------------------------------- /currency_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestApiService_Currencies(t *testing.T) { 9 | s := NewApiServiceFromEnv() 10 | rsp, err := s.Currencies(context.Background()) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | cl := CurrenciesModel{} 15 | if err := rsp.ReadData(&cl); err != nil { 16 | t.Fatal(err) 17 | } 18 | for _, c := range cl { 19 | t.Log(ToJsonString(c)) 20 | switch { 21 | case c.Name == "": 22 | t.Error("Empty key 'name'") 23 | case c.Currency == "": 24 | t.Error("Empty key 'currency'") 25 | case c.FullName == "": 26 | t.Error("Empty key 'fullName'") 27 | case c.WithdrawalMinSize == "": 28 | t.Error("Empty key 'withdrawalMinSize'") 29 | case c.WithdrawalMinFee == "": 30 | t.Error("Empty key 'withdrawalMinFee'") 31 | } 32 | } 33 | } 34 | 35 | func TestApiService_Currency(t *testing.T) { 36 | s := NewApiServiceFromEnv() 37 | rsp, err := s.Currency(context.Background(), "BTC", "") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | c := &CurrencyModel{} 42 | if err := rsp.ReadData(c); err != nil { 43 | t.Fatal(err) 44 | } 45 | t.Log(ToJsonString(c)) 46 | switch { 47 | case c.Name == "": 48 | t.Error("Empty key 'name'") 49 | case c.Currency == "": 50 | t.Error("Empty key 'currency'") 51 | case c.FullName == "": 52 | t.Error("Empty key 'fullName'") 53 | case c.WithdrawalMinSize == "": 54 | t.Error("Empty key 'withdrawalMinSize'") 55 | case c.WithdrawalMinFee == "": 56 | t.Error("Empty key 'withdrawalMinFee'") 57 | } 58 | } 59 | 60 | func TestApiService_Currency_V2(t *testing.T) { 61 | s := NewApiServiceFromEnv() 62 | rsp, err := s.CurrencyV2(context.Background(), "BTC", "") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | c := &CurrencyV2Model{} 67 | if err := rsp.ReadData(c); err != nil { 68 | t.Fatal(err) 69 | } 70 | t.Log(ToJsonString(c)) 71 | switch { 72 | case c.Name == "": 73 | t.Error("Empty key 'name'") 74 | case c.Currency == "": 75 | t.Error("Empty key 'currency'") 76 | case c.FullName == "": 77 | t.Error("Empty key 'fullName'") 78 | } 79 | } 80 | 81 | func TestApiService_Prices(t *testing.T) { 82 | s := NewApiServiceFromEnv() 83 | rsp, err := s.Prices(context.Background(), "USD", "BTC,KCS") 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | p := PricesModel{} 88 | if err := rsp.ReadData(&p); err != nil { 89 | t.Fatal(err) 90 | } 91 | if len(p) == 0 { 92 | t.Error("Empty prices") 93 | } 94 | t.Log(ToJsonString(p)) 95 | } 96 | 97 | func TestApiServiceCurrenciesV3(t *testing.T) { 98 | s := NewApiServiceFromEnv() 99 | rsp, err := s.CurrenciesV3(context.Background()) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | p := CurrenciesV3Model{} 104 | if err := rsp.ReadData(&p); err != nil { 105 | t.Fatal(err) 106 | } 107 | t.Log(ToJsonString(p)) 108 | } 109 | 110 | func TestApiService_CurrencyInfoV3(t *testing.T) { 111 | s := NewApiServiceFromEnv() 112 | rsp, err := s.CurrencyInfoV3(context.Background(), "BTC") 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | p := CurrencyV3Model{} 117 | if err := rsp.ReadData(&p); err != nil { 118 | t.Fatal(err) 119 | } 120 | t.Log(ToJsonString(p)) 121 | } 122 | -------------------------------------------------------------------------------- /websocket_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestApiService_WebSocketPublicToken(t *testing.T) { 9 | s := NewApiServiceFromEnv() 10 | rsp, err := s.WebSocketPublicToken(context.Background()) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | pt := &WebSocketTokenModel{} 15 | if err := rsp.ReadData(pt); err != nil { 16 | t.Fatal(err) 17 | } 18 | t.Log(pt.Token) 19 | switch { 20 | case pt.Token == "": 21 | t.Error("Empty key 'token'") 22 | case len(pt.Servers) == 0: 23 | t.Fatal("Empty key 'instanceServers'") 24 | } 25 | for _, s := range pt.Servers { 26 | t.Log(ToJsonString(s)) 27 | switch { 28 | case s.Endpoint == "": 29 | t.Error("Empty key 'endpoint'") 30 | case s.Protocol == "": 31 | t.Fatal("Empty key 'protocol'") 32 | } 33 | } 34 | } 35 | 36 | func TestApiService_WebSocketPrivateToken(t *testing.T) { 37 | s := NewApiServiceFromEnv() 38 | rsp, err := s.WebSocketPrivateToken(context.Background()) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | pt := &WebSocketTokenModel{} 43 | if err := rsp.ReadData(pt); err != nil { 44 | t.Fatal(err) 45 | } 46 | t.Log(pt.Token) 47 | switch { 48 | case pt.Token == "": 49 | t.Error("Empty key 'token'") 50 | case len(pt.Servers) == 0: 51 | t.Fatal("Empty key 'instanceServers'") 52 | } 53 | for _, s := range pt.Servers { 54 | t.Log(ToJsonString(s)) 55 | switch { 56 | case s.Endpoint == "": 57 | t.Error("Empty key 'endpoint'") 58 | case s.Protocol == "": 59 | t.Fatal("Empty key 'protocol'") 60 | } 61 | } 62 | } 63 | 64 | func TestWebSocketClient_Connect(t *testing.T) { 65 | s := NewApiServiceFromEnv() 66 | 67 | rsp, err := s.WebSocketPublicToken(context.Background()) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | tk := &WebSocketTokenModel{} 73 | if err := rsp.ReadData(tk); err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | c := s.NewWebSocketClient(tk) 78 | 79 | _, _, err = c.Connect() 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | } 84 | func TestWebSocketClient_Subscribe(t *testing.T) { 85 | t.SkipNow() 86 | 87 | s := NewApiServiceFromEnv() 88 | 89 | rsp, err := s.WebSocketPublicToken(context.Background()) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | 94 | tk := &WebSocketTokenModel{} 95 | if err := rsp.ReadData(tk); err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | c := s.NewWebSocketClient(tk) 100 | 101 | mc, ec, err := c.Connect() 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | 106 | ch1 := NewSubscribeMessage("/market/ticker:KCS-BTC", false) 107 | ch2 := NewSubscribeMessage("/market/ticker:ETH-BTC", false) 108 | uch := NewUnsubscribeMessage("/market/ticker:ETH-BTC", false) 109 | 110 | if err := c.Subscribe(ch1, ch2); err != nil { 111 | t.Fatal(err) 112 | } 113 | 114 | var i = 0 115 | for { 116 | select { 117 | case err := <-ec: 118 | c.Stop() // Stop subscribing the WebSocket feed 119 | t.Fatal(err) 120 | case msg := <-mc: 121 | t.Log(ToJsonString(msg)) 122 | i++ 123 | if i == 5 { 124 | t.Log("Unsubscribe ETH-BTC") 125 | if err = c.Unsubscribe(uch); err != nil { 126 | t.Fatal(err) 127 | } 128 | } 129 | if i == 10 { 130 | t.Log("Subscribe ETH-BTC") 131 | if err = c.Subscribe(ch2); err != nil { 132 | t.Fatal(err) 133 | } 134 | } 135 | if i == 15 { 136 | t.Log("Exit subscribing") 137 | c.Stop() // Stop subscribing the WebSocket feed 138 | return 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /deposit_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestApiService_CreateDepositAddress(t *testing.T) { 9 | t.SkipNow() 10 | 11 | s := NewApiServiceFromEnv() 12 | rsp, err := s.CreateDepositAddress(context.Background(), "KCS", "") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | a := &DepositAddressModel{} 17 | if err := rsp.ReadData(a); err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | t.Log(ToJsonString(a)) 22 | 23 | switch { 24 | case a.Address == "": 25 | t.Error("Empty key 'address'") 26 | case a.Memo == "": 27 | t.Error("Empty key 'memo'") 28 | } 29 | } 30 | 31 | func TestApiService_DepositAddresses(t *testing.T) { 32 | t.SkipNow() 33 | 34 | s := NewApiServiceFromEnv() 35 | rsp, err := s.DepositAddresses(context.Background(), "KCS", "") 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | if rsp.Code == "260200" { 40 | // Ignore deposit.disabled 41 | return 42 | } 43 | as := DepositAddressesModel{} 44 | if err := rsp.ReadData(&as); err != nil { 45 | t.Fatal(err) 46 | } 47 | t.Log(ToJsonString(as)) 48 | if as.Address == "" { 49 | t.Error("Empty key 'address'") 50 | } 51 | if as.Memo == "" { 52 | t.Error("Empty key 'memo'") 53 | } 54 | } 55 | 56 | func TestApiService_DepositAddressesV2(t *testing.T) { 57 | s := NewApiServiceFromEnv() 58 | rsp, err := s.DepositAddressesV2(context.Background(), "USDT") 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | as := DepositAddressesV2Model{} 63 | if err := rsp.ReadData(&as); err != nil { 64 | t.Fatal(err) 65 | } 66 | t.Log(ToJsonString(as)) 67 | } 68 | 69 | func TestApiService_Deposits(t *testing.T) { 70 | s := NewApiServiceFromEnv() 71 | p := map[string]string{} 72 | pp := &PaginationParam{CurrentPage: 1, PageSize: 10} 73 | rsp, err := s.Deposits(context.Background(), p, pp) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | ds := DepositsModel{} 78 | if _, err := rsp.ReadPaginationData(&ds); err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | for _, d := range ds { 83 | t.Log(ToJsonString(d)) 84 | switch { 85 | case d.Address == "": 86 | t.Error("Empty key 'address'") 87 | case d.Amount == "": 88 | t.Error("Empty key 'amount'") 89 | case d.Fee == "": 90 | t.Error("Empty key 'fee'") 91 | case d.Currency == "": 92 | t.Error("Empty key 'currency'") 93 | case d.WalletTxId == "": 94 | t.Error("Empty key 'walletTxId'") 95 | case d.Status == "": 96 | t.Error("Empty key 'status'") 97 | case d.CreatedAt == 0: 98 | t.Error("Empty key 'createdAt'") 99 | case d.UpdatedAt == 0: 100 | t.Error("Empty key 'updatedAt'") 101 | } 102 | } 103 | } 104 | 105 | func TestApiService_V1Deposits(t *testing.T) { 106 | t.SkipNow() 107 | s := NewApiServiceFromEnv() 108 | p := map[string]string{} 109 | pp := &PaginationParam{CurrentPage: 1, PageSize: 10} 110 | rsp, err := s.V1Deposits(context.Background(), p, pp) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | ds := V1DepositsModel{} 115 | if _, err := rsp.ReadPaginationData(&ds); err != nil { 116 | t.Fatal(err) 117 | } 118 | 119 | for _, d := range ds { 120 | t.Log(ToJsonString(d)) 121 | switch { 122 | case d.Amount == "": 123 | t.Error("Empty key 'amount'") 124 | case d.Currency == "": 125 | t.Error("Empty key 'currency'") 126 | case d.WalletTxId == "": 127 | t.Error("Empty key 'walletTxId'") 128 | case d.Status == "": 129 | t.Error("Empty key 'status'") 130 | case d.CreateAt == 0: 131 | t.Error("Empty key 'createAt'") 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /deposit.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // A DepositAddressModel represents a deposit address of currency for deposit. 9 | type DepositAddressModel struct { 10 | Address string `json:"address"` 11 | Memo string `json:"memo"` 12 | Chain string `json:"chain"` 13 | } 14 | 15 | // A DepositAddressesModel is the set of *DepositAddressModel. 16 | type DepositAddressesModel DepositAddressModel 17 | 18 | // A DepositModel represents a deposit record. 19 | type DepositModel struct { 20 | Chain string `json:"chain"` 21 | Address string `json:"address"` 22 | Memo string `json:"memo"` 23 | Amount string `json:"amount"` 24 | Fee string `json:"fee"` 25 | Currency string `json:"currency"` 26 | IsInner bool `json:"isInner"` 27 | WalletTxId string `json:"walletTxId"` 28 | Status string `json:"status"` 29 | Remark string `json:"remark"` 30 | CreatedAt int64 `json:"createdAt"` 31 | UpdatedAt int64 `json:"updatedAt"` 32 | } 33 | 34 | // A DepositsModel is the set of *DepositModel. 35 | type DepositsModel []*DepositModel 36 | 37 | // CreateDepositAddress creates a deposit address. 38 | func (as *ApiService) CreateDepositAddress(ctx context.Context, currency, chain string) (*ApiResponse, error) { 39 | params := map[string]string{"currency": currency} 40 | if chain != "" { 41 | params["chain"] = chain 42 | } 43 | req := NewRequest(http.MethodPost, "/api/v1/deposit-addresses", params) 44 | return as.Call(ctx, req) 45 | } 46 | 47 | // DepositAddresses returns the deposit address of currency for deposit. 48 | // If return data is empty, you may need create a deposit address first. 49 | func (as *ApiService) DepositAddresses(ctx context.Context, currency, chain string) (*ApiResponse, error) { 50 | params := map[string]string{"currency": currency} 51 | if chain != "" { 52 | params["chain"] = chain 53 | } 54 | req := NewRequest(http.MethodGet, "/api/v1/deposit-addresses", params) 55 | return as.Call(ctx, req) 56 | } 57 | 58 | type depositAddressV2Model struct { 59 | Address string `json:"address"` 60 | Memo string `json:"memo"` 61 | Chain string `json:"chain"` 62 | ContractAddress string `json:"contract_address"` 63 | } 64 | type DepositAddressesV2Model []*depositAddressV2Model 65 | 66 | // DepositAddressesV2 Get all deposit addresses for the currency you intend to deposit. 67 | // If the returned data is empty, you may need to create a deposit address first. 68 | func (as *ApiService) DepositAddressesV2(ctx context.Context, currency string) (*ApiResponse, error) { 69 | params := map[string]string{"currency": currency} 70 | req := NewRequest(http.MethodGet, "/api/v2/deposit-addresses", params) 71 | return as.Call(ctx, req) 72 | } 73 | 74 | // Deposits returns a list of deposit. 75 | func (as *ApiService) Deposits(ctx context.Context, params map[string]string, pagination *PaginationParam) (*ApiResponse, error) { 76 | pagination.ReadParam(params) 77 | req := NewRequest(http.MethodGet, "/api/v1/deposits", params) 78 | return as.Call(ctx, req) 79 | } 80 | 81 | // A V1DepositModel represents a v1 deposit record. 82 | type V1DepositModel struct { 83 | Amount string `json:"amount"` 84 | Currency string `json:"currency"` 85 | IsInner bool `json:"isInner"` 86 | WalletTxId string `json:"walletTxId"` 87 | Status string `json:"status"` 88 | CreateAt int64 `json:"createAt"` 89 | } 90 | 91 | // A V1DepositsModel is the set of *V1DepositModel. 92 | type V1DepositsModel []*V1DepositModel 93 | 94 | // V1Deposits returns a list of v1 historical deposits. 95 | func (as *ApiService) V1Deposits(ctx context.Context, params map[string]string, pagination *PaginationParam) (*ApiResponse, error) { 96 | pagination.ReadParam(params) 97 | req := NewRequest(http.MethodGet, "/api/v1/hist-deposits", params) 98 | return as.Call(ctx, req) 99 | } 100 | -------------------------------------------------------------------------------- /withdrawal_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestApiService_Withdrawals(t *testing.T) { 9 | s := NewApiServiceFromEnv() 10 | p := map[string]string{} 11 | pp := &PaginationParam{CurrentPage: 1, PageSize: 10} 12 | rsp, err := s.Withdrawals(context.Background(), p, pp) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | ws := WithdrawalsModel{} 17 | if _, err := rsp.ReadPaginationData(&ws); err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | for _, w := range ws { 22 | t.Log(ToJsonString(w)) 23 | switch { 24 | case w.Id == "": 25 | t.Error("Empty key 'id'") 26 | case w.Address == "": 27 | t.Error("Empty key 'address'") 28 | case w.Currency == "": 29 | t.Error("Empty key 'currency'") 30 | case w.Amount == "": 31 | t.Error("Empty key 'amount'") 32 | case w.Fee == "": 33 | t.Error("Empty key 'fee'") 34 | case w.Status == "": 35 | t.Error("Empty key 'status'") 36 | case w.CreatedAt == 0: 37 | t.Error("Empty key 'createdAt'") 38 | case w.UpdatedAt == 0: 39 | t.Error("Empty key 'updatedAt'") 40 | } 41 | } 42 | } 43 | 44 | func TestApiService_V1Withdrawals(t *testing.T) { 45 | t.SkipNow() 46 | s := NewApiServiceFromEnv() 47 | p := map[string]string{} 48 | pp := &PaginationParam{CurrentPage: 1, PageSize: 10} 49 | rsp, err := s.V1Withdrawals(context.Background(), p, pp) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | ws := V1WithdrawalsModel{} 54 | if _, err := rsp.ReadPaginationData(&ws); err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | for _, w := range ws { 59 | t.Log(ToJsonString(w)) 60 | switch { 61 | case w.Address == "": 62 | t.Error("Empty key 'address'") 63 | case w.Currency == "": 64 | t.Error("Empty key 'currency'") 65 | case w.Amount == "": 66 | t.Error("Empty key 'amount'") 67 | case w.Status == "": 68 | t.Error("Empty key 'status'") 69 | case w.WalletTxId == "": 70 | t.Error("Empty key 'walletTxId'") 71 | case w.CreateAt == 0: 72 | t.Error("Empty key 'createAt'") 73 | } 74 | } 75 | } 76 | 77 | func TestApiService_WithdrawalQuotas(t *testing.T) { 78 | s := NewApiServiceFromEnv() 79 | rsp, err := s.WithdrawalQuotas(context.Background(), "BTC", "") 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | wq := &WithdrawalQuotasModel{} 84 | if err := rsp.ReadData(wq); err != nil { 85 | t.Fatal(err) 86 | } 87 | t.Log(ToJsonString(wq)) 88 | switch { 89 | case wq.Currency == "": 90 | t.Error("Empty key 'currency'") 91 | case wq.AvailableAmount == "": 92 | t.Error("Empty key 'availableAmount'") 93 | case wq.RemainAmount == "": 94 | t.Error("Empty key 'remainAmount'") 95 | case wq.WithdrawMinSize == "": 96 | t.Error("Empty key 'withdrawMinSize'") 97 | case wq.LimitBTCAmount == "": 98 | t.Error("Empty key 'limitBTCAmount'") 99 | case wq.InnerWithdrawMinFee == "": 100 | t.Error("Empty key 'innerWithdrawMinFee'") 101 | case wq.WithdrawMinFee == "": 102 | t.Error("Empty key 'withdrawMinFee'") 103 | case wq.Precision == 0: 104 | t.Error("Empty key 'precision'") 105 | } 106 | } 107 | 108 | func TestApiService_ApplyWithdrawal(t *testing.T) { 109 | t.SkipNow() 110 | 111 | s := NewApiServiceFromEnv() 112 | rsp, err := s.ApplyWithdrawal(context.Background(), "BTC", "xx", "0.01", map[string]string{}) 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | w := &ApplyWithdrawalResultModel{} 117 | if err := rsp.ReadData(w); err != nil { 118 | t.Fatal(err) 119 | } 120 | t.Log(ToJsonString(w)) 121 | switch { 122 | case w.WithdrawalId == "": 123 | t.Error("Empty key 'withdrawalId'") 124 | } 125 | } 126 | 127 | func TestApiService_CancelWithdrawal(t *testing.T) { 128 | t.SkipNow() 129 | 130 | s := NewApiServiceFromEnv() 131 | rsp, err := s.CancelWithdrawal(context.Background(), "xxx") 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | w := &CancelWithdrawalResultModel{} 136 | if err := rsp.ReadData(w); err != nil { 137 | t.Fatal(err) 138 | } 139 | t.Log(ToJsonString(w)) 140 | switch { 141 | case len(w.CancelledWithdrawIds) == 0: 142 | t.Error("Empty key 'cancelledWithdrawIds'") 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /withdrawal.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // A WithdrawalModel represents a withdrawal. 9 | type WithdrawalModel struct { 10 | Chain string `json:"chain"` 11 | Id string `json:"id"` 12 | Address string `json:"address"` 13 | Memo string `json:"memo"` 14 | Currency string `json:"currency"` 15 | Amount string `json:"amount"` 16 | Fee string `json:"fee"` 17 | WalletTxId string `json:"walletTxId"` 18 | IsInner bool `json:"isInner"` 19 | Status string `json:"status"` 20 | Remark string `json:"remark"` 21 | CreatedAt int64 `json:"createdAt"` 22 | UpdatedAt int64 `json:"updatedAt"` 23 | } 24 | 25 | // A WithdrawalsModel is the set of *WithdrawalModel. 26 | type WithdrawalsModel []*WithdrawalModel 27 | 28 | // Withdrawals returns a list of withdrawals. 29 | func (as *ApiService) Withdrawals(ctx context.Context, params map[string]string, pagination *PaginationParam) (*ApiResponse, error) { 30 | pagination.ReadParam(params) 31 | req := NewRequest(http.MethodGet, "/api/v1/withdrawals", params) 32 | return as.Call(ctx, req) 33 | } 34 | 35 | // A V1WithdrawalModel represents a v1 historical withdrawal. 36 | type V1WithdrawalModel struct { 37 | Address string `json:"address"` 38 | Amount string `json:"amount"` 39 | Currency string `json:"currency"` 40 | IsInner bool `json:"isInner"` 41 | WalletTxId string `json:"walletTxId"` 42 | Status string `json:"status"` 43 | CreateAt int64 `json:"createAt"` 44 | } 45 | 46 | // A V1WithdrawalsModel is the set of *V1WithdrawalModel. 47 | type V1WithdrawalsModel []*V1WithdrawalModel 48 | 49 | // V1Withdrawals returns a list of v1 historical withdrawals. 50 | func (as *ApiService) V1Withdrawals(ctx context.Context, params map[string]string, pagination *PaginationParam) (*ApiResponse, error) { 51 | pagination.ReadParam(params) 52 | req := NewRequest(http.MethodGet, "/api/v1/hist-withdrawals", params) 53 | return as.Call(ctx, req) 54 | } 55 | 56 | // A WithdrawalQuotasModel represents the quotas for a currency. 57 | type WithdrawalQuotasModel struct { 58 | Currency string `json:"currency"` 59 | AvailableAmount string `json:"availableAmount"` 60 | RemainAmount string `json:"remainAmount"` 61 | WithdrawMinSize string `json:"withdrawMinSize"` 62 | LimitBTCAmount string `json:"limitBTCAmount"` 63 | InnerWithdrawMinFee string `json:"innerWithdrawMinFee"` 64 | UsedBTCAmount string `json:"usedBTCAmount"` 65 | IsWithdrawEnabled bool `json:"isWithdrawEnabled"` 66 | WithdrawMinFee string `json:"withdrawMinFee"` 67 | Precision uint8 `json:"precision"` 68 | Chain string `json:"chain"` 69 | } 70 | 71 | // WithdrawalQuotas returns the quotas of withdrawal. 72 | func (as *ApiService) WithdrawalQuotas(ctx context.Context, currency, chain string) (*ApiResponse, error) { 73 | params := map[string]string{"currency": currency} 74 | if chain != "" { 75 | params["chain"] = chain 76 | } 77 | req := NewRequest(http.MethodGet, "/api/v1/withdrawals/quotas", params) 78 | return as.Call(ctx, req) 79 | } 80 | 81 | // ApplyWithdrawalResultModel represents the result of ApplyWithdrawal(). 82 | type ApplyWithdrawalResultModel struct { 83 | WithdrawalId string `json:"withdrawalId"` 84 | } 85 | 86 | // ApplyWithdrawal applies a withdrawal. 87 | func (as *ApiService) ApplyWithdrawal(ctx context.Context, currency, address, amount string, options map[string]string) (*ApiResponse, error) { 88 | p := map[string]string{ 89 | "currency": currency, 90 | "address": address, 91 | "amount": amount, 92 | } 93 | for k, v := range options { 94 | p[k] = v 95 | } 96 | req := NewRequest(http.MethodPost, "/api/v1/withdrawals", p) 97 | return as.Call(ctx, req) 98 | } 99 | 100 | // CancelWithdrawalResultModel represents the result of CancelWithdrawal(). 101 | type CancelWithdrawalResultModel struct { 102 | CancelledWithdrawIds []string `json:"cancelledWithdrawIds"` 103 | } 104 | 105 | // CancelWithdrawal cancels a withdrawal by withdrawalId. 106 | func (as *ApiService) CancelWithdrawal(ctx context.Context, withdrawalId string) (*ApiResponse, error) { 107 | req := NewRequest(http.MethodDelete, "/api/v1/withdrawals/"+withdrawalId, nil) 108 | return as.Call(ctx, req) 109 | } 110 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/Kucoin/kucoin-go-sdk" 8 | ) 9 | 10 | func main() { 11 | //s := kucoin.NewApiServiceFromEnv() 12 | s := kucoin.NewApiService( 13 | kucoin.ApiKeyOption("key"), 14 | kucoin.ApiSecretOption("secret"), 15 | kucoin.ApiPassPhraseOption("passphrase"), 16 | ) 17 | 18 | ctx := context.Background() 19 | 20 | serverTime(ctx, s) 21 | accounts(ctx, s) 22 | orders(ctx, s) 23 | publicWebsocket(ctx, s) 24 | privateWebsocket(ctx, s) 25 | } 26 | 27 | func serverTime(ctx context.Context, s *kucoin.ApiService) { 28 | rsp, err := s.ServerTime(ctx) 29 | if err != nil { 30 | log.Printf("Error: %s", err.Error()) 31 | // Handle error 32 | return 33 | } 34 | 35 | var ts int64 36 | if err := rsp.ReadData(&ts); err != nil { 37 | // Handle error 38 | return 39 | } 40 | log.Printf("The server time: %d", ts) 41 | } 42 | 43 | func accounts(ctx context.Context, s *kucoin.ApiService) { 44 | rsp, err := s.Accounts(ctx, "", "") 45 | if err != nil { 46 | // Handle error 47 | return 48 | } 49 | 50 | as := kucoin.AccountsModel{} 51 | if err := rsp.ReadData(&as); err != nil { 52 | // Handle error 53 | return 54 | } 55 | 56 | for _, a := range as { 57 | log.Printf("Available balance: %s %s => %s", a.Type, a.Currency, a.Available) 58 | } 59 | } 60 | 61 | func orders(ctx context.Context, s *kucoin.ApiService) { 62 | rsp, err := s.Orders(ctx, map[string]string{}, &kucoin.PaginationParam{CurrentPage: 1, PageSize: 10}) 63 | if err != nil { 64 | // Handle error 65 | return 66 | } 67 | 68 | os := kucoin.OrdersModel{} 69 | pa, err := rsp.ReadPaginationData(&os) 70 | if err != nil { 71 | // Handle error 72 | return 73 | } 74 | log.Printf("Total num: %d, total page: %d", pa.TotalNum, pa.TotalPage) 75 | for _, o := range os { 76 | log.Printf("Order: %s, %s, %s", o.Id, o.Type, o.Price) 77 | } 78 | } 79 | func publicWebsocket(ctx context.Context, s *kucoin.ApiService) { 80 | rsp, err := s.WebSocketPublicToken(ctx) 81 | if err != nil { 82 | // Handle error 83 | return 84 | } 85 | 86 | tk := &kucoin.WebSocketTokenModel{} 87 | if err := rsp.ReadData(tk); err != nil { 88 | // Handle error 89 | return 90 | } 91 | 92 | c := s.NewWebSocketClient(tk) 93 | 94 | mc, ec, err := c.Connect() 95 | if err != nil { 96 | // Handle error 97 | return 98 | } 99 | 100 | ch1 := kucoin.NewSubscribeMessage("/market/ticker:KCS-BTC", false) 101 | ch2 := kucoin.NewSubscribeMessage("/market/ticker:ETH-BTC", false) 102 | uch := kucoin.NewUnsubscribeMessage("/market/ticker:ETH-BTC", false) 103 | 104 | if err := c.Subscribe(ch1, ch2); err != nil { 105 | // Handle error 106 | return 107 | } 108 | 109 | var i = 0 110 | for { 111 | select { 112 | case err := <-ec: 113 | c.Stop() // Stop subscribing the WebSocket feed 114 | log.Printf("Error: %s", err.Error()) 115 | // Handle error 116 | return 117 | case msg := <-mc: 118 | log.Printf("Received: %s", kucoin.ToJsonString(msg)) 119 | i++ 120 | if i == 5 { 121 | log.Println("Unsubscribe ETH-BTC") 122 | if err = c.Unsubscribe(uch); err != nil { 123 | log.Printf("Error: %s", err.Error()) 124 | // Handle error 125 | return 126 | } 127 | } 128 | if i == 10 { 129 | log.Println("Subscribe ETH-BTC") 130 | if err = c.Subscribe(ch2); err != nil { 131 | log.Printf("Error: %s", err.Error()) 132 | // Handle error 133 | return 134 | } 135 | } 136 | if i == 15 { 137 | log.Println("Exit subscription") 138 | c.Stop() // Stop subscribing the WebSocket feed 139 | return 140 | } 141 | } 142 | } 143 | } 144 | 145 | func privateWebsocket(ctx context.Context, s *kucoin.ApiService) { 146 | rsp, err := s.WebSocketPrivateToken(ctx) 147 | if err != nil { 148 | // Handle error 149 | return 150 | } 151 | 152 | tk := &kucoin.WebSocketTokenModel{} 153 | //tk.AcceptUserMessage = true 154 | if err := rsp.ReadData(tk); err != nil { 155 | // Handle error 156 | return 157 | } 158 | 159 | c := s.NewWebSocketClient(tk) 160 | 161 | mc, ec, err := c.Connect() 162 | if err != nil { 163 | // Handle error 164 | return 165 | } 166 | 167 | ch1 := kucoin.NewSubscribeMessage("/market/level3:BTC-USDT", false) 168 | ch2 := kucoin.NewSubscribeMessage("/account/balance", false) 169 | 170 | if err := c.Subscribe(ch1, ch2); err != nil { 171 | // Handle error 172 | return 173 | } 174 | 175 | var i = 0 176 | for { 177 | select { 178 | case err := <-ec: 179 | c.Stop() // Stop subscribing the WebSocket feed 180 | log.Printf("Error: %s", err.Error()) 181 | // Handle error 182 | return 183 | case msg := <-mc: 184 | log.Printf("Received: %s", kucoin.ToJsonString(msg)) 185 | i++ 186 | if i == 10 { 187 | log.Println("Subscribe ETH-BTC") 188 | if err = c.Subscribe(ch2); err != nil { 189 | log.Printf("Error: %s", err.Error()) 190 | // Handle error 191 | return 192 | } 193 | } 194 | if i == 15 { 195 | log.Println("Exit subscription") 196 | c.Stop() // Stop subscribing the WebSocket feed 197 | return 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /currency.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | ) 8 | 9 | // A CurrencyModel represents a model of known currency. 10 | type CurrencyModel struct { 11 | Name string `json:"name"` 12 | Currency string `json:"currency"` 13 | FullName string `json:"fullName"` 14 | Precision uint8 `json:"precision"` 15 | Confirms int64 `json:"confirms"` 16 | ContractAddress string `json:"contractAddress"` 17 | WithdrawalMinSize string `json:"withdrawalMinSize"` 18 | WithdrawalMinFee string `json:"withdrawalMinFee"` 19 | IsWithdrawEnabled bool `json:"isWithdrawEnabled"` 20 | IsDepositEnabled bool `json:"isDepositEnabled"` 21 | IsMarginEnabled bool `json:"isMarginEnabled"` 22 | IsDebitEnabled bool `json:"isDebitEnabled"` 23 | } 24 | 25 | // A CurrenciesModel is the set of *CurrencyModel. 26 | type CurrenciesModel []*CurrencyModel 27 | 28 | // Currencies returns a list of known currencies. 29 | func (as *ApiService) Currencies(ctx context.Context) (*ApiResponse, error) { 30 | req := NewRequest(http.MethodGet, "/api/v1/currencies", nil) 31 | return as.Call(ctx, req) 32 | } 33 | 34 | // Currency returns the details of the currency. 35 | // Deprecated: Use CurrencyV2 instead. 36 | func (as *ApiService) Currency(ctx context.Context, currency string, chain string) (*ApiResponse, error) { 37 | params := map[string]string{} 38 | if chain != "" { 39 | params["chain"] = chain 40 | } 41 | req := NewRequest(http.MethodGet, "/api/v1/currencies/"+currency, params) 42 | return as.Call(ctx, req) 43 | } 44 | 45 | // ChainsModel Chains Model 46 | type ChainsModel struct { 47 | ChainName string `json:"chainName"` 48 | WithdrawalMinSize string `json:"withdrawalMinSize"` 49 | WithdrawalMinFee string `json:"withdrawalMinFee"` 50 | IsWithdrawEnabled bool `json:"isWithdrawEnabled"` 51 | IsDepositEnabled bool `json:"isDepositEnabled"` 52 | Confirms int64 `json:"confirms"` 53 | ContractAddress string `json:"contractAddress"` 54 | ChainId string `json:"chainId"` 55 | } 56 | 57 | // CurrencyV2Model CurrencyV2 Model 58 | type CurrencyV2Model struct { 59 | Name string `json:"name"` 60 | Currency string `json:"currency"` 61 | FullName string `json:"fullName"` 62 | Precision uint8 `json:"precision"` 63 | Confirms int64 `json:"confirms"` 64 | ContractAddress string `json:"contractAddress"` 65 | IsMarginEnabled bool `json:"isMarginEnabled"` 66 | IsDebitEnabled bool `json:"isDebitEnabled"` 67 | Chains []*ChainsModel `json:"chains"` 68 | } 69 | 70 | // CurrencyV2 returns the details of the currency. 71 | func (as *ApiService) CurrencyV2(ctx context.Context, currency string, chain string) (*ApiResponse, error) { 72 | params := map[string]string{} 73 | if chain != "" { 74 | params["chain"] = chain 75 | } 76 | req := NewRequest(http.MethodGet, "/api/v2/currencies/"+currency, params) 77 | return as.Call(ctx, req) 78 | } 79 | 80 | type PricesModel map[string]string 81 | 82 | // Prices returns the fiat prices for currency. 83 | func (as *ApiService) Prices(ctx context.Context, base, currencies string) (*ApiResponse, error) { 84 | params := map[string]string{} 85 | if base != "" { 86 | params["base"] = base 87 | } 88 | if currencies != "" { 89 | params["currencies"] = currencies 90 | } 91 | req := NewRequest(http.MethodGet, "/api/v1/prices", params) 92 | return as.Call(ctx, req) 93 | } 94 | 95 | type CurrenciesV3Model []*CurrencyV3Model 96 | 97 | type CurrencyV3Model struct { 98 | Currency string `json:"currency"` 99 | Name string `json:"name"` 100 | FullName string `json:"fullName"` 101 | Precision int32 `json:"precision"` 102 | Confirms int32 `json:"confirms"` 103 | ContractAddress string `json:"contractAddress"` 104 | IsMarginEnabled bool `json:"isMarginEnabled"` 105 | IsDebitEnabled bool `json:"isDebitEnabled"` 106 | Chains []struct { 107 | ChainName string `json:"chainName"` 108 | WithdrawalMinFee json.Number `json:"withdrawalMinFee"` 109 | WithdrawalMinSize json.Number `json:"withdrawalMinSize"` 110 | WithdrawFeeRate json.Number `json:"withdrawFeeRate"` 111 | DepositMinSize json.Number `json:"depositMinSize"` 112 | IsWithdrawEnabled bool `json:"isWithdrawEnabled"` 113 | IsDepositEnabled bool `json:"isDepositEnabled"` 114 | PreConfirms int32 `json:"preConfirms"` 115 | ContractAddress string `json:"contractAddress"` 116 | ChainId string `json:"chainId"` 117 | Confirms int32 `json:"confirms"` 118 | } `json:"chains"` 119 | } 120 | 121 | func (as *ApiService) CurrenciesV3(ctx context.Context) (*ApiResponse, error) { 122 | req := NewRequest(http.MethodGet, "/api/v3/currencies/", nil) 123 | return as.Call(ctx, req) 124 | } 125 | 126 | // CurrencyInfoV3 Request via this endpoint to get the currency details of a specified currency 127 | func (as *ApiService) CurrencyInfoV3(ctx context.Context, currency string) (*ApiResponse, error) { 128 | req := NewRequest(http.MethodGet, "/api/v3/currencies/"+currency, nil) 129 | return as.Call(ctx, req) 130 | } 131 | -------------------------------------------------------------------------------- /earn_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestApiService_CreateEarnOrder(t *testing.T) { 9 | 10 | s := NewApiServiceFromEnv() 11 | p := &CreateEarnOrderReq{ 12 | ProductId: "2212", 13 | Amount: "10", 14 | AccountType: "TRADE", 15 | } 16 | rsp, err := s.CreateEarnOrder(context.Background(), p) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | o := &CreateEarnOrderRes{} 21 | if err := rsp.ReadData(o); err != nil { 22 | t.Fatal(err) 23 | } 24 | t.Log(ToJsonString(o)) 25 | } 26 | 27 | func TestApiService_DeleteEarnOrder(t *testing.T) { 28 | 29 | s := NewApiServiceFromEnv() 30 | rsp, err := s.DeleteEarnOrder(context.Background(), "2596986", "10", "TRADE", "1") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | o := &DeleteEarnOrderRes{} 35 | if err := rsp.ReadData(o); err != nil { 36 | t.Fatal(err) 37 | } 38 | t.Log(ToJsonString(o)) 39 | } 40 | 41 | func TestApiService_QueryOTCLoanInfo(t *testing.T) { 42 | 43 | s := NewApiServiceFromEnv() 44 | rsp, err := s.QueryOTCLoanInfo(context.Background()) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | o := &OTCLoanModel{} 49 | if err := rsp.ReadData(o); err != nil { 50 | t.Fatal(err) 51 | } 52 | for _, order := range o.Orders { 53 | t.Log(ToJsonString(order)) 54 | } 55 | 56 | t.Log(ToJsonString(o.Ltv)) 57 | t.Log(ToJsonString(o.ParentUid)) 58 | t.Log(ToJsonString(o.TotalMarginAmount)) 59 | t.Log(ToJsonString(o.TransferMarginAmount)) 60 | for _, margin := range o.Margins { 61 | t.Log(ToJsonString(margin)) 62 | } 63 | } 64 | 65 | func TestApiService_RedeemPreview(t *testing.T) { 66 | 67 | s := NewApiServiceFromEnv() 68 | rsp, err := s.RedeemPreview(context.Background(), "2596986", "TRADE") 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | o := &RedeemPreviewModel{} 74 | if err := rsp.ReadData(&o); err != nil { 75 | t.Fatal(err) 76 | } 77 | t.Log(ToJsonString(o)) 78 | } 79 | 80 | func TestApiService_QuerySavingProducts(t *testing.T) { 81 | s := NewApiServiceFromEnv() 82 | rsp, err := s.QuerySavingProducts(context.Background(), "") 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | os := EarnProductsRes{} 88 | if err := rsp.ReadData(&os); err != nil { 89 | t.Fatal(err) 90 | } 91 | for _, o := range os { 92 | t.Log(ToJsonString(o)) 93 | switch { 94 | case o.Id == "": 95 | t.Error("Empty key 'id'") 96 | case o.Currency == "": 97 | t.Error("Empty key 'Currency'") 98 | case o.Category == "": 99 | t.Error("Empty key 'Category'") 100 | case o.Type == "": 101 | t.Error("Empty key 'Type'") 102 | case o.Status == "": 103 | t.Error("Empty key 'Status'") 104 | } 105 | } 106 | } 107 | 108 | func TestApiService_QueryPromotionProducts(t *testing.T) { 109 | s := NewApiServiceFromEnv() 110 | rsp, err := s.QueryPromotionProducts(context.Background(), "USDT") 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | 115 | os := EarnProductsRes{} 116 | if err := rsp.ReadData(&os); err != nil { 117 | t.Fatal(err) 118 | } 119 | for _, o := range os { 120 | t.Log(ToJsonString(o)) 121 | switch { 122 | case o.Id == "": 123 | t.Error("Empty key 'id'") 124 | case o.Currency == "": 125 | t.Error("Empty key 'Currency'") 126 | case o.Category == "": 127 | t.Error("Empty key 'Category'") 128 | case o.Type == "": 129 | t.Error("Empty key 'Type'") 130 | case o.Status == "": 131 | t.Error("Empty key 'Status'") 132 | } 133 | } 134 | } 135 | 136 | func TestApiService_QueryKCSStakingProducts(t *testing.T) { 137 | s := NewApiServiceFromEnv() 138 | rsp, err := s.QueryKCSStakingProducts(context.Background(), "KCS") 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | 143 | os := EarnProductsRes{} 144 | if err := rsp.ReadData(&os); err != nil { 145 | t.Fatal(err) 146 | } 147 | for _, o := range os { 148 | t.Log(ToJsonString(o)) 149 | switch { 150 | case o.Id == "": 151 | t.Error("Empty key 'id'") 152 | case o.Currency == "": 153 | t.Error("Empty key 'Currency'") 154 | case o.Category == "": 155 | t.Error("Empty key 'Category'") 156 | case o.Type == "": 157 | t.Error("Empty key 'Type'") 158 | case o.Status == "": 159 | t.Error("Empty key 'Status'") 160 | } 161 | } 162 | } 163 | 164 | func TestApiService_QueryStakingProducts(t *testing.T) { 165 | s := NewApiServiceFromEnv() 166 | rsp, err := s.QueryStakingProducts(context.Background(), "") 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | 171 | os := EarnProductsRes{} 172 | if err := rsp.ReadData(&os); err != nil { 173 | t.Fatal(err) 174 | } 175 | for _, o := range os { 176 | t.Log(ToJsonString(o)) 177 | switch { 178 | case o.Id == "": 179 | t.Error("Empty key 'id'") 180 | case o.Currency == "": 181 | t.Error("Empty key 'Currency'") 182 | case o.Category == "": 183 | t.Error("Empty key 'Category'") 184 | case o.Type == "": 185 | t.Error("Empty key 'Type'") 186 | case o.Status == "": 187 | t.Error("Empty key 'Status'") 188 | } 189 | } 190 | } 191 | 192 | func TestApiService_QueryETHProducts(t *testing.T) { 193 | s := NewApiServiceFromEnv() 194 | rsp, err := s.QueryETHProducts(context.Background(), "") 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | 199 | os := EarnProductsRes{} 200 | if err := rsp.ReadData(&os); err != nil { 201 | t.Fatal(err) 202 | } 203 | for _, o := range os { 204 | t.Log(ToJsonString(o)) 205 | switch { 206 | case o.Id == "": 207 | t.Error("Empty key 'id'") 208 | case o.Currency == "": 209 | t.Error("Empty key 'Currency'") 210 | case o.Category == "": 211 | t.Error("Empty key 'Category'") 212 | case o.Type == "": 213 | t.Error("Empty key 'Type'") 214 | case o.Status == "": 215 | t.Error("Empty key 'Status'") 216 | } 217 | } 218 | } 219 | 220 | func TestApiService_QueryHoldAssets(t *testing.T) { 221 | s := NewApiServiceFromEnv() 222 | 223 | p := &PaginationParam{CurrentPage: 1, PageSize: 10} 224 | rsp, err := s.QueryHoldAssets(context.Background(), "", "", "", p) 225 | if err != nil { 226 | t.Fatal(err) 227 | } 228 | 229 | os := HoldAssetsRes{} 230 | if _, err := rsp.ReadPaginationData(&os); err != nil { 231 | t.Fatal(err) 232 | } 233 | if len(os) == 0 { 234 | t.SkipNow() 235 | } 236 | 237 | for _, o := range os { 238 | t.Log(ToJsonString(o)) 239 | } 240 | } 241 | 242 | func TestApiService_QueryOTCLoanAccountsInfo(t *testing.T) { 243 | s := NewApiServiceFromEnv() 244 | 245 | rsp, err := s.QueryOTCLoanAccountsInfo(context.Background()) 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | 250 | os := OTCAccountsModel{} 251 | if err := rsp.ReadData(&os); err != nil { 252 | t.Fatal(err) 253 | } 254 | if len(os) == 0 { 255 | t.SkipNow() 256 | } 257 | 258 | for _, o := range os { 259 | t.Log(ToJsonString(o)) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package kucoin provides two kinds of APIs: `RESTful API` and `WebSocket feed`. 3 | The official document: https://docs.kucoin.com 4 | */ 5 | package kucoin 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "errors" 11 | "fmt" 12 | "log" 13 | "os" 14 | "runtime" 15 | "time" 16 | 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | var ( 21 | // Version is SDK version. 22 | Version = "1.2.10" 23 | // DebugMode will record the logs of API and WebSocket to files in the directory "kucoin.LogDirectory" according to the minimum log level "kucoin.LogLevel". 24 | DebugMode = os.Getenv("API_DEBUG_MODE") == "1" 25 | ) 26 | 27 | func init() { 28 | // Initialize the logging component by default 29 | logrus.SetLevel(logrus.DebugLevel) 30 | if runtime.GOOS == "windows" { 31 | SetLoggerDirectory("tmp") 32 | } else { 33 | SetLoggerDirectory("/tmp") 34 | } 35 | } 36 | 37 | // SetLoggerDirectory sets the directory for logrus output. 38 | func SetLoggerDirectory(directory string) { 39 | var logFile string 40 | if !DebugMode { 41 | logFile = os.DevNull 42 | } else { 43 | logFile = fmt.Sprintf("%s/kucoin-sdk-%s.log", directory, time.Now().Format("2006-01-02")) 44 | } 45 | logWriter, err := os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0664) 46 | if err != nil { 47 | log.Panicf("Open file failed: %s", err.Error()) 48 | } 49 | logrus.SetOutput(logWriter) 50 | } 51 | 52 | // An ApiService provides a HTTP client and a signer to make a HTTP request with the signature to KuCoin API. 53 | type ApiService struct { 54 | apiBaseURI string 55 | apiKey string 56 | apiSecret string 57 | apiPassphrase string 58 | apiSkipVerifyTls bool 59 | requester Requester 60 | signer Signer 61 | apiKeyVersion string 62 | } 63 | 64 | // ProductionApiBaseURI is api base uri for production. 65 | const ProductionApiBaseURI = "https://api.kucoin.com" 66 | 67 | /** 68 | Note: about api key version 69 | To reinforce the security of the API, KuCoin upgraded the API key to version 2.0, the validation logic has also been changed. It is recommended to create(https://www.kucoin.com/account/api) and update your API key to version 2.0. The API key of version 1.0 will be still valid until May 1, 2021. 70 | */ 71 | 72 | // ApiKeyVersionV1 is v1 api key version 73 | const ApiKeyVersionV1 = "1" 74 | 75 | // ApiKeyVersionV2 is v2 api key version 76 | const ApiKeyVersionV2 = "2" 77 | 78 | // An ApiServiceOption is a option parameter to create the instance of ApiService. 79 | type ApiServiceOption func(service *ApiService) 80 | 81 | // ApiBaseURIOption creates a instance of ApiServiceOption about apiBaseURI. 82 | func ApiBaseURIOption(uri string) ApiServiceOption { 83 | return func(service *ApiService) { 84 | service.apiBaseURI = uri 85 | } 86 | } 87 | 88 | // ApiKeyOption creates a instance of ApiServiceOption about apiKey. 89 | func ApiKeyOption(key string) ApiServiceOption { 90 | return func(service *ApiService) { 91 | service.apiKey = key 92 | } 93 | } 94 | 95 | // ApiSecretOption creates a instance of ApiServiceOption about apiSecret. 96 | func ApiSecretOption(secret string) ApiServiceOption { 97 | return func(service *ApiService) { 98 | service.apiSecret = secret 99 | } 100 | } 101 | 102 | // ApiPassPhraseOption creates a instance of ApiServiceOption about apiPassPhrase. 103 | func ApiPassPhraseOption(passPhrase string) ApiServiceOption { 104 | return func(service *ApiService) { 105 | service.apiPassphrase = passPhrase 106 | } 107 | } 108 | 109 | // ApiSkipVerifyTlsOption creates a instance of ApiServiceOption about apiSkipVerifyTls. 110 | func ApiSkipVerifyTlsOption(skipVerifyTls bool) ApiServiceOption { 111 | return func(service *ApiService) { 112 | service.apiSkipVerifyTls = skipVerifyTls 113 | } 114 | } 115 | 116 | // ApiRequesterOption creates a instance of ApiServiceOption about requester. 117 | func ApiRequesterOption(requester Requester) ApiServiceOption { 118 | return func(service *ApiService) { 119 | service.requester = requester 120 | } 121 | } 122 | 123 | // ApiKeyVersionOption creates a instance of ApiServiceOption about apiKeyVersion. 124 | func ApiKeyVersionOption(apiKeyVersion string) ApiServiceOption { 125 | return func(service *ApiService) { 126 | service.apiKeyVersion = apiKeyVersion 127 | } 128 | } 129 | 130 | // NewApiService creates a instance of ApiService by passing ApiServiceOptions, then you can call methods. 131 | func NewApiService(opts ...ApiServiceOption) *ApiService { 132 | as := &ApiService{requester: &BasicRequester{}} 133 | for _, opt := range opts { 134 | opt(as) 135 | } 136 | if as.apiBaseURI == "" { 137 | as.apiBaseURI = ProductionApiBaseURI 138 | } 139 | 140 | if as.apiKeyVersion == "" { 141 | as.apiKeyVersion = ApiKeyVersionV1 142 | } 143 | 144 | if as.apiKey != "" { 145 | if as.apiKeyVersion == ApiKeyVersionV1 { 146 | as.signer = NewKcSigner(as.apiKey, as.apiSecret, as.apiPassphrase) 147 | } else { 148 | as.signer = NewKcSignerV2(as.apiKey, as.apiSecret, as.apiPassphrase) 149 | } 150 | } 151 | 152 | return as 153 | } 154 | 155 | // NewApiServiceFromEnv creates a instance of ApiService by environmental variables such as `API_BASE_URI` `API_KEY` `API_SECRET` `API_PASSPHRASE`, then you can call the methods of ApiService. 156 | func NewApiServiceFromEnv() *ApiService { 157 | return NewApiService( 158 | ApiBaseURIOption(os.Getenv("API_BASE_URI")), 159 | ApiKeyOption(os.Getenv("API_KEY")), 160 | ApiSecretOption(os.Getenv("API_SECRET")), 161 | ApiPassPhraseOption(os.Getenv("API_PASSPHRASE")), 162 | ApiSkipVerifyTlsOption(os.Getenv("API_SKIP_VERIFY_TLS") == "1"), 163 | ApiKeyVersionOption(os.Getenv("API_KEY_VERSION")), 164 | ) 165 | } 166 | 167 | // Call calls the API by passing *Request and returns *ApiResponse. 168 | func (as *ApiService) Call(ctx context.Context, request *Request) (*ApiResponse, error) { 169 | defer func() { 170 | if err := recover(); err != nil { 171 | log.Println("[[Recovery] panic recovered:", err) 172 | } 173 | }() 174 | 175 | request.BaseURI = as.apiBaseURI 176 | request.SkipVerifyTls = as.apiSkipVerifyTls 177 | request.Header.Set("Content-Type", "application/json") 178 | request.Header.Set("User-Agent", "KuCoin-Go-SDK/"+Version) 179 | if as.signer != nil { 180 | var b bytes.Buffer 181 | b.WriteString(request.Method) 182 | b.WriteString(request.RequestURI()) 183 | b.Write(request.Body) 184 | h := as.signer.(*KcSigner).Headers(b.String()) 185 | for k, v := range h { 186 | request.Header.Set(k, v) 187 | } 188 | } 189 | 190 | rsp, err := as.requester.Request(ctx, request, request.Timeout) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | ar := &ApiResponse{response: rsp} 196 | if err := rsp.ReadJsonBody(ar); err != nil { 197 | rb, _ := rsp.ReadBody() 198 | m := fmt.Sprintf("[Parse]Failure: parse JSON body failed because %s, %s %s with body=%s, respond code=%d body=%s", 199 | err.Error(), 200 | rsp.request.Method, 201 | rsp.request.RequestURI(), 202 | string(rsp.request.Body), 203 | rsp.StatusCode, 204 | string(rb), 205 | ) 206 | return ar, errors.New(m) 207 | } 208 | return ar, nil 209 | } 210 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "net/http/httputil" 14 | "net/url" 15 | "strings" 16 | "time" 17 | 18 | "github.com/sirupsen/logrus" 19 | ) 20 | 21 | // A Request represents a HTTP request. 22 | type Request struct { 23 | fullURL string 24 | requestURI string 25 | BaseURI string 26 | Method string 27 | Path string 28 | Query url.Values 29 | Body []byte 30 | Header http.Header 31 | Timeout time.Duration 32 | SkipVerifyTls bool 33 | } 34 | 35 | // NewRequest creates a instance of Request. 36 | func NewRequest(method, path string, params interface{}) *Request { 37 | r := &Request{ 38 | Method: method, 39 | Path: path, 40 | Query: make(url.Values), 41 | Header: make(http.Header), 42 | Body: []byte{}, 43 | Timeout: 30 * time.Second, 44 | } 45 | if r.Path == "" { 46 | r.Path = "/" 47 | } 48 | if r.Method == "" { 49 | r.Method = http.MethodGet 50 | } 51 | r.addParams(params) 52 | return r 53 | } 54 | 55 | func (r *Request) addParams(p interface{}) { 56 | if p == nil { 57 | return 58 | } 59 | 60 | switch r.Method { 61 | case http.MethodGet, http.MethodDelete: 62 | if v, ok := p.(url.Values); ok { 63 | r.Query = v 64 | return 65 | } 66 | for key, value := range p.(map[string]string) { 67 | r.Query.Add(key, value) 68 | } 69 | default: 70 | b, err := json.Marshal(p) 71 | if err != nil { 72 | log.Panic("Cannot marshal params to JSON string:", err.Error()) 73 | } 74 | r.Body = b 75 | } 76 | } 77 | 78 | // RequestURI returns the request uri. 79 | func (r *Request) RequestURI() string { 80 | if r.requestURI != "" { 81 | return r.requestURI 82 | } 83 | 84 | fu := r.FullURL() 85 | u, err := url.Parse(fu) 86 | if err != nil { 87 | r.requestURI = "/" 88 | } else { 89 | r.requestURI = u.RequestURI() 90 | } 91 | return r.requestURI 92 | } 93 | 94 | // FullURL returns the full url. 95 | func (r *Request) FullURL() string { 96 | if r.fullURL != "" { 97 | return r.fullURL 98 | } 99 | r.fullURL = fmt.Sprintf("%s%s", r.BaseURI, r.Path) 100 | if len(r.Query) > 0 { 101 | if strings.Contains(r.fullURL, "?") { 102 | r.fullURL += "&" + r.Query.Encode() 103 | } else { 104 | r.fullURL += "?" + r.Query.Encode() 105 | } 106 | } 107 | return r.fullURL 108 | } 109 | 110 | // HttpRequest creates a instance of *http.Request. 111 | func (r *Request) HttpRequest() (*http.Request, error) { 112 | req, err := http.NewRequest(r.Method, r.FullURL(), bytes.NewBuffer(r.Body)) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | for key, values := range r.Header { 118 | for _, value := range values { 119 | req.Header.Add(key, value) 120 | } 121 | } 122 | 123 | return req, nil 124 | } 125 | 126 | // Requester contains Request() method, can launch a http request. 127 | type Requester interface { 128 | Request(ctx context.Context, request *Request, timeout time.Duration) (*Response, error) 129 | } 130 | 131 | // A BasicRequester represents a basic implement of Requester by http.Client. 132 | type BasicRequester struct { 133 | } 134 | 135 | // Request makes a http request. 136 | func (br *BasicRequester) Request(ctx context.Context, request *Request, timeout time.Duration) (*Response, error) { 137 | tr := http.DefaultTransport 138 | tc := tr.(*http.Transport).TLSClientConfig 139 | if tc == nil { 140 | tc = &tls.Config{InsecureSkipVerify: request.SkipVerifyTls} 141 | } else { 142 | tc.InsecureSkipVerify = request.SkipVerifyTls 143 | } 144 | 145 | cli := http.DefaultClient 146 | cli.Transport, cli.Timeout = tr, timeout 147 | 148 | req, err := request.HttpRequest() 149 | if err != nil { 150 | return nil, err 151 | } 152 | // Prevent re-use of TCP connections 153 | // req.Close = true 154 | 155 | rid := time.Now().UnixNano() 156 | 157 | if DebugMode { 158 | dump, _ := httputil.DumpRequest(req, true) 159 | logrus.Debugf("Sent a HTTP request#%d: %s", rid, string(dump)) 160 | } 161 | 162 | rsp, err := cli.Do(req.WithContext(ctx)) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | if DebugMode { 168 | dump, _ := httputil.DumpResponse(rsp, true) 169 | logrus.Debugf("Received a HTTP response#%d: %s", rid, string(dump)) 170 | } 171 | 172 | return NewResponse( 173 | request, 174 | rsp, 175 | nil, 176 | ), nil 177 | } 178 | 179 | // A Response represents a HTTP response. 180 | type Response struct { 181 | request *Request 182 | *http.Response 183 | body []byte 184 | } 185 | 186 | // NewResponse Creates a new Response 187 | func NewResponse( 188 | request *Request, 189 | response *http.Response, 190 | body []byte, 191 | ) *Response { 192 | return &Response{ 193 | request: request, 194 | Response: response, 195 | body: body, 196 | } 197 | } 198 | 199 | // ReadBody read the response data, then return it. 200 | func (r *Response) ReadBody() ([]byte, error) { 201 | if r.body != nil { 202 | return r.body, nil 203 | } 204 | 205 | r.body = make([]byte, 0) 206 | defer r.Body.Close() 207 | b, err := ioutil.ReadAll(r.Body) 208 | if err != nil { 209 | return nil, err 210 | } 211 | r.body = b 212 | return r.body, nil 213 | } 214 | 215 | // ReadJsonBody read the response data as JSON into v. 216 | func (r *Response) ReadJsonBody(v interface{}) error { 217 | b, err := r.ReadBody() 218 | if err != nil { 219 | return err 220 | } 221 | return json.Unmarshal(b, v) 222 | } 223 | 224 | // The predefined API codes 225 | const ( 226 | ApiSuccess = "200000" 227 | ) 228 | 229 | // An ApiResponse represents a API response wrapped Response. 230 | type ApiResponse struct { 231 | response *Response 232 | Code string `json:"code"` 233 | RawData json.RawMessage `json:"data"` // delay parsing 234 | Message string `json:"msg"` 235 | } 236 | 237 | // HttpSuccessful judges the success of http. 238 | func (ar *ApiResponse) HttpSuccessful() bool { 239 | return ar.response.StatusCode == http.StatusOK 240 | } 241 | 242 | // ApiSuccessful judges the success of API. 243 | func (ar *ApiResponse) ApiSuccessful() bool { 244 | return ar.Code == ApiSuccess 245 | } 246 | 247 | // ReadData read the api response `data` as JSON into v. 248 | func (ar *ApiResponse) ReadData(v interface{}) error { 249 | if !ar.HttpSuccessful() { 250 | rsb, _ := ar.response.ReadBody() 251 | m := fmt.Sprintf("[HTTP]Failure: status code is NOT 200, %s %s with body=%s, respond code=%d body=%s", 252 | ar.response.request.Method, 253 | ar.response.request.RequestURI(), 254 | string(ar.response.request.Body), 255 | ar.response.StatusCode, 256 | string(rsb), 257 | ) 258 | return errors.New(m) 259 | } 260 | 261 | if !ar.ApiSuccessful() { 262 | m := fmt.Sprintf("[API]Failure: api code is NOT %s, %s %s with body=%s, respond code=%s message=\"%s\" data=%s", 263 | ApiSuccess, 264 | ar.response.request.Method, 265 | ar.response.request.RequestURI(), 266 | string(ar.response.request.Body), 267 | ar.Code, 268 | ar.Message, 269 | string(ar.RawData), 270 | ) 271 | return errors.New(m) 272 | } 273 | // when input parameter v is nil, read nothing and return nil 274 | if v == nil { 275 | return nil 276 | } 277 | 278 | if len(ar.RawData) == 0 { 279 | m := fmt.Sprintf("[API]Failure: try to read empty data, %s %s with body=%s, respond code=%s message=\"%s\" data=%s", 280 | ar.response.request.Method, 281 | ar.response.request.RequestURI(), 282 | string(ar.response.request.Body), 283 | ar.Code, 284 | ar.Message, 285 | string(ar.RawData), 286 | ) 287 | return errors.New(m) 288 | } 289 | 290 | return json.Unmarshal(ar.RawData, v) 291 | } 292 | 293 | // ReadPaginationData read the data `items` as JSON into v, and returns *PaginationModel. 294 | func (ar *ApiResponse) ReadPaginationData(v interface{}) (*PaginationModel, error) { 295 | p := &PaginationModel{} 296 | if err := ar.ReadData(p); err != nil { 297 | return nil, err 298 | } 299 | if err := p.ReadItems(v); err != nil { 300 | return p, err 301 | } 302 | return p, nil 303 | } 304 | -------------------------------------------------------------------------------- /earn.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | ) 8 | 9 | type CreateEarnOrderReq struct { 10 | ProductId string `json:"productId"` 11 | Amount string `json:"amount"` 12 | AccountType string `json:"accountType"` 13 | } 14 | 15 | type CreateEarnOrderRes struct { 16 | OrderId string `json:"orderId"` 17 | OrderTxId string `json:"orderTxId"` 18 | } 19 | 20 | // CreateEarnOrder subscribing to fixed income products. If the subscription fails, it returns the corresponding error code. 21 | func (as *ApiService) CreateEarnOrder(ctx context.Context, createEarnOrderReq *CreateEarnOrderReq) (*ApiResponse, error) { 22 | req := NewRequest(http.MethodPost, "/api/v1/earn/orders", createEarnOrderReq) 23 | return as.Call(ctx, req) 24 | } 25 | 26 | type DeleteEarnOrderRes struct { 27 | OrderTxId string `json:"orderTxId"` 28 | DeliverTime int64 `json:"deliverTime"` 29 | Status string `json:"status"` 30 | Amount string `json:"amount"` 31 | } 32 | 33 | // DeleteEarnOrder initiating redemption by holding ID. 34 | func (as *ApiService) DeleteEarnOrder(ctx context.Context, orderId, amount, fromAccountType, confirmPunishRedeem string) (*ApiResponse, error) { 35 | p := map[string]string{ 36 | "orderId": orderId, 37 | "fromAccountType": fromAccountType, 38 | "confirmPunishRedeem": confirmPunishRedeem, 39 | "amount": amount, 40 | } 41 | req := NewRequest(http.MethodDelete, "/api/v1/earn/orders", p) 42 | return as.Call(ctx, req) 43 | } 44 | 45 | type RedeemPreviewModel struct { 46 | Currency string `json:"currency"` 47 | RedeemAmount json.Number `json:"redeemAmount"` 48 | PenaltyInterestAmount json.Number `json:"penaltyInterestAmount"` 49 | RedeemPeriod int32 `json:"redeemPeriod"` 50 | DeliverTime int64 `json:"deliverTime"` 51 | ManualRedeemable bool `json:"manualRedeemable"` 52 | RedeemAll bool `json:"redeemAll"` 53 | } 54 | 55 | // RedeemPreview retrieves redemption preview information by holding ID 56 | func (as *ApiService) RedeemPreview(ctx context.Context, orderId, fromAccountType string) (*ApiResponse, error) { 57 | p := map[string]string{ 58 | "orderId": orderId, 59 | "fromAccountType": fromAccountType, 60 | } 61 | req := NewRequest(http.MethodGet, "/api/v1/earn/redeem-preview", p) 62 | return as.Call(ctx, req) 63 | } 64 | 65 | // QuerySavingProducts retrieves savings products. 66 | func (as *ApiService) QuerySavingProducts(ctx context.Context, currency string) (*ApiResponse, error) { 67 | p := map[string]string{ 68 | "currency": currency, 69 | } 70 | req := NewRequest(http.MethodGet, "/api/v1/earn/saving/products", p) 71 | return as.Call(ctx, req) 72 | } 73 | 74 | type HoldAssetsRes []*HoldAssetModel 75 | 76 | type HoldAssetModel struct { 77 | OrderId string `json:"orderId"` 78 | ProductId string `json:"productId"` 79 | ProductCategory string `json:"productCategory"` 80 | ProductType string `json:"productType"` 81 | Currency string `json:"currency"` 82 | IncomeCurrency string `json:"incomeCurrency"` 83 | ReturnRate json.Number `json:"returnRate"` 84 | HoldAmount json.Number `json:"holdAmount"` 85 | RedeemedAmount json.Number `json:"redeemedAmount"` 86 | RedeemingAmount json.Number `json:"redeemingAmount"` 87 | LockStartTime int64 `json:"lockStartTime"` 88 | LockEndTime int64 `json:"lockEndTime"` 89 | PurchaseTime int64 `json:"purchaseTime"` 90 | RedeemPeriod int32 `json:"redeemPeriod"` 91 | Status string `json:"status"` 92 | EarlyRedeemSupported int32 `json:"earlyRedeemSupported"` 93 | } 94 | 95 | // QueryHoldAssets retrieves current holding assets of fixed income products 96 | func (as *ApiService) QueryHoldAssets(ctx context.Context, productId, productCategory, currency string, pagination *PaginationParam) (*ApiResponse, error) { 97 | p := map[string]string{ 98 | "productId": productId, 99 | "productCategory": productCategory, 100 | "currency": currency, 101 | } 102 | pagination.ReadParam(p) 103 | req := NewRequest(http.MethodGet, "/api/v1/earn/hold-assets", p) 104 | return as.Call(ctx, req) 105 | } 106 | 107 | type EarnProductsRes []*EarnProductModel 108 | 109 | type EarnProductModel struct { 110 | Id string `json:"id"` 111 | Currency string `json:"currency"` 112 | Category string `json:"category"` 113 | Type string `json:"type"` 114 | Precision int32 `json:"precision"` 115 | ProductUpperLimit string `json:"productUpperLimit"` 116 | UserUpperLimit string `json:"userUpperLimit"` 117 | UserLowerLimit string `json:"userLowerLimit"` 118 | RedeemPeriod int `json:"redeemPeriod"` 119 | LockStartTime int64 `json:"lockStartTime"` 120 | LockEndTime int64 `json:"lockEndTime"` 121 | ApplyStartTime int64 `json:"applyStartTime"` 122 | ApplyEndTime int64 `json:"applyEndTime"` 123 | ReturnRate json.Number `json:"returnRate"` 124 | IncomeCurrency string `json:"incomeCurrency"` 125 | EarlyRedeemSupported int32 `json:"earlyRedeemSupported"` 126 | ProductRemainAmount json.Number `json:"productRemainAmount"` 127 | Status string `json:"status"` 128 | RedeemType string `json:"redeemType"` 129 | IncomeReleaseType string `json:"incomeReleaseType"` 130 | InterestDate int64 `json:"interestDate"` 131 | Duration int32 `json:"duration"` 132 | NewUserOnly int32 `json:"newUserOnly"` 133 | } 134 | 135 | // QueryPromotionProducts retrieves limited-time promotion products 136 | func (as *ApiService) QueryPromotionProducts(ctx context.Context, currency string) (*ApiResponse, error) { 137 | p := map[string]string{ 138 | "currency": currency, 139 | } 140 | req := NewRequest(http.MethodGet, "/api/v1/earn/promotion/products", p) 141 | return as.Call(ctx, req) 142 | } 143 | 144 | // QueryKCSStakingProducts retrieves KCS Staking products 145 | func (as *ApiService) QueryKCSStakingProducts(ctx context.Context, currency string) (*ApiResponse, error) { 146 | p := map[string]string{ 147 | "currency": currency, 148 | } 149 | req := NewRequest(http.MethodGet, "/api/v1/earn/kcs-staking/products", p) 150 | return as.Call(ctx, req) 151 | } 152 | 153 | // QueryStakingProducts retrieves Staking products 154 | func (as *ApiService) QueryStakingProducts(ctx context.Context, currency string) (*ApiResponse, error) { 155 | p := map[string]string{ 156 | "currency": currency, 157 | } 158 | req := NewRequest(http.MethodGet, "/api/v1/earn/staking/products", p) 159 | return as.Call(ctx, req) 160 | } 161 | 162 | // QueryETHProducts retrieves ETH Staking products 163 | func (as *ApiService) QueryETHProducts(ctx context.Context, currency string) (*ApiResponse, error) { 164 | p := map[string]string{ 165 | "currency": currency, 166 | } 167 | req := NewRequest(http.MethodGet, "/api/v1/earn/eth-staking/products", p) 168 | return as.Call(ctx, req) 169 | } 170 | 171 | type OTCLoanModel struct { 172 | ParentUid string `json:"parentUid"` 173 | Orders []struct { 174 | OrderId string `json:"orderId"` 175 | Currency string `json:"currency"` 176 | Principal json.Number `json:"principal"` 177 | Interest json.Number `json:"interest"` 178 | } `json:"orders"` 179 | Ltv struct { 180 | TransferLtv json.Number `json:"transferLtv"` 181 | OnlyClosePosLtv json.Number `json:"onlyClosePosLtv"` 182 | DelayedLiquidationLtv json.Number `json:"delayedLiquidationLtv"` 183 | InstantLiquidationLtv json.Number `json:"instantLiquidationLtv"` 184 | CurrentLtv json.Number `json:"currentLtv"` 185 | } `json:"ltv"` 186 | TotalMarginAmount json.Number `json:"totalMarginAmount"` 187 | TransferMarginAmount json.Number `json:"transferMarginAmount"` 188 | Margins []struct { 189 | MarginCcy string `json:"marginCcy"` 190 | MarginQty json.Number `json:"marginQty"` 191 | MarginFactor json.Number `json:"marginFactor"` 192 | } `json:"margins"` 193 | } 194 | 195 | // QueryOTCLoanInfo querying accounts that are currently involved in loans. 196 | func (as *ApiService) QueryOTCLoanInfo(ctx context.Context) (*ApiResponse, error) { 197 | req := NewRequest(http.MethodGet, "/api/v1/otc-loan/loan", nil) 198 | return as.Call(ctx, req) 199 | } 200 | 201 | type OTCAccountsModel []*OTCAccountModel 202 | 203 | type OTCAccountModel struct { 204 | Uid string `json:"uid"` 205 | MarginCcy string `json:"marginCcy"` 206 | MarginQty string `json:"marginQty"` 207 | MarginFactor string `json:"marginFactor"` 208 | AccountType string `json:"accountType"` 209 | IsParent bool `json:"isParent"` 210 | } 211 | 212 | // QueryOTCLoanAccountsInfo querying accounts that are currently involved in off-exchange funding and loans. 213 | func (as *ApiService) QueryOTCLoanAccountsInfo(ctx context.Context) (*ApiResponse, error) { 214 | req := NewRequest(http.MethodGet, "/api/v1/otc-loan/accounts", nil) 215 | return as.Call(ctx, req) 216 | } 217 | -------------------------------------------------------------------------------- /symbol_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestApiService_Symbols(t *testing.T) { 10 | s := NewApiServiceFromEnv() 11 | rsp, err := s.Symbols(context.Background(), "") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | l := SymbolsModel{} 16 | if err := rsp.ReadData(&l); err != nil { 17 | t.Fatal(err) 18 | } 19 | for _, c := range l { 20 | t.Log(ToJsonString(c)) 21 | switch { 22 | case c.Name == "": 23 | t.Error("Empty key 'name'") 24 | case c.Symbol == "": 25 | t.Error("Empty key 'symbol'") 26 | case c.BaseCurrency == "": 27 | t.Error("Empty key 'baseCurrency'") 28 | case c.QuoteCurrency == "": 29 | t.Error("Empty key 'quoteCurrency'") 30 | case c.BaseMinSize == "": 31 | t.Error("Empty key 'baseMinSize'") 32 | case c.QuoteMinSize == "": 33 | t.Error("Empty key 'quoteMinSize'") 34 | case c.BaseMaxSize == "": 35 | t.Error("Empty key 'baseMaxSize'") 36 | case c.QuoteMaxSize == "": 37 | t.Error("Empty key 'quoteMaxSize'") 38 | case c.BaseIncrement == "": 39 | t.Error("Empty key 'baseIncrement'") 40 | case c.QuoteIncrement == "": 41 | t.Error("Empty key 'quoteIncrement'") 42 | case c.FeeCurrency == "": 43 | t.Error("Empty key 'feeCurrency'") 44 | case c.PriceIncrement == "": 45 | t.Error("Empty key 'priceIncrement'") 46 | } 47 | } 48 | } 49 | 50 | func TestApiService_TickerLevel1(t *testing.T) { 51 | s := NewApiServiceFromEnv() 52 | rsp, err := s.TickerLevel1(context.Background(), "ETH-BTC") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | tk := &TickerLevel1Model{} 57 | if err := rsp.ReadData(tk); err != nil { 58 | t.Fatal(err) 59 | } 60 | t.Log(ToJsonString(tk)) 61 | switch { 62 | case tk.Sequence == "": 63 | t.Error("Empty key 'sequence'") 64 | case tk.Price == "": 65 | t.Error("Empty key 'price'") 66 | case tk.Size == "": 67 | t.Error("Empty key 'size'") 68 | case tk.BestBid == "": 69 | t.Error("Empty key 'bestBid'") 70 | case tk.BestBidSize == "": 71 | t.Error("Empty key 'bestBidSize'") 72 | case tk.BestAsk == "": 73 | t.Error("Empty key 'bestAsk'") 74 | case tk.BestAskSize == "": 75 | t.Error("Empty key 'bestAskSize'") 76 | } 77 | } 78 | 79 | func TestApiService_Tickers(t *testing.T) { 80 | s := NewApiServiceFromEnv() 81 | rsp, err := s.Tickers(context.Background()) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | ts := &TickersResponseModel{} 86 | if err := rsp.ReadData(ts); err != nil { 87 | t.Fatal(err) 88 | } 89 | if ts.Time == 0 { 90 | t.Error("Empty key 'time'") 91 | } 92 | for _, tk := range ts.Tickers { 93 | switch { 94 | case tk.Symbol == "": 95 | t.Error("Empty key 'symbol'") 96 | case tk.Vol == "": 97 | t.Error("Empty key 'vol'") 98 | case tk.ChangeRate == "": 99 | t.Error("Empty key 'changeRate'") 100 | //case tk.Buy == "": 101 | // t.Error("Empty key 'buy'") 102 | //case tk.Sell == "": 103 | // t.Error("Empty key 'sell'") 104 | //case tk.Last == "": 105 | // t.Error("Empty key 'last'") 106 | } 107 | } 108 | } 109 | 110 | func TestApiService_Stats24hr(t *testing.T) { 111 | s := NewApiServiceFromEnv() 112 | rsp, err := s.Stats24hr(context.Background(), "ETH-BTC") 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | st := &Stats24hrModel{} 117 | if err := rsp.ReadData(st); err != nil { 118 | t.Fatal(err) 119 | } 120 | t.Log(ToJsonString(st)) 121 | switch { 122 | case st.Symbol == "": 123 | t.Error("Empty key 'symbol'") 124 | case st.ChangeRate == "": 125 | t.Error("Empty key 'changRate'") 126 | } 127 | } 128 | 129 | func TestApiService_Markets(t *testing.T) { 130 | s := NewApiServiceFromEnv() 131 | rsp, err := s.Markets(context.Background()) 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | ms := MarketsModel{} 136 | 137 | if err := rsp.ReadData(&ms); err != nil { 138 | t.Fatal(err) 139 | } 140 | t.Log(ToJsonString(ms)) 141 | if len(ms) == 0 { 142 | t.Error("Empty markets") 143 | } 144 | } 145 | 146 | func TestApiService_AggregatedPartOrderBook(t *testing.T) { 147 | s := NewApiServiceFromEnv() 148 | rsp, err := s.AggregatedPartOrderBook(context.Background(), "ETH-BTC", 100) 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | c := &PartOrderBookModel{} 153 | if err := rsp.ReadData(c); err != nil { 154 | t.Fatal(err) 155 | } 156 | t.Log(ToJsonString(c)) 157 | switch { 158 | case c.Sequence == "": 159 | t.Error("Empty key 'sequence'") 160 | case len(c.Asks) == 0: 161 | t.Error("Empty key 'asks'") 162 | case len(c.Asks[0]) != 2: 163 | t.Error("Invalid ask length") 164 | case len(c.Bids) == 0: 165 | t.Error("Empty key 'bids'") 166 | case len(c.Bids[0]) != 2: 167 | t.Error("Invalid bid length") 168 | } 169 | } 170 | 171 | func TestApiService_AggregatedFullOrderBook(t *testing.T) { 172 | s := NewApiServiceFromEnv() 173 | rsp, err := s.AggregatedFullOrderBook(context.Background(), "ETH-BTC") 174 | if err != nil { 175 | t.Fatal(err) 176 | } 177 | c := &FullOrderBookModel{} 178 | if err := rsp.ReadData(c); err != nil { 179 | t.Fatal(err) 180 | } 181 | t.Log(ToJsonString(c)) 182 | switch { 183 | case c.Sequence == "": 184 | t.Error("Empty key 'sequence'") 185 | case len(c.Asks) == 0: 186 | t.Error("Empty key 'asks'") 187 | case len(c.Asks[0]) != 2: 188 | t.Error("Invalid ask length") 189 | case len(c.Bids) == 0: 190 | t.Error("Empty key 'bids'") 191 | case len(c.Bids[0]) != 2: 192 | t.Error("Invalid bid length") 193 | } 194 | } 195 | func TestApiService_AggregatedFullOrderBookV3(t *testing.T) { 196 | s := NewApiServiceFromEnv() 197 | rsp, err := s.AggregatedFullOrderBookV3(context.Background(), "BTC-USDT") 198 | if err != nil { 199 | t.Fatal(err) 200 | } 201 | c := &FullOrderBookModel{} 202 | if err := rsp.ReadData(c); err != nil { 203 | t.Fatal(err) 204 | } 205 | t.Log(ToJsonString(c)) 206 | switch { 207 | case c.Sequence == "": 208 | t.Error("Empty key 'sequence'") 209 | case len(c.Asks) == 0: 210 | t.Error("Empty key 'asks'") 211 | case len(c.Asks[0]) != 2: 212 | t.Error("Invalid ask length") 213 | case len(c.Bids) == 0: 214 | t.Error("Empty key 'bids'") 215 | case len(c.Bids[0]) != 2: 216 | t.Error("Invalid bid length") 217 | } 218 | } 219 | 220 | func TestApiService_AtomicFullOrderBook(t *testing.T) { 221 | s := NewApiServiceFromEnv() 222 | rsp, err := s.AtomicFullOrderBook(context.Background(), "ETH-BTC") 223 | if err != nil { 224 | t.Fatal(err) 225 | } 226 | c := &FullOrderBookModel{} 227 | if err := rsp.ReadData(c); err != nil { 228 | t.Fatal(err) 229 | } 230 | t.Log(ToJsonString(c)) 231 | switch { 232 | case c.Sequence == "": 233 | t.Error("Empty key 'sequence'") 234 | case len(c.Asks) == 0: 235 | t.Error("Empty key 'asks'") 236 | case len(c.Asks[0]) != 4: 237 | t.Error("Invalid ask length") 238 | case len(c.Bids) == 0: 239 | t.Error("Empty key 'bids'") 240 | case len(c.Bids[0]) != 4: 241 | t.Error("Invalid bid length") 242 | } 243 | } 244 | 245 | func TestApiService_AtomicFullOrderBookV2(t *testing.T) { 246 | s := NewApiServiceFromEnv() 247 | rsp, err := s.AtomicFullOrderBookV2(context.Background(), "ETH-BTC") 248 | if err != nil { 249 | t.Fatal(err) 250 | } 251 | c := &FullOrderBookV2Model{} 252 | if err := rsp.ReadData(c); err != nil { 253 | t.Fatal(err) 254 | } 255 | t.Log(ToJsonString(c)) 256 | switch { 257 | case c.Sequence == 0: 258 | t.Error("Empty key 'sequence'") 259 | case len(c.Asks) == 0: 260 | t.Error("Empty key 'asks'") 261 | case len(c.Asks[0]) != 4: 262 | t.Error("Invalid ask length") 263 | case len(c.Bids) == 0: 264 | t.Error("Empty key 'bids'") 265 | case len(c.Bids[0]) != 4: 266 | t.Error("Invalid bid length") 267 | } 268 | } 269 | 270 | func TestApiService_TradeHistories(t *testing.T) { 271 | s := NewApiServiceFromEnv() 272 | rsp, err := s.TradeHistories(context.Background(), "ETH-BTC") 273 | if err != nil { 274 | t.Fatal(err) 275 | } 276 | l := TradeHistoriesModel{} 277 | if err := rsp.ReadData(&l); err != nil { 278 | t.Fatal(err) 279 | } 280 | for _, c := range l { 281 | t.Log(ToJsonString(c)) 282 | switch { 283 | case c.Sequence == "": 284 | t.Error("Empty key 'sequence'") 285 | case c.Price == "": 286 | t.Error("Empty key 'price'") 287 | case c.Size == "": 288 | t.Error("Empty key 'size'") 289 | case c.Side == "": 290 | t.Error("Empty key 'side'") 291 | case c.Time == 0: 292 | t.Error("Empty key 'time'") 293 | } 294 | } 295 | } 296 | 297 | func TestApiService_KLines(t *testing.T) { 298 | s := NewApiServiceFromEnv() 299 | rsp, err := s.KLines(context.Background(), "ETH-BTC", "30min", time.Now().Unix()-7*24*3600, time.Now().Unix()) 300 | if err != nil { 301 | t.Fatal(err) 302 | } 303 | l := KLinesModel{} 304 | if err := rsp.ReadData(&l); err != nil { 305 | t.Fatal(err) 306 | } 307 | for _, c := range l { 308 | t.Log(ToJsonString(c)) 309 | if len(*c) != 7 { 310 | t.Error("Invalid length of rate") 311 | } 312 | } 313 | } 314 | 315 | func TestApiService_SymbolsV2(t *testing.T) { 316 | s := NewApiServiceFromEnv() 317 | rsp, err := s.SymbolsV2(context.Background(), "ETF") 318 | if err != nil { 319 | t.Fatal(err) 320 | } 321 | l := SymbolsModelV2{} 322 | if err := rsp.ReadData(&l); err != nil { 323 | t.Fatal(err) 324 | } 325 | for _, c := range l { 326 | t.Log(ToJsonString(c)) 327 | switch { 328 | case c.Name == "": 329 | t.Error("Empty key 'name'") 330 | case c.Symbol == "": 331 | t.Error("Empty key 'symbol'") 332 | case c.BaseCurrency == "": 333 | t.Error("Empty key 'baseCurrency'") 334 | case c.QuoteCurrency == "": 335 | t.Error("Empty key 'quoteCurrency'") 336 | case c.BaseMinSize == "": 337 | t.Error("Empty key 'baseMinSize'") 338 | case c.QuoteMinSize == "": 339 | t.Error("Empty key 'quoteMinSize'") 340 | case c.BaseMaxSize == "": 341 | t.Error("Empty key 'baseMaxSize'") 342 | case c.QuoteMaxSize == "": 343 | t.Error("Empty key 'quoteMaxSize'") 344 | case c.BaseIncrement == "": 345 | t.Error("Empty key 'baseIncrement'") 346 | case c.QuoteIncrement == "": 347 | t.Error("Empty key 'quoteIncrement'") 348 | case c.FeeCurrency == "": 349 | t.Error("Empty key 'feeCurrency'") 350 | case c.PriceIncrement == "": 351 | t.Error("Empty key 'priceIncrement'") 352 | case c.MinFunds == "": 353 | t.Error("Empty key 'feeCurrency'") 354 | } 355 | 356 | } 357 | } 358 | 359 | func TestApiService_SymbolsV2Detail(t *testing.T) { 360 | s := NewApiServiceFromEnv() 361 | rsp, err := s.SymbolsDetail(context.Background(), "BTC-USDT") 362 | if err != nil { 363 | t.Fatal(err) 364 | } 365 | c := SymbolModelV2{} 366 | if err := rsp.ReadData(&c); err != nil { 367 | t.Fatal(err) 368 | } 369 | t.Log(ToJsonString(c)) 370 | } 371 | -------------------------------------------------------------------------------- /symbol.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // A SymbolModel represents an available currency pairs for trading. 10 | type SymbolModel struct { 11 | Symbol string `json:"symbol"` 12 | Name string `json:"name"` 13 | BaseCurrency string `json:"baseCurrency"` 14 | QuoteCurrency string `json:"quoteCurrency"` 15 | Market string `json:"market"` 16 | BaseMinSize string `json:"baseMinSize"` 17 | QuoteMinSize string `json:"quoteMinSize"` 18 | BaseMaxSize string `json:"baseMaxSize"` 19 | QuoteMaxSize string `json:"quoteMaxSize"` 20 | BaseIncrement string `json:"baseIncrement"` 21 | QuoteIncrement string `json:"quoteIncrement"` 22 | PriceIncrement string `json:"priceIncrement"` 23 | FeeCurrency string `json:"feeCurrency"` 24 | EnableTrading bool `json:"enableTrading"` 25 | IsMarginEnabled bool `json:"isMarginEnabled"` 26 | PriceLimitRate string `json:"priceLimitRate"` 27 | } 28 | 29 | // A SymbolsModel is the set of *SymbolModel. 30 | type SymbolsModel []*SymbolModel 31 | 32 | // Symbols returns a list of available currency pairs for trading. 33 | // Deprecated 34 | func (as *ApiService) Symbols(ctx context.Context, market string) (*ApiResponse, error) { 35 | p := map[string]string{} 36 | if market != "" { 37 | p["market"] = market 38 | } 39 | req := NewRequest(http.MethodGet, "/api/v1/symbols", p) 40 | return as.Call(ctx, req) 41 | } 42 | 43 | // A TickerLevel1Model represents ticker include only the inside (i.e. best) bid and ask data, last price and last trade size. 44 | type TickerLevel1Model struct { 45 | Sequence string `json:"sequence"` 46 | Price string `json:"price"` 47 | Size string `json:"size"` 48 | BestBid string `json:"bestBid"` 49 | BestBidSize string `json:"bestBidSize"` 50 | BestAsk string `json:"bestAsk"` 51 | BestAskSize string `json:"bestAskSize"` 52 | Time int64 `json:"time"` 53 | } 54 | 55 | // TickerLevel1 returns the ticker include only the inside (i.e. best) bid and ask data, last price and last trade size. 56 | func (as *ApiService) TickerLevel1(ctx context.Context, symbol string) (*ApiResponse, error) { 57 | req := NewRequest(http.MethodGet, "/api/v1/market/orderbook/level1", map[string]string{"symbol": symbol}) 58 | return as.Call(ctx, req) 59 | } 60 | 61 | // A TickerModel represents a market ticker for all trading pairs in the market (including 24h volume). 62 | type TickerModel struct { 63 | Symbol string `json:"symbol"` 64 | SymbolName string `json:"symbolName"` 65 | Buy string `json:"buy"` 66 | Sell string `json:"sell"` 67 | ChangeRate string `json:"changeRate"` 68 | ChangePrice string `json:"changePrice"` 69 | High string `json:"high"` 70 | Low string `json:"low"` 71 | Vol string `json:"vol"` 72 | VolValue string `json:"volValue"` 73 | Last string `json:"last"` 74 | AveragePrice string `json:"averagePrice"` 75 | TakerFeeRate string `json:"takerFeeRate"` 76 | MakerFeeRate string `json:"makerFeeRate"` 77 | TakerCoefficient string `json:"takerCoefficient"` 78 | MakerCoefficient string `json:"makerCoefficient"` 79 | } 80 | 81 | // A TickersModel is the set of *MarketTickerModel. 82 | type TickersModel []*TickerModel 83 | 84 | // TickersResponseModel represents the response model of MarketTickers(). 85 | type TickersResponseModel struct { 86 | Time int64 `json:"time"` 87 | Tickers TickersModel `json:"ticker"` 88 | } 89 | 90 | // Tickers returns all tickers as TickersResponseModel for all trading pairs in the market (including 24h volume). 91 | func (as *ApiService) Tickers(ctx context.Context) (*ApiResponse, error) { 92 | req := NewRequest(http.MethodGet, "/api/v1/market/allTickers", nil) 93 | return as.Call(ctx, req) 94 | } 95 | 96 | // A Stats24hrModel represents 24 hr stats for the symbol. 97 | // Volume is in base currency units. 98 | // Open, high, low are in quote currency units. 99 | type Stats24hrModel struct { 100 | Time int64 `json:"time"` 101 | Symbol string `json:"symbol"` 102 | Buy string `json:"buy"` 103 | Sell string `json:"sell"` 104 | ChangeRate string `json:"changeRate"` 105 | ChangePrice string `json:"changePrice"` 106 | High string `json:"high"` 107 | Low string `json:"low"` 108 | Vol string `json:"vol"` 109 | VolValue string `json:"volValue"` 110 | Last string `json:"last"` 111 | AveragePrice string `json:"averagePrice"` 112 | TakerFeeRate string `json:"takerFeeRate"` 113 | MakerFeeRate string `json:"makerFeeRate"` 114 | TakerCoefficient string `json:"takerCoefficient"` 115 | MakerCoefficient string `json:"makerCoefficient"` 116 | } 117 | 118 | // Stats24hr returns 24 hr stats for the symbol. volume is in base currency units. open, high, low are in quote currency units. 119 | func (as *ApiService) Stats24hr(ctx context.Context, symbol string) (*ApiResponse, error) { 120 | req := NewRequest(http.MethodGet, "/api/v1/market/stats", map[string]string{"symbol": symbol}) 121 | return as.Call(ctx, req) 122 | } 123 | 124 | // MarketsModel returns Model of Markets API. 125 | type MarketsModel []string 126 | 127 | // Markets returns the transaction currencies for the entire trading market. 128 | func (as *ApiService) Markets(ctx context.Context) (*ApiResponse, error) { 129 | req := NewRequest(http.MethodGet, "/api/v1/markets", nil) 130 | return as.Call(ctx, req) 131 | } 132 | 133 | // A PartOrderBookModel represents a list of open orders for a symbol, a part of Order Book within 100 depth for each side(ask or bid). 134 | type PartOrderBookModel struct { 135 | Sequence string `json:"sequence"` 136 | Time int64 `json:"time"` 137 | Bids [][]string `json:"bids"` 138 | Asks [][]string `json:"asks"` 139 | } 140 | 141 | // AggregatedPartOrderBook returns a list of open orders(aggregated) for a symbol. 142 | func (as *ApiService) AggregatedPartOrderBook(ctx context.Context, symbol string, depth int64) (*ApiResponse, error) { 143 | req := NewRequest(http.MethodGet, "/api/v1/market/orderbook/level2_"+IntToString(depth), map[string]string{"symbol": symbol}) 144 | return as.Call(ctx, req) 145 | } 146 | 147 | // A FullOrderBookModel represents a list of open orders for a symbol, with full depth. 148 | type FullOrderBookModel struct { 149 | Sequence string `json:"sequence"` 150 | Time int64 `json:"time"` 151 | Bids [][]string `json:"bids"` 152 | Asks [][]string `json:"asks"` 153 | } 154 | 155 | // AggregatedFullOrderBook returns a list of open orders(aggregated) for a symbol. 156 | // Deprecated: Use AggregatedFullOrderBookV3/WebSocket instead. 157 | func (as *ApiService) AggregatedFullOrderBook(ctx context.Context, symbol string) (*ApiResponse, error) { 158 | req := NewRequest(http.MethodGet, "/api/v2/market/orderbook/level2", map[string]string{"symbol": symbol}) 159 | return as.Call(ctx, req) 160 | } 161 | 162 | // AggregatedFullOrderBookV3 returns a list of open orders(aggregated) for a symbol. 163 | func (as *ApiService) AggregatedFullOrderBookV3(ctx context.Context, symbol string) (*ApiResponse, error) { 164 | req := NewRequest(http.MethodGet, "/api/v3/market/orderbook/level2", map[string]string{"symbol": symbol}) 165 | return as.Call(ctx, req) 166 | } 167 | 168 | // A FullOrderBookV2Model represents a list of open orders for a symbol, with full depth. 169 | type FullOrderBookV2Model struct { 170 | Sequence int64 `json:"sequence"` 171 | Time int64 `json:"time"` 172 | Bids [][]interface{} `json:"bids"` 173 | Asks [][]interface{} `json:"asks"` 174 | } 175 | 176 | // AtomicFullOrderBook returns a list of open orders for a symbol. 177 | // Level-3 order book includes all bids and asks (non-aggregated, each item in Level-3 means a single order). 178 | func (as *ApiService) AtomicFullOrderBook(ctx context.Context, symbol string) (*ApiResponse, error) { 179 | req := NewRequest(http.MethodGet, "/api/v1/market/orderbook/level3", map[string]string{"symbol": symbol}) 180 | return as.Call(ctx, req) 181 | } 182 | 183 | // AtomicFullOrderBookV2 returns a list of open orders for a symbol. 184 | // Level-3 order book includes all bids and asks (non-aggregated, each item in Level-3 means a single order). 185 | func (as *ApiService) AtomicFullOrderBookV2(ctx context.Context, symbol string) (*ApiResponse, error) { 186 | req := NewRequest(http.MethodGet, "/api/v2/market/orderbook/level3", map[string]string{"symbol": symbol}) 187 | return as.Call(ctx, req) 188 | } 189 | 190 | // A TradeHistoryModel represents the latest trades for a symbol. 191 | type TradeHistoryModel struct { 192 | Sequence string `json:"sequence"` 193 | Price string `json:"price"` 194 | Size string `json:"size"` 195 | Side string `json:"side"` 196 | Time int64 `json:"time"` 197 | } 198 | 199 | // A TradeHistoriesModel is the set of *TradeHistoryModel. 200 | type TradeHistoriesModel []*TradeHistoryModel 201 | 202 | // TradeHistories returns a list the latest trades for a symbol. 203 | func (as *ApiService) TradeHistories(ctx context.Context, symbol string) (*ApiResponse, error) { 204 | req := NewRequest(http.MethodGet, "/api/v1/market/histories", map[string]string{"symbol": symbol}) 205 | return as.Call(ctx, req) 206 | } 207 | 208 | // KLineModel represents the k lines for a symbol. 209 | // Rates are returned in grouped buckets based on requested type. 210 | type KLineModel []string 211 | 212 | // A KLinesModel is the set of *KLineModel. 213 | type KLinesModel []*KLineModel 214 | 215 | // KLines returns the k lines for a symbol. 216 | // Data are returned in grouped buckets based on requested type. 217 | // Parameter #2 typo is the type of candlestick patterns. 218 | func (as *ApiService) KLines(ctx context.Context, symbol, typo string, startAt, endAt int64) (*ApiResponse, error) { 219 | req := NewRequest(http.MethodGet, "/api/v1/market/candles", map[string]string{ 220 | "symbol": symbol, 221 | "type": typo, 222 | "startAt": IntToString(startAt), 223 | "endAt": IntToString(endAt), 224 | }) 225 | return as.Call(ctx, req) 226 | } 227 | 228 | // SymbolsV2 returns a list of available currency pairs for trading. 229 | func (as *ApiService) SymbolsV2(ctx context.Context, market string) (*ApiResponse, error) { 230 | p := map[string]string{} 231 | if market != "" { 232 | p["market"] = market 233 | } 234 | req := NewRequest(http.MethodGet, "/api/v2/symbols", p) 235 | return as.Call(ctx, req) 236 | } 237 | 238 | // SymbolsDetail Request via this endpoint to get detail currency pairs for trading 239 | func (as *ApiService) SymbolsDetail(ctx context.Context, symbol string) (*ApiResponse, error) { 240 | req := NewRequest(http.MethodGet, fmt.Sprintf("/api/v2/symbols/%s", symbol), nil) 241 | return as.Call(ctx, req) 242 | } 243 | 244 | // A SymbolsModelV2 is the set of *SymbolsModelV2. 245 | type SymbolsModelV2 []*SymbolModelV2 246 | 247 | type SymbolModelV2 struct { 248 | Symbol string `json:"symbol"` 249 | Name string `json:"name"` 250 | BaseCurrency string `json:"baseCurrency"` 251 | QuoteCurrency string `json:"quoteCurrency"` 252 | Market string `json:"market"` 253 | BaseMinSize string `json:"baseMinSize"` 254 | QuoteMinSize string `json:"quoteMinSize"` 255 | BaseMaxSize string `json:"baseMaxSize"` 256 | QuoteMaxSize string `json:"quoteMaxSize"` 257 | BaseIncrement string `json:"baseIncrement"` 258 | QuoteIncrement string `json:"quoteIncrement"` 259 | PriceIncrement string `json:"priceIncrement"` 260 | FeeCurrency string `json:"feeCurrency"` 261 | EnableTrading bool `json:"enableTrading"` 262 | IsMarginEnabled bool `json:"isMarginEnabled"` 263 | PriceLimitRate string `json:"priceLimitRate"` 264 | MinFunds string `json:"minFunds"` 265 | } 266 | -------------------------------------------------------------------------------- /websocket.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "math/rand" 9 | "net/http" 10 | "net/url" 11 | "sync" 12 | "time" 13 | 14 | "github.com/gorilla/websocket" 15 | "github.com/pkg/errors" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // A WebSocketTokenModel contains a token and some servers for WebSocket feed. 20 | type WebSocketTokenModel struct { 21 | Token string `json:"token"` 22 | Servers WebSocketServersModel `json:"instanceServers"` 23 | AcceptUserMessage bool `json:"accept_user_message"` 24 | } 25 | 26 | // A WebSocketServerModel contains some servers for WebSocket feed. 27 | type WebSocketServerModel struct { 28 | PingInterval int64 `json:"pingInterval"` 29 | Endpoint string `json:"endpoint"` 30 | Protocol string `json:"protocol"` 31 | Encrypt bool `json:"encrypt"` 32 | PingTimeout int64 `json:"pingTimeout"` 33 | } 34 | 35 | // A WebSocketServersModel is the set of *WebSocketServerModel. 36 | type WebSocketServersModel []*WebSocketServerModel 37 | 38 | // RandomServer returns a server randomly. 39 | func (s WebSocketServersModel) RandomServer() (*WebSocketServerModel, error) { 40 | l := len(s) 41 | if l == 0 { 42 | return nil, errors.New("No available server ") 43 | } 44 | return s[rand.Intn(l)], nil 45 | } 46 | 47 | // WebSocketPublicToken returns the token for public channel. 48 | func (as *ApiService) WebSocketPublicToken(ctx context.Context) (*ApiResponse, error) { 49 | req := NewRequest(http.MethodPost, "/api/v1/bullet-public", map[string]string{}) 50 | return as.Call(ctx, req) 51 | } 52 | 53 | // WebSocketPrivateToken returns the token for private channel. 54 | func (as *ApiService) WebSocketPrivateToken(ctx context.Context) (*ApiResponse, error) { 55 | req := NewRequest(http.MethodPost, "/api/v1/bullet-private", map[string]string{}) 56 | return as.Call(ctx, req) 57 | } 58 | 59 | // All message types of WebSocket. 60 | const ( 61 | WelcomeMessage = "welcome" 62 | PingMessage = "ping" 63 | PongMessage = "pong" 64 | SubscribeMessage = "subscribe" 65 | AckMessage = "ack" 66 | UnsubscribeMessage = "unsubscribe" 67 | ErrorMessage = "error" 68 | Message = "message" 69 | Notice = "notice" 70 | Command = "command" 71 | ) 72 | 73 | // A WebSocketMessage represents a message between the WebSocket client and server. 74 | type WebSocketMessage struct { 75 | Id string `json:"id"` 76 | Type string `json:"type"` 77 | } 78 | 79 | // A WebSocketSubscribeMessage represents a message to subscribe the public/private channel. 80 | type WebSocketSubscribeMessage struct { 81 | *WebSocketMessage 82 | Topic string `json:"topic"` 83 | PrivateChannel bool `json:"privateChannel"` 84 | Response bool `json:"response"` 85 | } 86 | 87 | // NewPingMessage creates a ping message instance. 88 | func NewPingMessage() *WebSocketMessage { 89 | return &WebSocketMessage{ 90 | Id: IntToString(time.Now().UnixNano()), 91 | Type: PingMessage, 92 | } 93 | } 94 | 95 | // NewSubscribeMessage creates a subscribe message instance. 96 | func NewSubscribeMessage(topic string, privateChannel bool) *WebSocketSubscribeMessage { 97 | return &WebSocketSubscribeMessage{ 98 | WebSocketMessage: &WebSocketMessage{ 99 | Id: IntToString(time.Now().UnixNano()), 100 | Type: SubscribeMessage, 101 | }, 102 | Topic: topic, 103 | PrivateChannel: privateChannel, 104 | Response: true, 105 | } 106 | } 107 | 108 | // A WebSocketUnsubscribeMessage represents a message to unsubscribe the public/private channel. 109 | type WebSocketUnsubscribeMessage WebSocketSubscribeMessage 110 | 111 | // NewUnsubscribeMessage creates a unsubscribe message instance. 112 | func NewUnsubscribeMessage(topic string, privateChannel bool) *WebSocketUnsubscribeMessage { 113 | return &WebSocketUnsubscribeMessage{ 114 | WebSocketMessage: &WebSocketMessage{ 115 | Id: IntToString(time.Now().UnixNano()), 116 | Type: UnsubscribeMessage, 117 | }, 118 | Topic: topic, 119 | PrivateChannel: privateChannel, 120 | Response: true, 121 | } 122 | } 123 | 124 | // A WebSocketDownstreamMessage represents a message from the WebSocket server to client. 125 | type WebSocketDownstreamMessage struct { 126 | *WebSocketMessage 127 | Sn int64 `json:"sn"` 128 | Topic string `json:"topic"` 129 | Subject string `json:"subject"` 130 | RawData json.RawMessage `json:"data"` 131 | } 132 | 133 | // ReadData read the data in channel. 134 | func (m *WebSocketDownstreamMessage) ReadData(v interface{}) error { 135 | return json.Unmarshal(m.RawData, v) 136 | } 137 | 138 | // A WebSocketClient represents a connection to WebSocket server. 139 | type WebSocketClient struct { 140 | // Wait all goroutines quit 141 | wg *sync.WaitGroup 142 | // Stop subscribing channel 143 | done chan struct{} 144 | // Pong channel to check pong message 145 | pongs chan string 146 | // ACK channel to check pong message 147 | acks chan string 148 | // Error channel 149 | errors chan error 150 | // Downstream message channel 151 | messages chan *WebSocketDownstreamMessage 152 | conn *websocket.Conn 153 | token *WebSocketTokenModel 154 | server *WebSocketServerModel 155 | enableHeartbeat bool 156 | skipVerifyTls bool 157 | timeout time.Duration 158 | } 159 | 160 | var defaultTimeout = time.Second * 5 161 | 162 | // WebSocketClientOpts defines the options for the client 163 | // during the websocket connection. 164 | type WebSocketClientOpts struct { 165 | Token *WebSocketTokenModel 166 | TLSSkipVerify bool 167 | Timeout time.Duration 168 | } 169 | 170 | // NewWebSocketClient creates an instance of WebSocketClient. 171 | func (as *ApiService) NewWebSocketClient(token *WebSocketTokenModel) *WebSocketClient { 172 | return as.NewWebSocketClientOpts(WebSocketClientOpts{ 173 | Token: token, 174 | TLSSkipVerify: as.apiSkipVerifyTls, 175 | Timeout: defaultTimeout, 176 | }) 177 | } 178 | 179 | // NewWebSocketClientOpts creates an instance of WebSocketClient with the parsed options. 180 | func (as *ApiService) NewWebSocketClientOpts(opts WebSocketClientOpts) *WebSocketClient { 181 | wc := &WebSocketClient{ 182 | wg: &sync.WaitGroup{}, 183 | done: make(chan struct{}), 184 | errors: make(chan error, 1), 185 | pongs: make(chan string, 1), 186 | acks: make(chan string, 1), 187 | token: opts.Token, 188 | messages: make(chan *WebSocketDownstreamMessage, 2048), 189 | skipVerifyTls: opts.TLSSkipVerify, 190 | timeout: opts.Timeout, 191 | } 192 | return wc 193 | } 194 | 195 | // Connect connects the WebSocket server. 196 | func (wc *WebSocketClient) Connect() (<-chan *WebSocketDownstreamMessage, <-chan error, error) { 197 | // Find out a server 198 | s, err := wc.token.Servers.RandomServer() 199 | if err != nil { 200 | return wc.messages, wc.errors, err 201 | } 202 | wc.server = s 203 | 204 | // Concat ws url 205 | q := url.Values{} 206 | q.Add("connectId", IntToString(time.Now().UnixNano())) 207 | q.Add("token", wc.token.Token) 208 | if wc.token.AcceptUserMessage == true { 209 | q.Add("acceptUserMessage", "true") 210 | } 211 | u := fmt.Sprintf("%s?%s", s.Endpoint, q.Encode()) 212 | 213 | // Ignore verify tls 214 | websocket.DefaultDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: wc.skipVerifyTls} 215 | 216 | // Connect ws server 217 | websocket.DefaultDialer.ReadBufferSize = 2048000 //2000 kb 218 | wc.conn, _, err = websocket.DefaultDialer.Dial(u, nil) 219 | if err != nil { 220 | return wc.messages, wc.errors, err 221 | } 222 | 223 | // Must read the first welcome message 224 | for { 225 | m := &WebSocketDownstreamMessage{} 226 | if err := wc.conn.ReadJSON(m); err != nil { 227 | return wc.messages, wc.errors, err 228 | } 229 | if DebugMode { 230 | logrus.Debugf("Received a WebSocket message: %s", ToJsonString(m)) 231 | } 232 | if m.Type == ErrorMessage { 233 | return wc.messages, wc.errors, errors.Errorf("Error message: %s", ToJsonString(m)) 234 | } 235 | if m.Type == WelcomeMessage { 236 | break 237 | } 238 | } 239 | 240 | wc.wg.Add(2) 241 | go wc.read() 242 | go wc.keepHeartbeat() 243 | 244 | return wc.messages, wc.errors, nil 245 | } 246 | 247 | func (wc *WebSocketClient) read() { 248 | defer func() { 249 | close(wc.pongs) 250 | close(wc.messages) 251 | wc.wg.Done() 252 | }() 253 | 254 | for { 255 | select { 256 | case <-wc.done: 257 | return 258 | default: 259 | m := &WebSocketDownstreamMessage{} 260 | if err := wc.conn.ReadJSON(m); err != nil { 261 | wc.errors <- err 262 | return 263 | } 264 | if DebugMode { 265 | logrus.Debugf("Received a WebSocket message: %s", ToJsonString(m)) 266 | } 267 | // log.Printf("ReadJSON: %s", ToJsonString(m)) 268 | switch m.Type { 269 | case WelcomeMessage: 270 | case PongMessage: 271 | if wc.enableHeartbeat { 272 | wc.pongs <- m.Id 273 | } 274 | case AckMessage: 275 | // log.Printf("Subscribed: %s==%s? %s", channel.Id, m.Id, channel.Topic) 276 | wc.acks <- m.Id 277 | case ErrorMessage: 278 | wc.errors <- errors.Errorf("Error message: %s", ToJsonString(m)) 279 | return 280 | case Message, Notice, Command: 281 | wc.messages <- m 282 | default: 283 | wc.errors <- errors.Errorf("Unknown message type: %s", m.Type) 284 | } 285 | } 286 | } 287 | } 288 | 289 | func (wc *WebSocketClient) keepHeartbeat() { 290 | wc.enableHeartbeat = true 291 | // New ticker to send ping message 292 | pt := time.NewTicker(time.Duration(wc.server.PingInterval)*time.Millisecond - time.Millisecond*200) 293 | defer wc.wg.Done() 294 | defer pt.Stop() 295 | 296 | for { 297 | select { 298 | case <-wc.done: 299 | return 300 | case <-pt.C: 301 | p := NewPingMessage() 302 | m := ToJsonString(p) 303 | if DebugMode { 304 | logrus.Debugf("Sent a WebSocket message: %s", m) 305 | } 306 | if err := wc.conn.WriteMessage(websocket.TextMessage, []byte(m)); err != nil { 307 | wc.errors <- err 308 | return 309 | } 310 | 311 | // log.Printf("Ping: %s", ToJsonString(p)) 312 | // Waiting (with timeout) for the server to response pong message 313 | // If timeout, close this connection 314 | select { 315 | case pid := <-wc.pongs: 316 | if pid != p.Id { 317 | wc.errors <- errors.Errorf("Invalid pong id %s, expect %s", pid, p.Id) 318 | return 319 | } 320 | case <-time.After(time.Duration(wc.server.PingTimeout) * time.Millisecond): 321 | wc.errors <- errors.Errorf("Wait pong message timeout in %d ms", wc.server.PingTimeout) 322 | return 323 | } 324 | } 325 | } 326 | } 327 | 328 | // Subscribe subscribes the specified channel. 329 | func (wc *WebSocketClient) Subscribe(channels ...*WebSocketSubscribeMessage) error { 330 | for _, c := range channels { 331 | m := ToJsonString(c) 332 | if DebugMode { 333 | logrus.Debugf("Sent a WebSocket message: %s", m) 334 | } 335 | if err := wc.conn.WriteMessage(websocket.TextMessage, []byte(m)); err != nil { 336 | return err 337 | } 338 | //log.Printf("Subscribing: %s, %s", c.Id, c.Topic) 339 | select { 340 | case id := <-wc.acks: 341 | //log.Printf("ack: %s=>%s", id, c.Id) 342 | if id != c.Id { 343 | return errors.Errorf("Invalid ack id %s, expect %s", id, c.Id) 344 | } 345 | case err := <-wc.errors: 346 | return errors.Errorf("Subscribe failed, %s", err.Error()) 347 | case <-time.After(wc.timeout): 348 | return errors.Errorf("Wait ack message timeout in %v", wc.timeout) 349 | } 350 | } 351 | return nil 352 | } 353 | 354 | // Unsubscribe unsubscribes the specified channel. 355 | func (wc *WebSocketClient) Unsubscribe(channels ...*WebSocketUnsubscribeMessage) error { 356 | for _, c := range channels { 357 | m := ToJsonString(c) 358 | if DebugMode { 359 | logrus.Debugf("Sent a WebSocket message: %s", m) 360 | } 361 | if err := wc.conn.WriteMessage(websocket.TextMessage, []byte(m)); err != nil { 362 | return err 363 | } 364 | //log.Printf("Unsubscribing: %s, %s", c.Id, c.Topic) 365 | select { 366 | case id := <-wc.acks: 367 | //log.Printf("ack: %s=>%s", id, c.Id) 368 | if id != c.Id { 369 | return errors.Errorf("Invalid ack id %s, expect %s", id, c.Id) 370 | } 371 | case <-time.After(wc.timeout): 372 | return errors.Errorf("Wait ack message timeout in %v", wc.timeout) 373 | } 374 | } 375 | return nil 376 | } 377 | 378 | // Stop stops subscribing the specified channel, all goroutines quit. 379 | func (wc *WebSocketClient) Stop() { 380 | close(wc.done) 381 | _ = wc.conn.Close() 382 | wc.wg.Wait() 383 | } 384 | -------------------------------------------------------------------------------- /hf_order_types.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "encoding/json" 5 | "math/big" 6 | ) 7 | 8 | type HfPlaceOrderRes struct { 9 | OrderId string `json:"orderId"` 10 | ClientOid string `json:"clientOid"` 11 | Success bool `json:"success"` 12 | } 13 | 14 | type HfSyncPlaceOrderRes struct { 15 | OrderId string `json:"orderId"` 16 | ClientOid string `json:"clientOid"` 17 | OrderTime json.Number `json:"orderTime"` 18 | OriginSize string `json:"originSize"` 19 | DealSize string `json:"dealSize"` 20 | RemainSize string `json:"remainSize"` 21 | CanceledSize string `json:"canceledSize"` 22 | Status string `json:"status"` 23 | MatchTime json.Number `json:"matchTime"` 24 | } 25 | 26 | type HFCreateMultiOrderModel struct { 27 | ClientOid string `json:"clientOid"` 28 | Symbol string `json:"symbol"` 29 | OrderType string `json:"type"` 30 | TimeInForce string `json:"timeInForce"` 31 | Stp string `json:"stp"` 32 | Side string `json:"side"` 33 | Price string `json:"price"` 34 | Size string `json:"size"` 35 | CancelAfter big.Int `json:"cancelAfter"` 36 | PostOnly bool `json:"postOnly"` 37 | Hidden bool `json:"hidden"` 38 | Iceberg bool `json:"iceberg"` 39 | VisibleSize string `json:"visibleSize"` 40 | Tags string `json:"tags"` 41 | Remark string `json:"remark"` 42 | } 43 | 44 | type HfPlaceMultiOrdersRes []*HfPlaceOrderRes 45 | 46 | type HfModifyOrderRes struct { 47 | NewOrderId string `json:"newOrderId"` 48 | ClientOid string `json:"clientOid"` 49 | } 50 | 51 | type HfSyncCancelOrderRes struct { 52 | OrderId string `json:"orderId"` 53 | OriginSize string `json:"originSize"` 54 | OriginFunds string `json:"originFunds"` 55 | DealSize string `json:"dealSize"` 56 | RemainSize string `json:"remainSize"` 57 | CanceledSize string `json:"canceledSize"` 58 | Status string `json:"status"` 59 | } 60 | 61 | type HfSyncCancelOrderWithSizeRes struct { 62 | OrderId string `json:"orderId"` 63 | CancelSize string `json:"cancelSize"` 64 | } 65 | 66 | type HfOrdersModel []*HfOrderModel 67 | 68 | type HfOrderModel struct { 69 | Id string `json:"id"` 70 | Symbol string `json:"symbol"` 71 | OpType string `json:"opType"` 72 | Type string `json:"type"` 73 | Side string `json:"side"` 74 | Price string `json:"price"` 75 | Size string `json:"size"` 76 | Funds string `json:"funds"` 77 | DealSize string `json:"dealSize"` 78 | DealFunds string `json:"dealFunds"` 79 | Fee string `json:"fee"` 80 | FeeCurrency string `json:"feeCurrency"` 81 | Stp string `json:"stp"` 82 | TimeInForce string `json:"timeInForce"` 83 | PostOnly bool `json:"postOnly"` 84 | Hidden bool `json:"hidden"` 85 | Iceberg bool `json:"iceberg"` 86 | VisibleSize string `json:"visibleSize"` 87 | CancelAfter int64 `json:"cancelAfter"` 88 | Channel string `json:"channel"` 89 | ClientOid string `json:"clientOid"` 90 | Remark string `json:"remark"` 91 | Tags string `json:"tags"` 92 | CancelExist bool `json:"cancelExist"` 93 | CreatedAt json.Number `json:"createdAt"` 94 | LastUpdatedAt json.Number `json:"lastUpdatedAt"` 95 | TradeType string `json:"tradeType"` 96 | InOrderBook bool `json:"inOrderBook"` 97 | Active bool `json:"active"` 98 | CancelledSize string `json:"cancelledSize"` 99 | CancelledFunds string `json:"cancelledFunds"` 100 | RemainSize string `json:"remainSize"` 101 | RemainFunds string `json:"remainFunds"` 102 | } 103 | 104 | type HfAutoCancelSettingRes struct { 105 | CurrentTime json.Number `json:"currentTime"` 106 | TriggerTime json.Number `json:"triggerTime"` 107 | } 108 | 109 | type AUtoCancelSettingModel struct { 110 | Timeout int64 `json:"timeout"` 111 | Symbols string `json:"symbols"` 112 | CurrentTime json.Number `json:"currentTime"` 113 | TriggerTime json.Number `json:"triggerTime"` 114 | } 115 | 116 | type HfOrderIdModel struct { 117 | OrderId string `json:"orderId"` 118 | } 119 | 120 | type HfClientOidModel struct { 121 | ClientOid string `json:"clientOid"` 122 | } 123 | 124 | type HfTransactionDetailsModel struct { 125 | LastId json.Number `json:"lastId"` 126 | Items []*HfTransactionDetailModel `json:"items"` 127 | } 128 | 129 | type HfTransactionDetailModel struct { 130 | Id json.Number `json:"id"` 131 | Symbol string `json:"symbol"` 132 | TradeId json.Number `json:"tradeId"` 133 | OrderId string `json:"orderId"` 134 | CounterOrderId string `json:"counterOrderId"` 135 | Side string `json:"side"` 136 | Liquidity string `json:"liquidity"` 137 | ForceTaker bool `json:"forceTaker"` 138 | Price string `json:"price"` 139 | Size string `json:"size"` 140 | Funds string `json:"funds"` 141 | Fee string `json:"fee"` 142 | FeeRate string `json:"feeRate"` 143 | FeeCurrency string `json:"feeCurrency"` 144 | OrderType string `json:"type"` 145 | Stop string `json:"stop"` 146 | CreatedAt json.Number `json:"createdAt"` 147 | TradeType string `json:"tradeType"` 148 | } 149 | 150 | type HfCancelOrdersResultModel struct { 151 | SucceedSymbols []string `json:"succeedSymbols"` 152 | FailedSymbols []*HfCancelOrdersFailedResultModel `json:"failedSymbols"` 153 | } 154 | type HfCancelOrdersFailedResultModel struct { 155 | Symbol string `json:"symbol"` 156 | Error string `json:"error"` 157 | } 158 | 159 | type HfPlaceOrderReq struct { 160 | ClientOid string `json:"clientOid"` 161 | Symbol string `json:"symbol"` 162 | Side string `json:"side"` 163 | Stp string `json:"stp"` 164 | Tags string `json:"tags"` 165 | Remark string `json:"remark"` 166 | 167 | Price string `json:"price,omitempty"` 168 | Size string `json:"size,omitempty"` 169 | TimeInForce string `json:"timeInForce,omitempty"` 170 | CancelAfter int64 `json:"cancelAfter,omitempty"` 171 | PostOnly bool `json:"postOnly,omitempty"` 172 | Hidden bool `json:"hidden,omitempty"` 173 | Iceberg bool `json:"iceberg,omitempty"` 174 | VisibleSize bool `json:"visibleSize,omitempty"` 175 | 176 | Funds string `json:"funds,omitempty"` 177 | } 178 | 179 | type HfMarginOrderV3Req struct { 180 | ClientOid string `json:"clientOid"` 181 | Symbol string `json:"symbol"` 182 | Side string `json:"side"` 183 | Type string `json:"type"` 184 | Stp string `json:"stp"` 185 | IsIsolated bool `json:"isIsolated"` 186 | AutoBorrow bool `json:"autoBorrow"` 187 | AutoRepay bool `json:"autoRepay"` 188 | 189 | Price string `json:"price,omitempty"` 190 | Size string `json:"size,omitempty"` 191 | TimeInForce string `json:"timeInForce,omitempty"` 192 | CancelAfter int64 `json:"cancelAfter,omitempty"` 193 | PostOnly bool `json:"postOnly,omitempty"` 194 | Hidden bool `json:"hidden,omitempty"` 195 | Iceberg bool `json:"iceberg,omitempty"` 196 | VisibleSize bool `json:"visibleSize,omitempty"` 197 | 198 | Funds string `json:"funds,omitempty"` 199 | } 200 | 201 | type HfMarginOrderV3Resp struct { 202 | OrderNo string `json:"orderNo"` 203 | BorrowSize string `json:"borrowSize"` 204 | LoanApplyId string `json:"loanApplyId"` 205 | } 206 | 207 | type HfCancelMarinOrderV3Req struct { 208 | OrderId string `json:"orderId"` 209 | Symbol string `json:"symbol"` 210 | } 211 | 212 | type HfCancelMarinOrderV3Resp struct { 213 | OrderId string `json:"orderId"` 214 | } 215 | 216 | type HfCancelClientMarinOrderV3Req struct { 217 | ClientOid string `json:"clientOid"` 218 | Symbol string `json:"symbol"` 219 | } 220 | 221 | type HfCancelClientMarinOrderV3Resp struct { 222 | ClientOid string `json:"clientOid"` 223 | } 224 | 225 | type HfCancelAllMarginOrdersV3Req struct { 226 | TradeType string `json:"tradeType" url:"tradeType"` 227 | Symbol string `json:"symbol" url:"symbol"` 228 | } 229 | 230 | type HfCancelAllMarginOrdersV3Resp string 231 | 232 | type HFMarginActiveSymbolsModel struct { 233 | SymbolSize int32 `json:"symbolSize"` 234 | Symbols []string `json:"symbols"` 235 | } 236 | 237 | type HfMarinActiveOrdersV3Req struct { 238 | TradeType string `url:"tradeType"` 239 | Symbol string `url:"symbol"` 240 | } 241 | 242 | type MarginOrderV3Model struct { 243 | Id string `json:"id"` 244 | Symbol string `json:"symbol"` 245 | OpType string `json:"opType"` 246 | Type string `json:"type"` 247 | Side string `json:"side"` 248 | Price string `json:"price"` 249 | Size string `json:"size"` 250 | Funds string `json:"funds"` 251 | DealFunds string `json:"dealFunds"` 252 | DealSize string `json:"dealSize"` 253 | Fee string `json:"fee"` 254 | FeeCurrency string `json:"feeCurrency"` 255 | Stp string `json:"stp"` 256 | TimeInForce string `json:"timeInForce"` 257 | PostOnly bool `json:"postOnly"` 258 | Hidden bool `json:"hidden"` 259 | Iceberg bool `json:"iceberg"` 260 | VisibleSize string `json:"visibleSize"` 261 | CancelAfter int `json:"cancelAfter"` 262 | Channel string `json:"channel"` 263 | ClientOid string `json:"clientOid"` 264 | Remark string `json:"remark"` 265 | Tags string `json:"tags"` 266 | Active bool `json:"active"` 267 | InOrderBook bool `json:"inOrderBook"` 268 | CancelExist bool `json:"cancelExist"` 269 | CreatedAt int64 `json:"createdAt"` 270 | LastUpdatedAt int64 `json:"lastUpdatedAt"` 271 | TradeType string `json:"tradeType"` 272 | } 273 | 274 | type HfMarinActiveOrdersV3Resp []*MarginOrderV3Model 275 | 276 | type HfMarinDoneOrdersV3Req struct { 277 | TradeType string `url:"tradeType"` 278 | Symbol string `url:"symbol"` 279 | Side string `url:"side,omitempty"` 280 | Type string `url:"type,omitempty"` 281 | StartAt int64 `url:"startAt,omitempty"` 282 | EndAt int64 `url:"endAt,omitempty"` 283 | LastId int64 `url:"lastId,omitempty"` 284 | Limit int `url:"limit,omitempty"` 285 | } 286 | 287 | type HfMarinDoneOrdersV3Resp struct { 288 | Items []*MarginFillModel `json:"items"` 289 | LastId int64 `json:"lastId"` 290 | } 291 | 292 | type MarginFillModel struct { 293 | ID int64 `json:"id"` 294 | Symbol string `json:"symbol"` 295 | OpType string `json:"opType"` 296 | Type string `json:"type"` 297 | Side string `json:"side"` 298 | Price string `json:"price"` 299 | Size string `json:"size"` 300 | Funds string `json:"funds"` 301 | DealFunds string `json:"dealFunds"` 302 | DealSize string `json:"dealSize"` 303 | Fee string `json:"fee"` 304 | FeeCurrency string `json:"feeCurrency"` 305 | STP string `json:"stp"` 306 | TimeInForce string `json:"timeInForce"` 307 | PostOnly bool `json:"postOnly"` 308 | Hidden bool `json:"hidden"` 309 | Iceberg bool `json:"iceberg"` 310 | VisibleSize string `json:"visibleSize"` 311 | CancelAfter int64 `json:"cancelAfter"` 312 | Channel string `json:"channel"` 313 | ClientOid string `json:"clientOid"` 314 | Remark string `json:"remark"` 315 | Tags string `json:"tags"` 316 | Active bool `json:"active"` 317 | InOrderBook bool `json:"inOrderBook"` 318 | CancelExist bool `json:"cancelExist"` 319 | CreatedAt int64 `json:"createdAt"` 320 | LastUpdatedAt int64 `json:"lastUpdatedAt"` 321 | TradeType string `json:"tradeType"` 322 | } 323 | 324 | type HfMarinOrderV3Req struct { 325 | OrderId string 326 | Symbol string 327 | } 328 | 329 | type HfMarinOrderV3Resp struct { 330 | MarginFillModel 331 | } 332 | 333 | type HfMarinClientOrderV3Req struct { 334 | ClientOid string 335 | Symbol string 336 | } 337 | 338 | type HfMarinClientOrderV3Resp struct { 339 | MarginFillModel 340 | } 341 | 342 | type HfMarinFillsV3Req struct { 343 | Symbol string `url:"symbol"` 344 | TradeType string `url:"tradeType"` 345 | OrderId string `url:"orderId,omitempty"` 346 | Side string `url:"side,omitempty"` 347 | Type string `url:"type,omitempty"` 348 | StartAt int64 `url:"startAt,omitempty"` 349 | EndAt int64 `url:"endAt,omitempty"` 350 | LastId int64 `url:"lastId,omitempty"` 351 | Limit int `url:"limit,omitempty"` 352 | } 353 | 354 | type HfMarinFillsV3Resp struct { 355 | Items []*MarginFillModel `json:"items"` 356 | LastId int64 `json:"lastId"` 357 | } 358 | -------------------------------------------------------------------------------- /order_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestApiService_CreateOrder(t *testing.T) { 11 | t.SkipNow() 12 | 13 | s := NewApiServiceFromEnv() 14 | p := &CreateOrderModel{ 15 | ClientOid: IntToString(time.Now().UnixNano()), 16 | Side: "buy", 17 | Symbol: "KCS-ETH", 18 | Price: "0.0036", 19 | Size: "1", 20 | } 21 | rsp, err := s.CreateOrder(context.Background(), p) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | o := &CreateOrderResultModel{} 26 | if err := rsp.ReadData(o); err != nil { 27 | t.Fatal(err) 28 | } 29 | t.Log(ToJsonString(o)) 30 | switch { 31 | case o.OrderId == "": 32 | t.Error("Empty key 'OrderId'") 33 | } 34 | } 35 | 36 | func TestApiService_CreateMultiOrder(t *testing.T) { 37 | t.SkipNow() 38 | 39 | s := NewApiServiceFromEnv() 40 | 41 | orders := make([]*CreateOrderModel, 0, 5) 42 | for i := 0; i < 5; i++ { 43 | p := &CreateOrderModel{ 44 | ClientOid: IntToString(time.Now().UnixNano() + int64(i)), 45 | Side: "buy", 46 | Price: "0.0036", 47 | Size: "1", 48 | Remark: "Multi " + strconv.Itoa(i), 49 | } 50 | orders = append(orders, p) 51 | } 52 | rsp, err := s.CreateMultiOrder(context.Background(), "KCS-ETH", orders) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | r := &CreateMultiOrderResultModel{} 57 | if err := rsp.ReadData(r); err != nil { 58 | t.Fatal(err) 59 | } 60 | t.Log(ToJsonString(r)) 61 | } 62 | 63 | func TestApiService_CancelOrder(t *testing.T) { 64 | t.SkipNow() 65 | 66 | s := NewApiServiceFromEnv() 67 | rsp, err := s.CancelOrder(context.Background(), "order id") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | o := &CancelOrderResultModel{} 72 | if err := rsp.ReadData(o); err != nil { 73 | t.Fatal(err) 74 | } 75 | t.Log(ToJsonString(o)) 76 | switch { 77 | case len(o.CancelledOrderIds) == 0: 78 | t.Error("Empty key 'cancelledOrderIds'") 79 | } 80 | } 81 | 82 | func TestApiService_CancelOrderByClient(t *testing.T) { 83 | t.SkipNow() 84 | 85 | s := NewApiServiceFromEnv() 86 | rsp, err := s.CancelOrderByClient(context.Background(), "client id") 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | o := &CancelOrderByClientResultModel{} 91 | if err := rsp.ReadData(o); err != nil { 92 | t.Fatal(err) 93 | } 94 | t.Log(ToJsonString(o)) 95 | switch { 96 | case len(o.CancelledOrderId) == 0: 97 | t.Error("Empty key 'cancelledOrderId'") 98 | case len(o.ClientOid) == 0: 99 | t.Error("Empty key 'clientOid'") 100 | } 101 | } 102 | 103 | func TestApiService_CancelOrders(t *testing.T) { 104 | t.SkipNow() 105 | 106 | s := NewApiServiceFromEnv() 107 | rsp, err := s.CancelOrders(context.Background(), map[string]string{ 108 | "symbol": "ETH-BTC", 109 | "tradeType": "TRADE", 110 | }) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | o := &CancelOrderResultModel{} 115 | if err := rsp.ReadData(o); err != nil { 116 | t.Fatal(err) 117 | } 118 | t.Log(ToJsonString(o)) 119 | switch { 120 | case len(o.CancelledOrderIds) == 0: 121 | t.Error("Empty key 'cancelledOrderIds'") 122 | } 123 | } 124 | 125 | func TestApiService_Orders(t *testing.T) { 126 | s := NewApiServiceFromEnv() 127 | p := &PaginationParam{CurrentPage: 1, PageSize: 10} 128 | rsp, err := s.Orders(context.Background(), map[string]string{}, p) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | os := OrdersModel{} 134 | if _, err := rsp.ReadPaginationData(&os); err != nil { 135 | t.Fatal(err) 136 | } 137 | for _, o := range os { 138 | t.Log(ToJsonString(o)) 139 | switch { 140 | case o.Id == "": 141 | t.Error("Empty key 'id'") 142 | case o.Symbol == "": 143 | t.Error("Empty key 'symbol'") 144 | case o.OpType == "": 145 | t.Error("Empty key 'opType'") 146 | case o.Type == "": 147 | t.Error("Empty key 'type'") 148 | case o.Side == "": 149 | t.Error("Empty key 'side'") 150 | } 151 | } 152 | } 153 | 154 | func TestApiService_V1Orders(t *testing.T) { 155 | t.SkipNow() 156 | s := NewApiServiceFromEnv() 157 | p := &PaginationParam{CurrentPage: 1, PageSize: 10} 158 | rsp, err := s.V1Orders(context.Background(), map[string]string{}, p) 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | 163 | os := V1OrdersModel{} 164 | if _, err := rsp.ReadPaginationData(&os); err != nil { 165 | t.Fatal(err) 166 | } 167 | for _, o := range os { 168 | t.Log(ToJsonString(o)) 169 | switch { 170 | case o.Symbol == "": 171 | t.Error("Empty key 'symbol'") 172 | case o.DealPrice == "": 173 | t.Error("Empty key 'dealPrice'") 174 | case o.DealValue == "": 175 | t.Error("Empty key 'dealValue'") 176 | case o.Amount == "": 177 | t.Error("Empty key 'amount'") 178 | case o.Fee == "": 179 | t.Error("Empty key 'fee'") 180 | case o.Side == "": 181 | t.Error("Empty key 'side'") 182 | } 183 | } 184 | } 185 | 186 | func TestApiService_Order(t *testing.T) { 187 | s := NewApiServiceFromEnv() 188 | 189 | p := &PaginationParam{CurrentPage: 1, PageSize: 10} 190 | rsp, err := s.Orders(context.Background(), map[string]string{}, p) 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | 195 | os := OrdersModel{} 196 | if _, err := rsp.ReadPaginationData(&os); err != nil { 197 | t.Fatal(err) 198 | } 199 | if len(os) == 0 { 200 | t.SkipNow() 201 | } 202 | 203 | rsp, err = s.Order(context.Background(), os[0].Id) 204 | if err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | o := &OrderModel{} 209 | if err := rsp.ReadData(&o); err != nil { 210 | t.Fatal(err) 211 | } 212 | t.Log(ToJsonString(o)) 213 | switch { 214 | case o.Id == "": 215 | t.Error("Empty key 'id'") 216 | case o.Symbol == "": 217 | t.Error("Empty key 'symbol'") 218 | case o.OpType == "": 219 | t.Error("Empty key 'opType'") 220 | case o.Type == "": 221 | t.Error("Empty key 'type'") 222 | case o.Side == "": 223 | t.Error("Empty key 'side'") 224 | } 225 | } 226 | 227 | func TestApiService_RecentOrders(t *testing.T) { 228 | s := NewApiServiceFromEnv() 229 | rsp, err := s.RecentOrders(context.Background()) 230 | if err != nil { 231 | t.Fatal(err) 232 | } 233 | 234 | os := OrdersModel{} 235 | if err := rsp.ReadData(&os); err != nil { 236 | t.Fatal(err) 237 | } 238 | for _, o := range os { 239 | t.Log(ToJsonString(o)) 240 | switch { 241 | case o.Id == "": 242 | t.Error("Empty key 'id'") 243 | case o.Symbol == "": 244 | t.Error("Empty key 'symbol'") 245 | case o.OpType == "": 246 | t.Error("Empty key 'opType'") 247 | case o.Type == "": 248 | t.Error("Empty key 'type'") 249 | case o.Side == "": 250 | t.Error("Empty key 'side'") 251 | } 252 | } 253 | } 254 | 255 | func TestApiService_OrderByClient(t *testing.T) { 256 | t.SkipNow() 257 | 258 | s := NewApiServiceFromEnv() 259 | rsp, err := s.OrderByClient(context.Background(), "client id") 260 | if err != nil { 261 | t.Fatal(err) 262 | } 263 | 264 | o := &OrderModel{} 265 | if err := rsp.ReadData(&o); err != nil { 266 | t.Fatal(err) 267 | } 268 | t.Log(ToJsonString(o)) 269 | switch { 270 | case o.Id == "": 271 | t.Error("Empty key 'id'") 272 | case o.Symbol == "": 273 | t.Error("Empty key 'symbol'") 274 | case o.OpType == "": 275 | t.Error("Empty key 'opType'") 276 | case o.Type == "": 277 | t.Error("Empty key 'type'") 278 | case o.Side == "": 279 | t.Error("Empty key 'side'") 280 | } 281 | } 282 | 283 | func TestApiService_CreatMarginOrder(t *testing.T) { 284 | t.SkipNow() 285 | 286 | s := NewApiServiceFromEnv() 287 | p := &CreateOrderModel{ 288 | ClientOid: IntToString(time.Now().UnixNano()), 289 | Side: "buy", 290 | Symbol: "BTC-USDT", 291 | Price: "1", 292 | Size: "1", 293 | } 294 | rsp, err := s.CreateMarginOrder(context.Background(), p) 295 | if err != nil { 296 | t.Fatal(err) 297 | } 298 | o := &CreateOrderResultModel{} 299 | if err := rsp.ReadData(o); err != nil { 300 | t.Fatal(err) 301 | } 302 | t.Log(ToJsonString(o)) 303 | switch { 304 | case o.OrderId == "": 305 | t.Error("Empty key 'OrderId'") 306 | } 307 | } 308 | 309 | func TestApiService_CreateStopOrder(t *testing.T) { 310 | t.SkipNow() 311 | 312 | s := NewApiServiceFromEnv() 313 | p := &CreateOrderModel{ 314 | ClientOid: IntToString(time.Now().UnixNano()), 315 | Side: "buy", 316 | Symbol: "BTC-USDT", 317 | Price: "1", 318 | Size: "1", 319 | StopPrice: "10.0", 320 | } 321 | rsp, err := s.CreateStopOrder(context.Background(), p) 322 | if err != nil { 323 | t.Fatal(err) 324 | } 325 | o := &CreateOrderResultModel{} 326 | if err := rsp.ReadData(o); err != nil { 327 | t.Fatal(err) 328 | } 329 | t.Log(ToJsonString(o)) 330 | switch { 331 | case o.OrderId == "": 332 | t.Error("Empty key 'OrderId'") 333 | } 334 | } 335 | func TestApiService_CancelStopOrder(t *testing.T) { 336 | t.SkipNow() 337 | 338 | s := NewApiServiceFromEnv() 339 | rsp, err := s.CancelStopOrder(context.Background(), "xxxxx") 340 | if err != nil { 341 | t.Fatal(err) 342 | } 343 | o := &CancelOrderResultModel{} 344 | if err := rsp.ReadData(o); err != nil { 345 | t.Fatal(err) 346 | } 347 | t.Log(ToJsonString(o)) 348 | } 349 | 350 | func TestApiService_CancelStopOrderBy(t *testing.T) { 351 | t.SkipNow() 352 | 353 | s := NewApiServiceFromEnv() 354 | rsp, err := s.CancelStopOrderBy(context.Background(), map[string]string{"orderId": "xxxx"}) 355 | if err != nil { 356 | t.Fatal(err) 357 | } 358 | o := &CancelOrderResultModel{} 359 | if err := rsp.ReadData(o); err != nil { 360 | t.Fatal(err) 361 | } 362 | t.Log(ToJsonString(o)) 363 | } 364 | 365 | func TestApiService_StopOrder(t *testing.T) { 366 | s := NewApiServiceFromEnv() 367 | rsp, err := s.StopOrder(context.Background(), "vs8hoo98rathe2ak003ag5t9") 368 | if err != nil { 369 | t.Fatal(err) 370 | } 371 | o := &StopOrderModel{} 372 | if err := rsp.ReadData(o); err != nil { 373 | t.Fatal(err) 374 | } 375 | t.Log(ToJsonString(o)) 376 | } 377 | 378 | func TestApiService_StopOrderByClient(t *testing.T) { 379 | s := NewApiServiceFromEnv() 380 | rsp, err := s.StopOrderByClient(context.Background(), "1112", map[string]string{}) 381 | if err != nil { 382 | t.Fatal(err) 383 | } 384 | o := &StopOrderListModel{} 385 | t.Log(ToJsonString(rsp)) 386 | if err := rsp.ReadData(o); err != nil { 387 | t.Fatal(err) 388 | } 389 | t.Log(ToJsonString(o)) 390 | } 391 | 392 | func TestApiService_CancelStopOrderByClient(t *testing.T) { 393 | s := NewApiServiceFromEnv() 394 | rsp, err := s.CancelStopOrderByClient(context.Background(), "1112", map[string]string{}) 395 | if err != nil { 396 | t.Fatal(err) 397 | } 398 | o := &CancelStopOrderByClientModel{} 399 | t.Log(ToJsonString(rsp)) 400 | if err := rsp.ReadData(o); err != nil { 401 | t.Fatal(err) 402 | } 403 | t.Log(ToJsonString(o)) 404 | } 405 | 406 | func TestApiService_CreateOcoOrderModel(t *testing.T) { 407 | 408 | s := NewApiServiceFromEnv() 409 | p := &CreateOcoOrderModel{ 410 | Side: "buy", 411 | Symbol: "BTC-USDT", 412 | Price: "1", 413 | Size: "1", 414 | StopPrice: "100000", 415 | LimitPrice: "100002", 416 | TradeType: "TRADE", 417 | ClientOid: IntToString(time.Now().UnixNano()), 418 | Remark: "xx", 419 | } 420 | rsp, err := s.CreateOcoOrder(context.Background(), p) 421 | if err != nil { 422 | t.Fatal(err) 423 | } 424 | o := &CreateOrderResultModel{} 425 | if err := rsp.ReadData(o); err != nil { 426 | t.Fatal(err) 427 | } 428 | t.Log(ToJsonString(o)) 429 | switch { 430 | case o.OrderId == "": 431 | t.Error("Empty key 'OrderId'") 432 | } 433 | } 434 | 435 | func TestApiService_DeleteOcoOrder(t *testing.T) { 436 | 437 | s := NewApiServiceFromEnv() 438 | rsp, err := s.DeleteOcoOrder(context.Background(), "65d1c7042e6db70007e639b2") 439 | if err != nil { 440 | t.Fatal(err) 441 | } 442 | o := &CancelledOcoOrderResModel{} 443 | if err := rsp.ReadData(o); err != nil { 444 | t.Fatal(err) 445 | } 446 | t.Log(ToJsonString(o)) 447 | switch { 448 | case len(o.CancelledOrderIds) == 0: 449 | t.Error("Empty key 'cancelledOrderIds'") 450 | } 451 | } 452 | 453 | func TestApiService_DeleteOcoOrderClientId(t *testing.T) { 454 | 455 | s := NewApiServiceFromEnv() 456 | rsp, err := s.DeleteOcoOrderClientId(context.Background(), "order client id") 457 | if err != nil { 458 | t.Fatal(err) 459 | } 460 | o := &CancelledOcoOrderResModel{} 461 | if err := rsp.ReadData(o); err != nil { 462 | t.Fatal(err) 463 | } 464 | t.Log(ToJsonString(o)) 465 | switch { 466 | case len(o.CancelledOrderIds) == 0: 467 | t.Error("Empty key 'cancelledOrderIds'") 468 | } 469 | } 470 | 471 | func TestApiService_DeleteOcoOrders(t *testing.T) { 472 | 473 | s := NewApiServiceFromEnv() 474 | rsp, err := s.DeleteOcoOrders(context.Background(), "BTC-USDT", "") 475 | if err != nil { 476 | t.Fatal(err) 477 | } 478 | o := &CancelledOcoOrderResModel{} 479 | if err := rsp.ReadData(o); err != nil { 480 | t.Fatal(err) 481 | } 482 | t.Log(ToJsonString(o)) 483 | switch { 484 | case len(o.CancelledOrderIds) == 0: 485 | t.Error("Empty key 'cancelledOrderIds'") 486 | } 487 | } 488 | 489 | func TestApiService_OcoOrderDetail(t *testing.T) { 490 | 491 | s := NewApiServiceFromEnv() 492 | rsp, err := s.OcoOrderDetail(context.Background(), "65d1c7042e6db70007e639b2") 493 | if err != nil { 494 | t.Fatal(err) 495 | } 496 | 497 | o := &OrderDetailModel{} 498 | if err := rsp.ReadData(&o); err != nil { 499 | t.Fatal(err) 500 | } 501 | t.Log(ToJsonString(o)) 502 | } 503 | 504 | func TestApiService_OcoOrder(t *testing.T) { 505 | 506 | s := NewApiServiceFromEnv() 507 | rsp, err := s.OcoOrder(context.Background(), "65d1c7042e6db70007e639b2") 508 | if err != nil { 509 | t.Fatal(err) 510 | } 511 | 512 | o := &OcoOrderResModel{} 513 | if err := rsp.ReadData(&o); err != nil { 514 | t.Fatal(err) 515 | } 516 | t.Log(ToJsonString(o)) 517 | } 518 | 519 | func TestApiService_OcoClientOrder(t *testing.T) { 520 | 521 | s := NewApiServiceFromEnv() 522 | rsp, err := s.OcoClientOrder(context.Background(), "1708246787246002000") 523 | if err != nil { 524 | t.Fatal(err) 525 | } 526 | 527 | o := &OcoOrderResModel{} 528 | if err := rsp.ReadData(&o); err != nil { 529 | t.Fatal(err) 530 | } 531 | t.Log(ToJsonString(o)) 532 | } 533 | 534 | func TestApiService_OcoOrders(t *testing.T) { 535 | 536 | s := NewApiServiceFromEnv() 537 | p2 := &PaginationParam{CurrentPage: 1, PageSize: 10} 538 | p1 := map[string]string{ 539 | "symbol": "BTC-USDT", 540 | } 541 | rsp, err := s.OcoOrders(context.Background(), p1, p2) 542 | if err != nil { 543 | t.Fatal(err) 544 | } 545 | 546 | o := &OcoOrdersModel{} 547 | if _, err := rsp.ReadPaginationData(&o); err != nil { 548 | t.Fatal(err) 549 | } 550 | t.Log(ToJsonString(o)) 551 | } 552 | -------------------------------------------------------------------------------- /account_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestApiService_Accounts(t *testing.T) { 11 | s := NewApiServiceFromEnv() 12 | rsp, err := s.Accounts(context.Background(), "", "") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | cl := AccountsModel{} 17 | if err := rsp.ReadData(&cl); err != nil { 18 | t.Fatal(err) 19 | } 20 | for _, c := range cl { 21 | t.Log(ToJsonString(c)) 22 | switch { 23 | case c.Id == "": 24 | t.Error("Empty key 'id'") 25 | case c.Currency == "": 26 | t.Error("Empty key 'currency'") 27 | case c.Type == "": 28 | t.Error("Empty key 'type'") 29 | case c.Balance == "": 30 | t.Error("Empty key 'balance'") 31 | case c.Available == "": 32 | t.Error("Empty key 'available'") 33 | } 34 | } 35 | } 36 | 37 | func TestApiService_Account(t *testing.T) { 38 | s := NewApiServiceFromEnv() 39 | rsp, err := s.Accounts(context.Background(), "", "") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | cl := AccountsModel{} 44 | if err := rsp.ReadData(&cl); err != nil { 45 | t.Fatal(err) 46 | } 47 | if len(cl) == 0 { 48 | return 49 | } 50 | rsp, err = s.Account(context.Background(), cl[0].Id) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | a := &AccountModel{} 55 | if err := rsp.ReadData(a); err != nil { 56 | t.Fatal(err) 57 | } 58 | t.Log(ToJsonString(a)) 59 | switch { 60 | case a.Currency == "": 61 | t.Error("Empty key 'currency'") 62 | case a.Holds == "": 63 | t.Error("Empty key 'holds'") 64 | case a.Balance == "": 65 | t.Error("Empty key 'balance'") 66 | case a.Available == "": 67 | t.Error("Empty key 'available'") 68 | } 69 | } 70 | 71 | func TestApiService_SubAccountUsers(t *testing.T) { 72 | s := NewApiServiceFromEnv() 73 | rsp, err := s.SubAccountUsers(context.Background()) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | cl := SubAccountUsersModel{} 78 | if err := rsp.ReadData(&cl); err != nil { 79 | t.Fatal(err) 80 | } 81 | if len(cl) == 0 { 82 | return 83 | } 84 | for _, c := range cl { 85 | t.Log(ToJsonString(c)) 86 | switch { 87 | case c.UserId == "": 88 | t.Error("Empty key 'userId'") 89 | case c.SubName == "": 90 | t.Error("Empty key 'subName'") 91 | } 92 | } 93 | } 94 | 95 | func TestApiService_SubAccounts(t *testing.T) { 96 | s := NewApiServiceFromEnv() 97 | rsp, err := s.SubAccounts(context.Background()) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | cl := SubAccountsModel{} 102 | if err := rsp.ReadData(&cl); err != nil { 103 | t.Fatal(err) 104 | } 105 | if len(cl) == 0 { 106 | return 107 | } 108 | for _, c := range cl { 109 | t.Log(ToJsonString(c)) 110 | switch { 111 | case c.SubUserId == "": 112 | t.Error("Empty key 'subUserId'") 113 | case c.SubName == "": 114 | t.Error("Empty key 'subName'") 115 | } 116 | for _, b := range c.MainAccounts { 117 | switch { 118 | case b.Currency == "": 119 | t.Error("Empty key 'currency'") 120 | } 121 | } 122 | for _, b := range c.TradeAccounts { 123 | switch { 124 | case b.Currency == "": 125 | t.Error("Empty key 'currency'") 126 | } 127 | } 128 | } 129 | } 130 | 131 | func TestApiService_SubAccount(t *testing.T) { 132 | s := NewApiServiceFromEnv() 133 | 134 | ctx := context.Background() 135 | 136 | rsp, err := s.SubAccounts(context.Background()) 137 | if err != nil { 138 | t.Fatal(err) 139 | } 140 | cl := SubAccountsModel{} 141 | if err := rsp.ReadData(&cl); err != nil { 142 | t.Fatal(err) 143 | } 144 | if len(cl) == 0 { 145 | return 146 | } 147 | rsp, err = s.SubAccount(ctx, cl[0].SubUserId) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | a := SubAccountModel{} 152 | if err := rsp.ReadData(&a); err != nil { 153 | t.Fatal(err) 154 | } 155 | t.Log(ToJsonString(a)) 156 | switch { 157 | case a.SubUserId == "": 158 | t.Error("Empty key 'subUserId'") 159 | case a.SubName == "": 160 | t.Error("Empty key 'subName'") 161 | } 162 | for _, b := range a.MainAccounts { 163 | switch { 164 | case b.Currency == "": 165 | t.Error("Empty key 'currency'") 166 | } 167 | } 168 | for _, b := range a.TradeAccounts { 169 | switch { 170 | case b.Currency == "": 171 | t.Error("Empty key 'currency'") 172 | } 173 | } 174 | } 175 | 176 | func TestApiService_AccountsTransferable(t *testing.T) { 177 | s := NewApiServiceFromEnv() 178 | rsp, err := s.AccountsTransferable(context.Background(), "MATIC", "MAIN") 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | a := &AccountsTransferableModel{} 183 | if err := rsp.ReadData(a); err != nil { 184 | t.Fatal(err) 185 | } 186 | t.Log(ToJsonString(a)) 187 | } 188 | 189 | func TestApiService_CreateAccount(t *testing.T) { 190 | t.SkipNow() 191 | 192 | s := NewApiServiceFromEnv() 193 | rsp, err := s.CreateAccount(context.Background(), "trade", "BTC") 194 | if err != nil { 195 | t.Log(fmt.Sprintf("Create account failed: %s, %s", rsp.Code, rsp.Message)) 196 | t.Fatal(err) 197 | } 198 | if rsp.Code == "230005" { 199 | t.Log(fmt.Sprintf("Account exits: %s, %s", rsp.Code, rsp.Message)) 200 | return 201 | } 202 | a := &CreateAccountModel{} 203 | if err := rsp.ReadData(a); err != nil { 204 | t.Fatal(err) 205 | } 206 | t.Log(a.Id) 207 | switch { 208 | case a.Id == "": 209 | t.Error("Empty key 'id'") 210 | } 211 | } 212 | 213 | func TestApiService_AccountLedgers(t *testing.T) { 214 | s := NewApiServiceFromEnv() 215 | 216 | ctx := context.Background() 217 | 218 | rsp, err := s.Accounts(ctx, "", "") 219 | if err != nil { 220 | t.Fatal(err) 221 | } 222 | l := AccountsModel{} 223 | if err := rsp.ReadData(&l); err != nil { 224 | t.Fatal(err) 225 | } 226 | if len(l) == 0 { 227 | return 228 | } 229 | p := &PaginationParam{CurrentPage: 1, PageSize: 10} 230 | rsp, err = s.AccountLedgersV2(ctx, map[string]string{}, p) 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | hs := AccountLedgersModel{} 235 | if _, err := rsp.ReadPaginationData(&hs); err != nil { 236 | t.Fatal(err) 237 | } 238 | for _, h := range hs { 239 | t.Log(ToJsonString(h)) 240 | switch { 241 | case h.Currency == "": 242 | t.Error("Empty key 'currency'") 243 | case h.Amount == "": 244 | t.Error("Empty key 'amount'") 245 | case h.Fee == "": 246 | t.Error("Empty key 'fee'") 247 | case h.Balance == "": 248 | t.Error("Empty key 'balance'") 249 | case h.BizType == "": 250 | t.Error("Empty key 'bizType'") 251 | case h.Direction == "": 252 | t.Error("Empty key 'direction'") 253 | case h.CreatedAt == 0: 254 | t.Error("Empty key 'createdAt'") 255 | } 256 | } 257 | } 258 | 259 | func TestApiService_AccountHolds(t *testing.T) { 260 | s := NewApiServiceFromEnv() 261 | 262 | ctx := context.Background() 263 | 264 | rsp, err := s.Accounts(ctx, "", "") 265 | if err != nil { 266 | t.Fatal(err) 267 | } 268 | l := AccountsModel{} 269 | if err := rsp.ReadData(&l); err != nil { 270 | t.Fatal(err) 271 | } 272 | if len(l) == 0 { 273 | return 274 | } 275 | p := &PaginationParam{CurrentPage: 1, PageSize: 10} 276 | rsp, err = s.AccountHolds(ctx, l[0].Id, p) 277 | if err != nil { 278 | t.Fatal(err) 279 | } 280 | hs := AccountHoldsModel{} 281 | if _, err := rsp.ReadPaginationData(&hs); err != nil { 282 | t.Fatal(err) 283 | } 284 | for _, h := range hs { 285 | t.Log(ToJsonString(h)) 286 | switch { 287 | case h.Currency == "": 288 | t.Error("Empty key 'currency'") 289 | case h.HoldAmount == "": 290 | t.Error("Empty key 'holdAmount'") 291 | case h.BizType == "": 292 | t.Error("Empty key 'bizType'") 293 | case h.OrderId == "": 294 | t.Error("Empty key 'orderId'") 295 | case h.CreatedAt == 0: 296 | t.Error("Empty key 'createdAt'") 297 | case h.UpdatedAt == 0: 298 | t.Error("Empty key 'updatedAt'") 299 | } 300 | } 301 | } 302 | 303 | func TestApiService_InnerTransferV2(t *testing.T) { 304 | t.SkipNow() 305 | 306 | s := NewApiServiceFromEnv() 307 | clientOid := IntToString(time.Now().Unix()) 308 | rsp, err := s.InnerTransferV2(context.Background(), clientOid, "KCS", "main", "trade", "2") 309 | if err != nil { 310 | t.Fatal(err) 311 | } 312 | v := &InnerTransferResultModel{} 313 | if err := rsp.ReadData(v); err != nil { 314 | t.Fatal(err) 315 | } 316 | t.Log(ToJsonString(v)) 317 | if v.OrderId == "" { 318 | t.Error("Empty key 'orderId'") 319 | } 320 | } 321 | 322 | func TestApiService_SubTransferV2(t *testing.T) { 323 | t.SkipNow() 324 | 325 | s := NewApiServiceFromEnv() 326 | clientOid := IntToString(time.Now().Unix()) 327 | p := map[string]string{ 328 | "clientOid": clientOid, 329 | "currency": "MATIC", 330 | "amount": "9", 331 | "direction": "OUT", 332 | "accountType": "MAIN", 333 | "subAccountType": "MAIN", 334 | "subUserId": "6482f1e32ba86200010eb03e", 335 | } 336 | rsp, err := s.SubTransferV2(context.Background(), p) 337 | if err != nil { 338 | t.Fatal(err) 339 | } 340 | v := &SubTransferResultModel{} 341 | if err := rsp.ReadData(v); err != nil { 342 | t.Fatal(err) 343 | } 344 | if v.OrderId == "" { 345 | t.Error("Empty key 'orderId'") 346 | } 347 | } 348 | 349 | func TestBaseFee(t *testing.T) { 350 | s := NewApiServiceFromEnv() 351 | rsp, err := s.BaseFee(context.Background(), "1") 352 | if err != nil { 353 | t.Fatal(err) 354 | } 355 | 356 | v := &BaseFeeModel{} 357 | if err := rsp.ReadData(v); err != nil { 358 | t.Fatal(err) 359 | } 360 | 361 | t.Log(v) 362 | t.Log(ToJsonString(v)) 363 | } 364 | 365 | func TestActualFee(t *testing.T) { 366 | s := NewApiServiceFromEnv() 367 | rsp, err := s.ActualFee(context.Background(), "BTC-USDT") 368 | if err != nil { 369 | t.Fatal(err) 370 | } 371 | 372 | v := &TradeFeesResultModel{} 373 | if err := rsp.ReadData(v); err != nil { 374 | t.Fatal(err) 375 | } 376 | 377 | t.Log(v) 378 | t.Log(ToJsonString(v)) 379 | } 380 | 381 | func TestApiService_SubAccountUsersV2(t *testing.T) { 382 | s := NewApiServiceFromEnv() 383 | pp := PaginationParam{ 384 | CurrentPage: 1, 385 | PageSize: 2, 386 | } 387 | rsp, err := s.SubAccountUsersV2(context.Background(), &pp) 388 | if err != nil { 389 | t.Fatal(err) 390 | } 391 | cl := SubAccountUsersModelV2{} 392 | if _, err := rsp.ReadPaginationData(&cl); err != nil { 393 | t.Fatal(err) 394 | } 395 | if len(cl) == 0 { 396 | return 397 | } 398 | for _, c := range cl { 399 | t.Log(ToJsonString(c)) 400 | switch { 401 | case c.UserId == "": 402 | t.Error("Empty key 'userId'") 403 | case c.SubName == "": 404 | t.Error("Empty key 'subName'") 405 | } 406 | } 407 | } 408 | 409 | func TestApiService_UserInfoV2(t *testing.T) { 410 | s := NewApiServiceFromEnv() 411 | rsp, err := s.UserSummaryInfoV2(context.Background()) 412 | if err != nil { 413 | t.Fatal(err) 414 | } 415 | cl := UserSummaryInfoModelV2{} 416 | if err := rsp.ReadData(&cl); err != nil { 417 | t.Fatal(err) 418 | } 419 | t.Log(cl) 420 | t.Log(ToJsonString(cl)) 421 | } 422 | 423 | func TestApiService_CreateSubAccountV2(t *testing.T) { 424 | subName := "marginFen1991" 425 | s := NewApiServiceFromEnv() 426 | rsp, err := s.CreateSubAccountV2(context.Background(), "Youaremine1314.", "", subName, "Margin") 427 | if err != nil { 428 | t.Fatal(err) 429 | } 430 | cl := CreateSubAccountV2Res{} 431 | if err := rsp.ReadData(&cl); err != nil { 432 | t.Fatal(err) 433 | } 434 | t.Log(cl) 435 | t.Log(ToJsonString(cl)) 436 | 437 | if cl.SubName != subName { 438 | t.Error("Create sub account v2 fail") 439 | } 440 | } 441 | 442 | func TestApiService_SubApiKey(t *testing.T) { 443 | subName := "TestSubAccount1Fen" 444 | s := NewApiServiceFromEnv() 445 | rsp, err := s.SubApiKey(context.Background(), subName, "") 446 | if err != nil { 447 | t.Fatal(err) 448 | } 449 | cl := SubApiKeyRes{} 450 | if err := rsp.ReadData(&cl); err != nil { 451 | t.Fatal(err) 452 | } 453 | t.Log(cl) 454 | t.Log(ToJsonString(cl)) 455 | } 456 | 457 | func TestApiService_CreateSubApiKey(t *testing.T) { 458 | t.SkipNow() 459 | s := NewApiServiceFromEnv() 460 | rsp, err := s.CreateSubApiKey(context.Background(), "TestSubAccount3Fen", "123abcABC", "3", "General", "", "") 461 | if err != nil { 462 | t.Fatal(err) 463 | } 464 | cl := CreateSubApiKeyRes{} 465 | if err := rsp.ReadData(&cl); err != nil { 466 | t.Fatal(err) 467 | } 468 | t.Log(cl) 469 | t.Log(ToJsonString(cl)) 470 | } 471 | 472 | func TestApiService_UpdateSubApiKey(t *testing.T) { 473 | t.SkipNow() 474 | s := NewApiServiceFromEnv() 475 | rsp, err := s.UpdateSubApiKey(context.Background(), "TestSubAccount1Fen", "123abcABC", "648804c835848e0001690fb9", "Trade", "", "30") 476 | if err != nil { 477 | t.Fatal(err) 478 | } 479 | cl := UpdateSubApiKeyRes{} 480 | if err := rsp.ReadData(&cl); err != nil { 481 | t.Fatal(err) 482 | } 483 | t.Log(cl) 484 | t.Log(ToJsonString(cl)) 485 | } 486 | 487 | func TestApiService_DeleteSubApiKey(t *testing.T) { 488 | t.SkipNow() 489 | s := NewApiServiceFromEnv() 490 | rsp, err := s.DeleteSubApiKey(context.Background(), "TestSubAccount3Fen", "123abcABC", "6497fc7c19a9ea0001d7ac46") 491 | if err != nil { 492 | t.Fatal(err) 493 | } 494 | cl := UpdateSubApiKeyRes{} 495 | if err := rsp.ReadData(&cl); err != nil { 496 | t.Fatal(err) 497 | } 498 | t.Log(cl) 499 | t.Log(ToJsonString(cl)) 500 | } 501 | 502 | func TestApiService_SubAccountsV2(t *testing.T) { 503 | s := NewApiServiceFromEnv() 504 | 505 | pp := PaginationParam{ 506 | CurrentPage: 1, 507 | PageSize: 10, 508 | } 509 | rsp, err := s.SubAccountsV2(context.Background(), &pp) 510 | if err != nil { 511 | t.Fatal(err) 512 | } 513 | cl := SubAccountsModel{} 514 | if _, err := rsp.ReadPaginationData(&cl); err != nil { 515 | t.Fatal(err) 516 | } 517 | if len(cl) == 0 { 518 | return 519 | } 520 | for _, c := range cl { 521 | t.Log(ToJsonString(c)) 522 | switch { 523 | case c.SubUserId == "": 524 | t.Error("Empty key 'subUserId'") 525 | case c.SubName == "": 526 | t.Error("Empty key 'subName'") 527 | } 528 | for _, b := range c.MainAccounts { 529 | switch { 530 | case b.Currency == "": 531 | t.Error("Empty key 'currency'") 532 | } 533 | } 534 | for _, b := range c.TradeAccounts { 535 | switch { 536 | case b.Currency == "": 537 | t.Error("Empty key 'currency'") 538 | } 539 | } 540 | } 541 | } 542 | 543 | func TestApiService_UniversalTransfer(t *testing.T) { 544 | p := &UniversalTransferReq{ 545 | ClientOid: IntToString(time.Now().Unix()), 546 | Type: "INTERNAL", 547 | Currency: "USDT", 548 | Amount: "5", 549 | FromAccountType: "TRADE", 550 | ToAccountType: "CONTRACT", 551 | } 552 | s := NewApiServiceFromEnv() 553 | rsp, err := s.UniversalTransfer(context.Background(), p) 554 | if err != nil { 555 | t.Fatal(err) 556 | } 557 | v := &UniversalTransferRes{} 558 | if err := rsp.ReadData(v); err != nil { 559 | t.Fatal(err) 560 | } 561 | t.Log(ToJsonString(v)) 562 | if v.OrderId == "" { 563 | t.Error("Empty key 'orderId'") 564 | } 565 | } 566 | -------------------------------------------------------------------------------- /hf_order.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/google/go-querystring/query" 10 | ) 11 | 12 | // HfPlaceOrder There are two types of orders: 13 | // (limit) order: set price and quantity for the transaction. 14 | // (market) order : set amount or quantity for the transaction. 15 | func (as *ApiService) HfPlaceOrder(ctx context.Context, params map[string]string) (*ApiResponse, error) { 16 | req := NewRequest(http.MethodPost, "/api/v1/hf/orders", params) 17 | return as.Call(ctx, req) 18 | } 19 | 20 | // HfSyncPlaceOrder The difference between this interface 21 | // and "Place hf order" is that this interface will synchronously 22 | // return the order information after the order matching is completed. 23 | // For higher latency requirements, please select the "Place hf order" interface. 24 | // If there is a requirement for returning data integrity, please select this interface 25 | func (as *ApiService) HfSyncPlaceOrder(ctx context.Context, params map[string]string) (*ApiResponse, error) { 26 | req := NewRequest(http.MethodPost, "/api/v1/hf/orders/sync", params) 27 | return as.Call(ctx, req) 28 | } 29 | 30 | // HfPlaceMultiOrders This endpoint supports sequential batch order placement from a single endpoint. 31 | // A maximum of 5orders can be placed simultaneously. 32 | // The order types must be limit orders of the same trading pair 33 | // (this endpoint currently only supports spot trading and does not support margin trading) 34 | func (as *ApiService) HfPlaceMultiOrders(ctx context.Context, orders []*HFCreateMultiOrderModel) (*ApiResponse, error) { 35 | p := map[string]interface{}{ 36 | "orderList": orders, 37 | } 38 | req := NewRequest(http.MethodPost, "/api/v1/hf/orders/multi", p) 39 | return as.Call(ctx, req) 40 | } 41 | 42 | // HfSyncPlaceMultiOrders The request parameters of this interface 43 | // are the same as those of the "Sync place multiple hf orders" interface 44 | // The difference between this interface and "Sync place multiple hf orders" is that 45 | // this interface will synchronously return the order information after the order matching is completed. 46 | func (as *ApiService) HfSyncPlaceMultiOrders(ctx context.Context, orders []*HFCreateMultiOrderModel) (*ApiResponse, error) { 47 | p := map[string]interface{}{ 48 | "orderList": orders, 49 | } 50 | req := NewRequest(http.MethodPost, "/api/v1/hf/orders/multi/sync", p) 51 | return as.Call(ctx, req) 52 | } 53 | 54 | type HfSyncPlaceMultiOrdersRes []*HfSyncPlaceOrderRes 55 | 56 | // HfModifyOrder 57 | // This interface can modify the price and quantity of the order according to orderId or clientOid. 58 | func (as *ApiService) HfModifyOrder(ctx context.Context, params map[string]string) (*ApiResponse, error) { 59 | req := NewRequest(http.MethodPost, "/api/v1/hf/orders/alter", params) 60 | return as.Call(ctx, req) 61 | } 62 | 63 | // HfCancelOrder This endpoint can be used to cancel a high-frequency order by orderId. 64 | func (as *ApiService) HfCancelOrder(ctx context.Context, orderId, symbol string) (*ApiResponse, error) { 65 | p := map[string]string{ 66 | "symbol": symbol, 67 | } 68 | req := NewRequest(http.MethodDelete, "/api/v1/hf/orders/"+orderId, p) 69 | return as.Call(ctx, req) 70 | } 71 | 72 | // HfSyncCancelOrder The difference between this interface and "Cancel orders by orderId" is that 73 | // this interface will synchronously return the order information after the order canceling is completed. 74 | func (as *ApiService) HfSyncCancelOrder(ctx context.Context, orderId, symbol string) (*ApiResponse, error) { 75 | p := map[string]string{ 76 | "symbol": symbol, 77 | } 78 | req := NewRequest(http.MethodDelete, "/api/v1/hf/orders/sync/"+orderId, p) 79 | return as.Call(ctx, req) 80 | } 81 | 82 | // HfCancelOrderByClientId This endpoint sends out a request to cancel a high-frequency order using clientOid. 83 | func (as *ApiService) HfCancelOrderByClientId(ctx context.Context, clientOid, symbol string) (*ApiResponse, error) { 84 | p := map[string]string{ 85 | "symbol": symbol, 86 | } 87 | req := NewRequest(http.MethodDelete, "/api/v1/hf/orders/client-order/"+clientOid, p) 88 | return as.Call(ctx, req) 89 | } 90 | 91 | // HfSyncCancelOrderByClientId The difference between this interface and "Cancellation of order by clientOid" 92 | // is that this interface will synchronously return the order information after the order canceling is completed. 93 | func (as *ApiService) HfSyncCancelOrderByClientId(ctx context.Context, clientOid, symbol string) (*ApiResponse, error) { 94 | p := map[string]string{ 95 | "symbol": symbol, 96 | } 97 | req := NewRequest(http.MethodDelete, "/api/v1/hf/orders/sync/client-order/"+clientOid, p) 98 | return as.Call(ctx, req) 99 | } 100 | 101 | // HfSyncCancelOrderWithSize This interface can cancel the specified quantity of the order according to the orderId. 102 | func (as *ApiService) HfSyncCancelOrderWithSize(ctx context.Context, orderId, symbol, cancelSize string) (*ApiResponse, error) { 103 | p := map[string]string{ 104 | "symbol": symbol, 105 | "cancelSize": cancelSize, 106 | } 107 | req := NewRequest(http.MethodDelete, "/api/v1/hf/orders/cancel/"+orderId, p) 108 | return as.Call(ctx, req) 109 | } 110 | 111 | // HfSyncCancelAllOrders his endpoint allows cancellation of all orders related to a specific trading pair 112 | // with a status of open 113 | // (including all orders pertaining to high-frequency trading accounts and non-high-frequency trading accounts) 114 | func (as *ApiService) HfSyncCancelAllOrders(ctx context.Context, symbol string) (*ApiResponse, error) { 115 | p := map[string]string{ 116 | "symbol": symbol, 117 | } 118 | req := NewRequest(http.MethodDelete, "/api/v1/hf/orders", p) 119 | return as.Call(ctx, req) 120 | } 121 | 122 | // HfObtainActiveOrders This endpoint obtains a list of all active HF orders. 123 | // The return data is sorted in descending order based on the latest update times. 124 | func (as *ApiService) HfObtainActiveOrders(ctx context.Context, symbol string) (*ApiResponse, error) { 125 | p := map[string]string{ 126 | "symbol": symbol, 127 | } 128 | req := NewRequest(http.MethodGet, "/api/v1/hf/orders/active", p) 129 | return as.Call(ctx, req) 130 | } 131 | 132 | // HfObtainActiveSymbols This interface can query all trading pairs that the user has active orders 133 | func (as *ApiService) HfObtainActiveSymbols(ctx context.Context) (*ApiResponse, error) { 134 | req := NewRequest(http.MethodGet, "/api/v1/hf/orders/active/symbols", nil) 135 | return as.Call(ctx, req) 136 | } 137 | 138 | type HfSymbolsModel struct { 139 | Symbols []string `json:"symbols"` 140 | } 141 | 142 | // HfObtainFilledOrders This endpoint obtains a list of filled HF orders and returns paginated data. 143 | // The returned data is sorted in descending order based on the latest order update times. 144 | func (as *ApiService) HfObtainFilledOrders(ctx context.Context, p map[string]string) (*ApiResponse, error) { 145 | req := NewRequest(http.MethodGet, "/api/v1/hf/orders/done", p) 146 | return as.Call(ctx, req) 147 | } 148 | 149 | type HfFilledOrdersModel struct { 150 | LastId json.Number `json:"lastId"` 151 | Items []*HfOrderModel `json:"items"` 152 | } 153 | 154 | // HfOrderDetail This endpoint can be used to obtain information for a single HF order using the order id. 155 | func (as *ApiService) HfOrderDetail(ctx context.Context, orderId, symbol string) (*ApiResponse, error) { 156 | p := map[string]string{ 157 | "symbol": symbol, 158 | } 159 | req := NewRequest(http.MethodGet, "/api/v1/hf/orders/"+orderId, p) 160 | return as.Call(ctx, req) 161 | } 162 | 163 | // HfOrderDetailByClientOid The endpoint can be used to obtain information about a single order using clientOid. 164 | // If the order does not exist, then there will be a prompt saying that the order does not exist. 165 | func (as *ApiService) HfOrderDetailByClientOid(ctx context.Context, clientOid, symbol string) (*ApiResponse, error) { 166 | p := map[string]string{ 167 | "symbol": symbol, 168 | } 169 | req := NewRequest(http.MethodGet, "/api/v1/hf/orders/client-order/"+clientOid, p) 170 | return as.Call(ctx, req) 171 | } 172 | 173 | // HfAutoCancelSetting automatically cancel all orders of the set trading pair after the specified time. 174 | // If this interface is not called again for renewal or cancellation before the set time, 175 | // the system will help the user to cancel the order of the corresponding trading pair. 176 | // otherwise it will not. 177 | func (as *ApiService) HfAutoCancelSetting(ctx context.Context, timeout int64, symbol string) (*ApiResponse, error) { 178 | p := map[string]interface{}{ 179 | "symbol": symbol, 180 | "timeout": timeout, 181 | } 182 | req := NewRequest(http.MethodPost, "/api/v1/hf/orders/dead-cancel-all", p) 183 | return as.Call(ctx, req) 184 | } 185 | 186 | // HfQueryAutoCancelSetting Through this interface, you can query the settings of automatic order cancellation 187 | func (as *ApiService) HfQueryAutoCancelSetting(ctx context.Context) (*ApiResponse, error) { 188 | req := NewRequest(http.MethodGet, "/api/v1/hf/orders/dead-cancel-all/query", nil) 189 | return as.Call(ctx, req) 190 | } 191 | 192 | // HfTransactionDetails This endpoint can be used to obtain a list of the latest HF transaction details. 193 | // The returned results are paginated. The data is sorted in descending order according to time. 194 | func (as *ApiService) HfTransactionDetails(ctx context.Context, p map[string]string) (*ApiResponse, error) { 195 | req := NewRequest(http.MethodGet, "/api/v1/hf/fills", p) 196 | return as.Call(ctx, req) 197 | } 198 | 199 | // HfCancelOrders This endpoint can be used to cancel all hf orders. return HfCancelOrdersResultModel 200 | func (as *ApiService) HfCancelOrders(ctx context.Context) (*ApiResponse, error) { 201 | req := NewRequest(http.MethodDelete, "/api/v1/hf/orders/cancelAll", nil) 202 | return as.Call(ctx, req) 203 | } 204 | 205 | func (as *ApiService) HfPlaceOrderTest(ctx context.Context, p *HfPlaceOrderReq) (*ApiResponse, error) { 206 | req := NewRequest(http.MethodPost, "/api/v1/hf/orders/test", p) 207 | return as.Call(ctx, req) 208 | } 209 | 210 | func (as *ApiService) HfMarginActiveSymbols(ctx context.Context, tradeType string) (*ApiResponse, error) { 211 | p := map[string]string{ 212 | "tradeType": tradeType, 213 | } 214 | req := NewRequest(http.MethodGet, "/api/v3/hf/margin/order/active/symbols", p) 215 | return as.Call(ctx, req) 216 | } 217 | 218 | // HfCreateMarinOrderV3 This interface is used to place cross-margin or isolated-margin high-frequency margin trading 219 | func (as *ApiService) HfCreateMarinOrderV3(ctx context.Context, p *HfMarginOrderV3Req) (*ApiResponse, error) { 220 | req := NewRequest(http.MethodPost, "/api/v3/hf/margin/order", p) 221 | return as.Call(ctx, req) 222 | } 223 | 224 | // HfCreateMarinOrderTestV3 Order test endpoint, the request parameters and return parameters of this endpoint are exactly the same as the order endpoint, 225 | // and can be used to verify whether the signature is correct and other operations. After placing an order, 226 | // the order will not enter the matching system, and the order cannot be queried. 227 | func (as *ApiService) HfCreateMarinOrderTestV3(ctx context.Context, p *HfMarginOrderV3Req) (*ApiResponse, error) { 228 | req := NewRequest(http.MethodPost, "/api/v3/hf/margin/order/test", p) 229 | return as.Call(ctx, req) 230 | } 231 | 232 | // HfCancelMarinOrderV3 Cancel a single order by orderId. If the order cannot be canceled (sold or canceled), 233 | // an error message will be returned, and the reason can be obtained according to the returned msg. 234 | func (as *ApiService) HfCancelMarinOrderV3(ctx context.Context, p *HfCancelMarinOrderV3Req) (*ApiResponse, error) { 235 | req := NewRequest(http.MethodDelete, fmt.Sprintf("/api/v3/hf/margin/orders/%s?symbol=%s", p.OrderId, p.Symbol), nil) 236 | return as.Call(ctx, req) 237 | } 238 | 239 | // HfCancelClientMarinOrderV3 Cancel a single order by clientOid. 240 | func (as *ApiService) HfCancelClientMarinOrderV3(ctx context.Context, p *HfCancelClientMarinOrderV3Req) (*ApiResponse, error) { 241 | req := NewRequest(http.MethodDelete, fmt.Sprintf("/api/v3/hf/margin/orders/client-order/%s?symbol=%s", p.ClientOid, p.Symbol), nil) 242 | return as.Call(ctx, req) 243 | } 244 | 245 | // HfCancelAllMarginOrdersV3 This endpoint only sends cancellation requests. 246 | // The results of the requests must be obtained by checking the order detail or subscribing to websocket. 247 | func (as *ApiService) HfCancelAllMarginOrdersV3(ctx context.Context, p *HfCancelAllMarginOrdersV3Req) (*ApiResponse, error) { 248 | v, err := query.Values(p) 249 | if err != nil { 250 | return nil, err 251 | } 252 | req := NewRequest(http.MethodDelete, "/api/v3/hf/margin/orders", v) 253 | return as.Call(ctx, req) 254 | } 255 | 256 | // HfMarinActiveOrdersV3 This interface is to obtain all active hf margin order lists, 257 | // and the return value of the active order interface is the paged data of all uncompleted order lists. 258 | func (as *ApiService) HfMarinActiveOrdersV3(ctx context.Context, p *HfMarinActiveOrdersV3Req) (*ApiResponse, error) { 259 | v, err := query.Values(p) 260 | if err != nil { 261 | return nil, err 262 | } 263 | req := NewRequest(http.MethodGet, "/api/v3/hf/margin/orders/active", v) 264 | return as.Call(ctx, req) 265 | } 266 | 267 | // HfMarinDoneOrdersV3 This endpoint obtains a list of filled margin HF orders and returns paginated data. 268 | // The returned data is sorted in descending order based on the latest order update times. 269 | func (as *ApiService) HfMarinDoneOrdersV3(ctx context.Context, p *HfMarinDoneOrdersV3Req) (*ApiResponse, error) { 270 | v, err := query.Values(p) 271 | if err != nil { 272 | return nil, err 273 | } 274 | req := NewRequest(http.MethodGet, "/api/v3/hf/margin/orders/done", v) 275 | return as.Call(ctx, req) 276 | } 277 | 278 | // HfMarinOrderV3 This endpoint can be used to obtain information for a single margin HF order using the order id. 279 | func (as *ApiService) HfMarinOrderV3(ctx context.Context, p *HfMarinOrderV3Req) (*ApiResponse, error) { 280 | req := NewRequest(http.MethodGet, fmt.Sprintf("/api/v3/hf/margin/orders/%s?symbol=%s", p.OrderId, p.Symbol), nil) 281 | return as.Call(ctx, req) 282 | } 283 | 284 | // HfMarinClientOrderV3 This endpoint can be used to obtain information for a single margin HF order using the clientOid. 285 | func (as *ApiService) HfMarinClientOrderV3(ctx context.Context, p *HfMarinClientOrderV3Req) (*ApiResponse, error) { 286 | req := NewRequest(http.MethodGet, fmt.Sprintf("/api/v3/hf/margin/orders/client-order/%s?symbol=%s", p.ClientOid, p.Symbol), nil) 287 | return as.Call(ctx, req) 288 | } 289 | 290 | // HfMarinFillsV3 This endpoint can be used to obtain a list of the latest margin HF transaction details. 291 | // The returned results are paginated. 292 | // The data is sorted in descending order according to time. 293 | func (as *ApiService) HfMarinFillsV3(ctx context.Context, p *HfMarinFillsV3Req) (*ApiResponse, error) { 294 | v, err := query.Values(p) 295 | if err != nil { 296 | return nil, err 297 | } 298 | req := NewRequest(http.MethodGet, "/api/v3/hf/margin/fills", v) 299 | return as.Call(ctx, req) 300 | } 301 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice: SDK Deprecation 2 | 3 | Thank you for your support and usage of this SDK. We want to inform you that **this project is no longer actively maintained or updated**. 4 | 5 | To ensure you have access to the latest features, improvements, and support, we recommend transitioning to our new SDK: [**KuCoin Universal SDK**](https://github.com/Kucoin/kucoin-universal-sdk). 6 | 7 | The KuCoin Universal SDK offers: 8 | - A unified architecture across multiple programming languages. 9 | - Enhanced performance and stability. 10 | - Continued support and updates. 11 | 12 | 👉 **New SDK Repository**: [https://github.com/Kucoin/kucoin-universal-sdk](https://github.com/Kucoin/kucoin-universal-sdk) 13 | 14 | We appreciate your understanding and encourage you to migrate to the new SDK for a better development experience. Should you have any questions or require assistance, feel free to reach out to us. 15 | 16 | 17 | 18 | # Go SDK for KuCoin API 19 | > The detailed document [https://docs.kucoin.com](https://docs.kucoin.com), in order to receive the latest API change notifications, please `Watch` this repository. 20 | 21 | [![Latest Version](https://img.shields.io/github/release/Kucoin/kucoin-go-sdk.svg)](https://github.com/Kucoin/kucoin-go-sdk/releases) 22 | [![GoDoc](https://godoc.org/github.com/Kucoin/kucoin-go-sdk?status.svg)](https://godoc.org/github.com/Kucoin/kucoin-go-sdk) 23 | [![Build Status](https://travis-ci.org/Kucoin/kucoin-go-sdk.svg?branch=master)](https://travis-ci.org/Kucoin/kucoin-go-sdk) 24 | [![Go Report Card](https://goreportcard.com/badge/github.com/Kucoin/kucoin-go-sdk)](https://goreportcard.com/report/github.com/Kucoin/kucoin-go-sdk) 25 | [![Sourcegraph](https://sourcegraph.com/github.com/Kucoin/kucoin-go-sdk/-/badge.svg)](https://sourcegraph.com/github.com/Kucoin/kucoin-go-sdk?badge) 26 | 27 | 28 | 29 | ## Install 30 | 31 | ```bash 32 | go get github.com/Kucoin/kucoin-go-sdk 33 | ``` 34 | 35 | ## Usage 36 | 37 | ### Choose environment 38 | 39 | | Environment | BaseUri | 40 | | -------- | -------- | 41 | | *Production* | `https://api.kucoin.com(DEFAULT)` `https://api.kucoin.cc` | 42 | | *Sandbox* | `https://openapi-sandbox.kucoin.com` | 43 | 44 | ### Create ApiService 45 | 46 | ###### **Note** 47 | To reinforce the security of the APIS, KuCoin upgraded the API key to version 2.0, the validation logic has also been changed. It is recommended to create(https://www.kucoin.com/account/api) and update your API key to version 2.0. 48 | The API key of version 1.0 will be still valid until May 1, 2021. 49 | 50 | ```go 51 | // API key version 2.0 52 | s := kucoin.NewApiService( 53 | // kucoin.ApiBaseURIOption("https://api.kucoin.com"), 54 | kucoin.ApiKeyOption("key"), 55 | kucoin.ApiSecretOption("secret"), 56 | kucoin.ApiPassPhraseOption("passphrase"), 57 | kucoin.ApiKeyVersionOption(ApiKeyVersionV2) 58 | ) 59 | 60 | // API key version 1.0 61 | s := kucoin.NewApiService( 62 | // kucoin.ApiBaseURIOption("https://api.kucoin.com"), 63 | kucoin.ApiKeyOption("key"), 64 | kucoin.ApiSecretOption("secret"), 65 | kucoin.ApiPassPhraseOption("passphrase"), 66 | ) 67 | // Or add these options into the environmental variable 68 | // Bash: 69 | // export API_BASE_URI=https://api.kucoin.com 70 | // export API_KEY=key 71 | // export API_SECRET=secret 72 | // export API_PASSPHRASE=passphrase 73 | // export API_KEY_VERSION=2 74 | // s := NewApiServiceFromEnv() 75 | ``` 76 | 77 | ### Debug mode & logging 78 | 79 | ```go 80 | // Require package github.com/sirupsen/logrus 81 | // Debug mode will record the logs of API and WebSocket to files. 82 | // Default values: LogLevel=logrus.DebugLevel, LogDirectory="/tmp" 83 | kucoin.DebugMode = true 84 | // Or export API_DEBUG_MODE=1 85 | 86 | // Logging in your code 87 | // kucoin.SetLoggerDirectory("/tmp") 88 | // logrus.SetLevel(logrus.DebugLevel) 89 | logrus.Debugln("I'm a debug message") 90 | ``` 91 | 92 | ### Examples 93 | > See the test case for more examples. 94 | 95 | #### Example of API `without` authentication 96 | 97 | ```go 98 | rsp, err := s.ServerTime() 99 | if err != nil { 100 | log.Printf("Error: %s", err.Error()) 101 | // Handle error 102 | return 103 | } 104 | 105 | var ts int64 106 | if err := rsp.ReadData(&ts); err != nil { 107 | // Handle error 108 | return 109 | } 110 | log.Printf("The server time: %d", ts) 111 | ``` 112 | 113 | #### Example of API `with` authentication 114 | 115 | ```go 116 | // Without pagination 117 | rsp, err := s.Accounts("", "") 118 | if err != nil { 119 | // Handle error 120 | return 121 | } 122 | 123 | as := kucoin.AccountsModel{} 124 | if err := rsp.ReadData(&as); err != nil { 125 | // Handle error 126 | return 127 | } 128 | 129 | for _, a := range as { 130 | log.Printf("Available balance: %s %s => %s", a.Type, a.Currency, a.Available) 131 | } 132 | ``` 133 | 134 | ```go 135 | // Handle pagination 136 | rsp, err := s.Orders(map[string]string{}, &kucoin.PaginationParam{CurrentPage: 1, PageSize: 10}) 137 | if err != nil { 138 | // Handle error 139 | return 140 | } 141 | 142 | os := kucoin.OrdersModel{} 143 | pa, err := rsp.ReadPaginationData(&os) 144 | if err != nil { 145 | // Handle error 146 | return 147 | } 148 | log.Printf("Total num: %d, total page: %d", pa.TotalNum, pa.TotalPage) 149 | for _, o := range os { 150 | log.Printf("Order: %s, %s, %s", o.Id, o.Type, o.Price) 151 | } 152 | ``` 153 | 154 | #### Example of WebSocket feed 155 | > Require package [gorilla/websocket](https://github.com/gorilla/websocket) 156 | 157 | ```bash 158 | go get github.com/gorilla/websocket github.com/pkg/errors 159 | ``` 160 | 161 | ```go 162 | rsp, err := s.WebSocketPublicToken() 163 | if err != nil { 164 | // Handle error 165 | return 166 | } 167 | 168 | tk := &kucoin.WebSocketTokenModel{} 169 | if err := rsp.ReadData(tk); err != nil { 170 | // Handle error 171 | return 172 | } 173 | 174 | c := s.NewWebSocketClient(tk) 175 | 176 | mc, ec, err := c.Connect() 177 | if err != nil { 178 | // Handle error 179 | return 180 | } 181 | 182 | ch1 := kucoin.NewSubscribeMessage("/market/ticker:KCS-BTC", false) 183 | ch2 := kucoin.NewSubscribeMessage("/market/ticker:ETH-BTC", false) 184 | uch := kucoin.NewUnsubscribeMessage("/market/ticker:ETH-BTC", false) 185 | 186 | if err := c.Subscribe(ch1, ch2); err != nil { 187 | // Handle error 188 | return 189 | } 190 | 191 | var i = 0 192 | for { 193 | select { 194 | case err := <-ec: 195 | c.Stop() // Stop subscribing the WebSocket feed 196 | log.Printf("Error: %s", err.Error()) 197 | // Handle error 198 | return 199 | case msg := <-mc: 200 | // log.Printf("Received: %s", kucoin.ToJsonString(m)) 201 | t := &kucoin.TickerLevel1Model{} 202 | if err := msg.ReadData(t); err != nil { 203 | log.Printf("Failure to read: %s", err.Error()) 204 | return 205 | } 206 | log.Printf("Ticker: %s, %s, %s, %s", msg.Topic, t.Sequence, t.Price, t.Size) 207 | i++ 208 | if i == 5 { 209 | log.Println("Unsubscribe ETH-BTC") 210 | if err = c.Unsubscribe(uch); err != nil { 211 | log.Printf("Error: %s", err.Error()) 212 | // Handle error 213 | return 214 | } 215 | } 216 | if i == 10 { 217 | log.Println("Subscribe ETH-BTC") 218 | if err = c.Subscribe(ch2); err != nil { 219 | log.Printf("Error: %s", err.Error()) 220 | // Handle error 221 | return 222 | } 223 | } 224 | if i == 15 { 225 | log.Println("Exit subscription") 226 | c.Stop() // Stop subscribing the WebSocket feed 227 | return 228 | } 229 | } 230 | } 231 | ``` 232 | 233 | ### API list 234 |
235 | Trade Fee 236 | 237 | | API | Authentication | Description | 238 | | -------- | -------- | -------- | 239 | | ApiService.BaseFee() | YES | https://docs.kucoin.com/#basic-user-fee | 240 | | ApiService.ActualFee() | YES | https://docs.kucoin.com/#actual-fee-rate-of-the-trading-pair | 241 | 242 |
243 | 244 |
245 | Stop Order 246 | 247 | | API | Authentication | Description | 248 | | -------- | -------- | -------- | 249 | | ApiService.CreateStopOrder() | YES | https://docs.kucoin.com/#place-a-new-order-2 | 250 | | ApiService.CancelStopOrder() | YES | https://docs.kucoin.com/#cancel-an-order-2 | 251 | | ApiService.CancelStopOrderBy() | YES | https://docs.kucoin.com/#cancel-orders | 252 | | ApiService.StopOrder() | YES | https://docs.kucoin.com/#get-single-order-info | 253 | | ApiService.StopOrders() | YES | https://docs.kucoin.com/#list-stop-orders | 254 | | ApiService.StopOrderByClient() | YES | https://docs.kucoin.com/#get-single-order-by-clientoid | 255 | | ApiService.CancelStopOrderByClient() | YES | https://docs.kucoin.com/#cancel-single-order-by-clientoid-2 | 256 | 257 |
258 | 259 |
260 | Account 261 | 262 | | API | Authentication | Description | 263 | | -------- | -------- | -------- | 264 | | ApiService.CreateAccount() | YES | https://docs.kucoin.com/#create-an-account | 265 | | ApiService.Accounts() | YES | https://docs.kucoin.com/#list-accounts | 266 | | ApiService.Account() | YES | https://docs.kucoin.com/#get-an-account | 267 | | ApiService.SubAccountUsers() | YES | https://docs.kucoin.com/#get-user-info-of-all-sub-accounts | 268 | | ApiService.SubAccounts() | YES | https://docs.kucoin.com/#get-the-aggregated-balance-of-all-sub-accounts-of-the-current-user | 269 | | ApiService.SubAccount() | YES | https://docs.kucoin.com/#get-account-balance-of-a-sub-account | 270 | | ApiService.AccountLedgers() | YES | `DEPRECATED` https://docs.kucoin.com/#get-account-ledgers-deprecated | 271 | | ApiService.AccountHolds() | YES | https://docs.kucoin.com/#get-holds | 272 | | ApiService.InnerTransfer() | YES | `DEPRECATED` https://docs.kucoin.com/#inner-transfer | 273 | | ApiService.InnerTransferV2() | YES | https://docs.kucoin.com/#inner-transfer | 274 | | ApiService.SubTransfer() | YES | `DEPRECATED` | 275 | | ApiService.SubTransferV2() | YES | https://docs.kucoin.com/#transfer-between-master-user-and-sub-user | 276 | | ApiService.AccountLedgersV2() | YES | https://docs.kucoin.com/#get-account-ledgers | 277 | 278 |
279 | 280 |
281 | Deposit 282 | 283 | | API | Authentication | Description | 284 | | -------- | -------- | -------- | 285 | | ApiService.CreateDepositAddress() | YES | https://docs.kucoin.com/#create-deposit-address | 286 | | ApiService.DepositAddresses() | YES | https://docs.kucoin.com/#get-deposit-address | 287 | | ApiService.V1Deposits() | YES | https://docs.kucoin.com/#get-v1-historical-deposits-list | 288 | | ApiService.Deposits() | YES | https://docs.kucoin.com/#get-deposit-list | 289 | 290 |
291 | 292 |
293 | Fill 294 | 295 | | API | Authentication | Description | 296 | | -------- | -------- | -------- | 297 | | ApiService.Fills() | YES | https://docs.kucoin.com/#list-fills | 298 | | ApiService.RecentFills() | YES | https://docs.kucoin.com/#recent-fills | 299 | 300 |
301 | 302 |
303 | Order 304 | 305 | | API | Authentication | Description | 306 | | -------- | -------- | -------- | 307 | | ApiService.CreateOrder() | YES | https://docs.kucoin.com/#place-a-new-order | 308 | | ApiService.CreateMultiOrder() | YES | https://docs.kucoin.com/#place-bulk-orders | 309 | | ApiService.CancelOrder() | YES | https://docs.kucoin.com/#cancel-an-order | 310 | | ApiService.CancelOrders() | YES | https://docs.kucoin.com/#cancel-all-orders | 311 | | ApiService.V1Orders() | YES | https://docs.kucoin.com/#get-v1-historical-orders-list | 312 | | ApiService.Orders() | YES | https://docs.kucoin.com/#list-orders | 313 | | ApiService.Order() | YES | https://docs.kucoin.com/#get-an-order | 314 | | ApiService.RecentOrders() | YES | https://docs.kucoin.com/#recent-orders | 315 | | ApiService.CreateMarginOrder() | YES | https://docs.kucoin.com/#place-a-margin-order | 316 | | ApiService.CancelOrderByClient() | YES | https://docs.kucoin.com/#cancel-single-order-by-clientoid | 317 | | ApiService.OrderByClient() | YES | https://docs.kucoin.com/#get-single-active-order-by-clientoid| 318 | 319 |
320 | 321 |
322 | WebSocket Feed 323 | 324 | | API | Authentication | Description | 325 | | -------- | -------- | -------- | 326 | | ApiService.WebSocketPublicToken() | NO | https://docs.kucoin.com/#apply-connect-token | 327 | | ApiService.WebSocketPrivateToken() | YES | https://docs.kucoin.com/#apply-connect-token | 328 | | ApiService.NewWebSocketClient() | - | https://docs.kucoin.com/#websocket-feed | 329 | 330 |
331 | 332 |
333 | Withdrawal 334 | 335 | | API | Authentication | Description | 336 | | -------- | -------- | -------- | 337 | | ApiService.WithdrawalQuotas() | YES | https://docs.kucoin.com/#get-withdrawal-quotas | 338 | | ApiService.V1Withdrawals() | YES | https://docs.kucoin.com/#get-v1-historical-withdrawals-list | 339 | | ApiService.Withdrawals() | YES | https://docs.kucoin.com/#get-withdrawals-list | 340 | | ApiService.ApplyWithdrawal() | YES | https://docs.kucoin.com/#apply-withdraw | 341 | | ApiService.CancelWithdrawal() | YES | https://docs.kucoin.com/#cancel-withdrawal | 342 | 343 |
344 | 345 |
346 | Currency 347 | 348 | | API | Authentication | Description | 349 | | -------- | -------- | -------- | 350 | | ApiService.Currencies() | NO | https://docs.kucoin.com/#get-currencies | 351 | | ApiService.Currency() | NO | https://docs.kucoin.com/#get-currency-detail | 352 | | ApiService.Prices() | NO | https://docs.kucoin.com/#get-fiat-price | 353 | 354 |
355 | 356 |
357 | Symbol 358 | 359 | | API | Authentication | Description | 360 | | -------- | -------- | -------- | 361 | | ApiService.Symbols() | NO | https://docs.kucoin.com/#get-symbols-list | 362 | | ApiService.TickerLevel1() | NO | https://docs.kucoin.com/#get-ticker | 363 | | ApiService.Tickers() | NO | https://docs.kucoin.com/#get-all-tickers | 364 | | ApiService.AggregatedPartOrderBook() | NO | https://docs.kucoin.com/#get-part-order-book-aggregated | 365 | | ApiService.AggregatedFullOrderBook() | NO | https://docs.kucoin.com/#get-full-order-book-aggregated | 366 | | ApiService.AtomicFullOrderBook() | NO | https://docs.kucoin.com/#get-full-order-book-atomic | 367 | | ApiService.TradeHistories() | NO | https://docs.kucoin.com/#get-trade-histories | 368 | | ApiService.KLines() | NO | https://docs.kucoin.com/#get-klines | 369 | | ApiService.Stats24hr() | NO | https://docs.kucoin.com/#get-24hr-stats | 370 | | ApiService.Markets() | NO | https://docs.kucoin.com/#get-market-list | 371 | 372 |
373 | 374 |
375 | Time 376 | 377 | | API | Authentication | Description | 378 | | -------- | -------- | -------- | 379 | | ApiService.ServerTime() | NO | https://docs.kucoin.com/#server-time | 380 | 381 |
382 | 383 |
384 | Service Status 385 | 386 | | API | Authentication | Description | 387 | | -------- | -------- | -------- | 388 | | ApiService.ServiceStatus() | NO | https://docs.kucoin.com/#service-status | 389 | 390 |
391 | 392 | ## Run tests 393 | 394 | ```shell 395 | # Add your API configuration items into the environmental variable first 396 | export API_BASE_URI=https://api.kucoin.com 397 | export API_KEY=key 398 | export API_SECRET=secret 399 | export API_PASSPHRASE=passphrase 400 | export API_KEY_VERSION=2 401 | 402 | # Run tests 403 | go test -v 404 | ``` 405 | 406 | ## License 407 | 408 | [MIT](LICENSE) 409 | -------------------------------------------------------------------------------- /order.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // A CreateOrderModel is the input parameter of CreateOrder(). 9 | type CreateOrderModel struct { 10 | // BASE PARAMETERS 11 | ClientOid string `json:"clientOid"` 12 | Side string `json:"side"` 13 | Symbol string `json:"symbol,omitempty"` 14 | Type string `json:"type,omitempty"` 15 | Remark string `json:"remark,omitempty"` 16 | Stop string `json:"stop,omitempty"` 17 | StopPrice string `json:"stopPrice,omitempty"` 18 | STP string `json:"stp,omitempty"` 19 | TradeType string `json:"tradeType,omitempty"` 20 | 21 | // LIMIT ORDER PARAMETERS 22 | Price string `json:"price,omitempty"` 23 | Size string `json:"size,omitempty"` 24 | TimeInForce string `json:"timeInForce,omitempty"` 25 | CancelAfter int64 `json:"cancelAfter,omitempty"` 26 | PostOnly bool `json:"postOnly,omitempty"` 27 | Hidden bool `json:"hidden,omitempty"` 28 | IceBerg bool `json:"iceberg,omitempty"` 29 | VisibleSize string `json:"visibleSize,omitempty"` 30 | 31 | // MARKET ORDER PARAMETERS 32 | // Size string `json:"size"` 33 | Funds string `json:"funds,omitempty"` 34 | 35 | // MARGIN ORDER PARAMETERS 36 | MarginMode string `json:"marginMode,omitempty"` 37 | AutoBorrow bool `json:"autoBorrow,omitempty"` 38 | AutoRepay bool `json:"autoRepay,omitempty"` 39 | } 40 | 41 | // A CreateOrderResultModel represents the result of CreateOrder(). 42 | type CreateOrderResultModel struct { 43 | OrderId string `json:"orderId"` 44 | } 45 | 46 | // CreateOrder places a new order. 47 | func (as *ApiService) CreateOrder(ctx context.Context, o *CreateOrderModel) (*ApiResponse, error) { 48 | req := NewRequest(http.MethodPost, "/api/v1/orders", o) 49 | return as.Call(ctx, req) 50 | } 51 | 52 | // CreateOrderTest places a new order test. 53 | func (as *ApiService) CreateOrderTest(ctx context.Context, o *CreateOrderModel) (*ApiResponse, error) { 54 | req := NewRequest(http.MethodPost, "/api/v1/orders/test", o) 55 | return as.Call(ctx, req) 56 | } 57 | 58 | // A CreateMultiOrderResultModel represents the result of CreateMultiOrder(). 59 | type CreateMultiOrderResultModel struct { 60 | Data OrdersModel `json:"data"` 61 | } 62 | 63 | // CreateMultiOrder places bulk orders. 64 | func (as *ApiService) CreateMultiOrder(ctx context.Context, symbol string, orders []*CreateOrderModel) (*ApiResponse, error) { 65 | params := map[string]interface{}{ 66 | "symbol": symbol, 67 | "orderList": orders, 68 | } 69 | req := NewRequest(http.MethodPost, "/api/v1/orders/multi", params) 70 | return as.Call(ctx, req) 71 | } 72 | 73 | // A CancelOrderResultModel represents the result of CancelOrder(). 74 | type CancelOrderResultModel struct { 75 | CancelledOrderIds []string `json:"cancelledOrderIds"` 76 | } 77 | 78 | // CancelOrder cancels a previously placed order. 79 | func (as *ApiService) CancelOrder(ctx context.Context, orderId string) (*ApiResponse, error) { 80 | req := NewRequest(http.MethodDelete, "/api/v1/orders/"+orderId, nil) 81 | return as.Call(ctx, req) 82 | } 83 | 84 | // A CancelOrderByClientResultModel represents the result of CancelOrderByClient(). 85 | type CancelOrderByClientResultModel struct { 86 | CancelledOrderId string `json:"cancelledOrderId"` 87 | ClientOid string `json:"clientOid"` 88 | } 89 | 90 | // CancelOrderByClient cancels a previously placed order by client ID. 91 | func (as *ApiService) CancelOrderByClient(ctx context.Context, clientOid string) (*ApiResponse, error) { 92 | req := NewRequest(http.MethodDelete, "/api/v1/order/client-order/"+clientOid, nil) 93 | return as.Call(ctx, req) 94 | } 95 | 96 | // CancelOrders cancels all orders of the symbol. 97 | // With best effort, cancel all open orders. The response is a list of ids of the canceled orders. 98 | func (as *ApiService) CancelOrders(ctx context.Context, p map[string]string) (*ApiResponse, error) { 99 | req := NewRequest(http.MethodDelete, "/api/v1/orders", p) 100 | return as.Call(ctx, req) 101 | } 102 | 103 | // An OrderModel represents an order. 104 | type OrderModel struct { 105 | Id string `json:"id"` 106 | Symbol string `json:"symbol"` 107 | OpType string `json:"opType"` 108 | Type string `json:"type"` 109 | Side string `json:"side"` 110 | Price string `json:"price"` 111 | Size string `json:"size"` 112 | Funds string `json:"funds"` 113 | DealFunds string `json:"dealFunds"` 114 | DealSize string `json:"dealSize"` 115 | Fee string `json:"fee"` 116 | FeeCurrency string `json:"feeCurrency"` 117 | Stp string `json:"stp"` 118 | Stop string `json:"stop"` 119 | StopTriggered bool `json:"stopTriggered"` 120 | StopPrice string `json:"stopPrice"` 121 | TimeInForce string `json:"timeInForce"` 122 | PostOnly bool `json:"postOnly"` 123 | Hidden bool `json:"hidden"` 124 | IceBerg bool `json:"iceberg"` 125 | VisibleSize string `json:"visibleSize"` 126 | CancelAfter int64 `json:"cancelAfter"` 127 | Channel string `json:"channel"` 128 | ClientOid string `json:"clientOid"` 129 | Remark string `json:"remark"` 130 | Tags string `json:"tags"` 131 | IsActive bool `json:"isActive"` 132 | CancelExist bool `json:"cancelExist"` 133 | CreatedAt int64 `json:"createdAt"` 134 | TradeType string `json:"tradeType"` 135 | } 136 | 137 | // A OrdersModel is the set of *OrderModel. 138 | type OrdersModel []*OrderModel 139 | 140 | // Orders returns a list your current orders. 141 | func (as *ApiService) Orders(ctx context.Context, params map[string]string, pagination *PaginationParam) (*ApiResponse, error) { 142 | pagination.ReadParam(params) 143 | req := NewRequest(http.MethodGet, "/api/v1/orders", params) 144 | return as.Call(ctx, req) 145 | } 146 | 147 | // A V1OrderModel represents a v1 order. 148 | type V1OrderModel struct { 149 | Symbol string `json:"symbol"` 150 | DealPrice string `json:"dealPrice"` 151 | DealValue string `json:"dealValue"` 152 | Amount string `json:"amount"` 153 | Fee string `json:"fee"` 154 | Side string `json:"side"` 155 | CreatedAt int64 `json:"createdAt"` 156 | } 157 | 158 | // A V1OrdersModel is the set of *V1OrderModel. 159 | type V1OrdersModel []*V1OrderModel 160 | 161 | // V1Orders returns a list of v1 historical orders. 162 | // Deprecated 163 | func (as *ApiService) V1Orders(ctx context.Context, params map[string]string, pagination *PaginationParam) (*ApiResponse, error) { 164 | pagination.ReadParam(params) 165 | req := NewRequest(http.MethodGet, "/api/v1/hist-orders", params) 166 | return as.Call(ctx, req) 167 | } 168 | 169 | // Order returns a single order by order id. 170 | func (as *ApiService) Order(ctx context.Context, orderId string) (*ApiResponse, error) { 171 | req := NewRequest(http.MethodGet, "/api/v1/orders/"+orderId, nil) 172 | return as.Call(ctx, req) 173 | } 174 | 175 | // OrderByClient returns a single order by client id. 176 | func (as *ApiService) OrderByClient(ctx context.Context, clientOid string) (*ApiResponse, error) { 177 | req := NewRequest(http.MethodGet, "/api/v1/order/client-order/"+clientOid, nil) 178 | return as.Call(ctx, req) 179 | } 180 | 181 | // RecentOrders returns the recent orders of the latest transactions within 24 hours. 182 | func (as *ApiService) RecentOrders(ctx context.Context) (*ApiResponse, error) { 183 | req := NewRequest(http.MethodGet, "/api/v1/limit/orders", nil) 184 | return as.Call(ctx, req) 185 | } 186 | 187 | // CreateStopOrder places a new stop-order. 188 | func (as *ApiService) CreateStopOrder(ctx context.Context, o *CreateOrderModel) (*ApiResponse, error) { 189 | req := NewRequest(http.MethodPost, "/api/v1/stop-order", o) 190 | return as.Call(ctx, req) 191 | } 192 | 193 | // CancelStopOrder cancels a previously placed stop-order. 194 | func (as *ApiService) CancelStopOrder(ctx context.Context, orderId string) (*ApiResponse, error) { 195 | req := NewRequest(http.MethodDelete, "/api/v1/stop-order/"+orderId, nil) 196 | return as.Call(ctx, req) 197 | } 198 | 199 | // CancelStopOrderByClientModel returns Model of CancelStopOrderByClient API 200 | type CancelStopOrderByClientModel struct { 201 | CancelledOrderId string `json:"cancelledOrderId"` 202 | ClientOid string `json:"clientOid"` 203 | } 204 | 205 | // CancelStopOrderByClient cancels a previously placed stop-order by client ID. 206 | func (as *ApiService) CancelStopOrderByClient(ctx context.Context, clientOid string, p map[string]string) (*ApiResponse, error) { 207 | p["clientOid"] = clientOid 208 | 209 | req := NewRequest(http.MethodDelete, "/api/v1/stop-order/cancelOrderByClientOid", p) 210 | return as.Call(ctx, req) 211 | } 212 | 213 | // StopOrderModel RESPONSES of StopOrder 214 | type StopOrderModel struct { 215 | Id string `json:"id"` 216 | Symbol string `json:"symbol"` 217 | UserId string `json:"userId"` 218 | Status string `json:"status"` 219 | Type string `json:"type"` 220 | Side string `json:"side"` 221 | Price string `json:"price"` 222 | Size string `json:"size"` 223 | Funds string `json:"funds"` 224 | Stp string `json:"stp"` 225 | TimeInForce string `json:"timeInForce"` 226 | CancelAfter int64 `json:"cancelAfter"` 227 | PostOnly bool `json:"postOnly"` 228 | Hidden bool `json:"hidden"` 229 | IceBerg bool `json:"iceberg"` 230 | VisibleSize string `json:"visibleSize"` 231 | Channel string `json:"channel"` 232 | ClientOid string `json:"clientOid"` 233 | Remark string `json:"remark"` 234 | Tags string `json:"tags"` 235 | OrderTime int64 `json:"orderTime"` 236 | DomainId string `json:"domainId"` 237 | TradeSource string `json:"tradeSource"` 238 | TradeType string `json:"tradeType"` 239 | FeeCurrency string `json:"feeCurrency"` 240 | TakerFeeRate string `json:"takerFeeRate"` 241 | MakerFeeRate string `json:"makerFeeRate"` 242 | CreatedAt int64 `json:"createdAt"` 243 | Stop string `json:"stop"` 244 | StopTriggerTime string `json:"stopTriggerTime"` 245 | StopPrice string `json:"stopPrice"` 246 | } 247 | 248 | // StopOrder returns a single order by stop-order id. 249 | func (as *ApiService) StopOrder(ctx context.Context, orderId string) (*ApiResponse, error) { 250 | req := NewRequest(http.MethodGet, "/api/v1/stop-order/"+orderId, nil) 251 | return as.Call(ctx, req) 252 | } 253 | 254 | // StopOrderListModel StopOrderByClient model 255 | type StopOrderListModel []*StopOrderModel 256 | 257 | // StopOrderByClient returns a single stop-order by client id. 258 | func (as *ApiService) StopOrderByClient(ctx context.Context, clientOid string, p map[string]string) (*ApiResponse, error) { 259 | p["clientOid"] = clientOid 260 | 261 | req := NewRequest(http.MethodGet, "/api/v1/stop-order/queryOrderByClientOid", p) 262 | return as.Call(ctx, req) 263 | } 264 | 265 | // StopOrders returns a list your current orders. 266 | func (as *ApiService) StopOrders(ctx context.Context, params map[string]string, pagination *PaginationParam) (*ApiResponse, error) { 267 | pagination.ReadParam(params) 268 | req := NewRequest(http.MethodGet, "/api/v1/stop-order", params) 269 | return as.Call(ctx, req) 270 | } 271 | 272 | // CancelStopOrderBy returns a list your current orders. 273 | func (as *ApiService) CancelStopOrderBy(ctx context.Context, params map[string]string) (*ApiResponse, error) { 274 | req := NewRequest(http.MethodDelete, "/api/v1/stop-order/cancel", params) 275 | return as.Call(ctx, req) 276 | } 277 | 278 | // CreateMarginOrder places a new margin order. 279 | func (as *ApiService) CreateMarginOrder(ctx context.Context, o *CreateOrderModel) (*ApiResponse, error) { 280 | req := NewRequest(http.MethodPost, "/api/v1/margin/order", o) 281 | return as.Call(ctx, req) 282 | } 283 | 284 | // CreateMarginOrderTest places a new margin test order. 285 | func (as *ApiService) CreateMarginOrderTest(ctx context.Context, o *CreateOrderModel) (*ApiResponse, error) { 286 | req := NewRequest(http.MethodPost, "/api/v1/margin/order/test", o) 287 | return as.Call(ctx, req) 288 | } 289 | 290 | // A CreateOcoOrderModel is the input parameter of CreatOcoOrder(). 291 | type CreateOcoOrderModel struct { 292 | Side string `json:"side"` 293 | Symbol string `json:"symbol,omitempty"` 294 | Price string `json:"price,omitempty"` 295 | Size string `json:"size,omitempty"` 296 | StopPrice string `json:"stopPrice,omitempty"` 297 | LimitPrice string `json:"limitPrice,omitempty"` 298 | TradeType string `json:"tradeType"` 299 | ClientOid string `json:"clientOid,omitempty"` 300 | Remark string `json:"remark"` 301 | } 302 | 303 | // CreateOcoOrder places a new margin order. 304 | func (as *ApiService) CreateOcoOrder(ctx context.Context, o *CreateOcoOrderModel) (*ApiResponse, error) { 305 | req := NewRequest(http.MethodPost, "/api/v3/oco/order", o) 306 | return as.Call(ctx, req) 307 | } 308 | 309 | type CancelledOcoOrderResModel struct { 310 | CancelledOrderIds []string `json:"cancelledOrderIds"` 311 | } 312 | 313 | // DeleteOcoOrder cancel a oco order. return CancelledOcoOrderResModel 314 | func (as *ApiService) DeleteOcoOrder(ctx context.Context, orderId string) (*ApiResponse, error) { 315 | req := NewRequest(http.MethodDelete, "/api/v3/oco/order/"+orderId, nil) 316 | return as.Call(ctx, req) 317 | } 318 | 319 | // DeleteOcoOrderClientId cancel a oco order with clientOrderId. return CancelledOcoOrderResModel 320 | func (as *ApiService) DeleteOcoOrderClientId(ctx context.Context, clientOrderId string) (*ApiResponse, error) { 321 | req := NewRequest(http.MethodDelete, "/api/v3/oco/client-order/"+clientOrderId, nil) 322 | return as.Call(ctx, req) 323 | } 324 | 325 | // DeleteOcoOrders cancel all oco order. return CancelledOcoOrderResModel 326 | func (as *ApiService) DeleteOcoOrders(ctx context.Context, symbol, orderIds string) (*ApiResponse, error) { 327 | params := map[string]interface{}{ 328 | "symbol": symbol, 329 | "orderIds": orderIds, 330 | } 331 | req := NewRequest(http.MethodDelete, "/api/v3/oco/orders", params) 332 | return as.Call(ctx, req) 333 | } 334 | 335 | type OcoOrderResModel struct { 336 | OrderId string `json:"order_id"` 337 | Symbol string `json:"symbol"` 338 | ClientOid string `json:"clientOid"` 339 | OrderTime int64 `json:"orderTime"` 340 | Status string `json:"status"` 341 | } 342 | 343 | // OcoOrder returns a oco order by order id. return OcoOrderResModel 344 | func (as *ApiService) OcoOrder(ctx context.Context, orderId string) (*ApiResponse, error) { 345 | req := NewRequest(http.MethodGet, "/api/v3/oco/order/"+orderId, nil) 346 | return as.Call(ctx, req) 347 | } 348 | 349 | // OcoClientOrder returns a oco order by order id. return OcoOrderResModel 350 | func (as *ApiService) OcoClientOrder(ctx context.Context, clientOrderId string) (*ApiResponse, error) { 351 | req := NewRequest(http.MethodGet, "/api/v3/oco/client-order/"+clientOrderId, nil) 352 | return as.Call(ctx, req) 353 | } 354 | 355 | type OcoOrdersRes []*OcoOrderResModel 356 | 357 | // OcoOrders returns a oco order by order id. return OcoOrdersRes 358 | func (as *ApiService) OcoOrders(ctx context.Context, p map[string]string, pagination *PaginationParam) (*ApiResponse, error) { 359 | pagination.ReadParam(p) 360 | req := NewRequest(http.MethodGet, "/api/v3/oco/orders", p) 361 | return as.Call(ctx, req) 362 | } 363 | 364 | type OcoOrdersModel []*OrderDetailModel 365 | 366 | type OrderDetailModel struct { 367 | OrderId string `json:"order_id"` 368 | Symbol string `json:"symbol"` 369 | ClientOid string `json:"clientOid"` 370 | OrderTime int64 `json:"orderTime"` 371 | Status string `json:"status"` 372 | Orders []*OcoSubOrderModel `json:"orders"` 373 | } 374 | type OcoSubOrderModel struct { 375 | Id string `json:"id"` 376 | Symbol string `json:"symbol"` 377 | Side string `json:"side"` 378 | Price string `json:"price"` 379 | StopPrice string `json:"stopPrice"` 380 | Size string `json:"size"` 381 | Status string `json:"status"` 382 | } 383 | 384 | // OcoOrderDetail returns a oco order detail by order id. return OrderDetailModel 385 | func (as *ApiService) OcoOrderDetail(ctx context.Context, orderId string) (*ApiResponse, error) { 386 | req := NewRequest(http.MethodGet, "/api/v3/oco/order/details/"+orderId, nil) 387 | return as.Call(ctx, req) 388 | } 389 | -------------------------------------------------------------------------------- /hf_order_test.go: -------------------------------------------------------------------------------- 1 | package kucoin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestApiService_HfPlaceOrder(t *testing.T) { 10 | s := NewApiServiceFromEnv() 11 | clientOid := IntToString(time.Now().Unix()) 12 | p := map[string]string{ 13 | "clientOid": clientOid, 14 | "symbol": "KCS-USDT", 15 | "type": "limit", 16 | "side": "buy", 17 | "stp": "CN", 18 | "size": "1", 19 | "price": "0.1", 20 | } 21 | rsp, err := s.HfPlaceOrder(context.Background(), p) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | v := &HfPlaceOrderRes{} 26 | if err := rsp.ReadData(v); err != nil { 27 | t.Fatal(err) 28 | } 29 | t.Log(ToJsonString(v)) 30 | if v.OrderId == "" { 31 | t.Error("Empty key 'orderId'") 32 | } 33 | } 34 | 35 | func TestApiService_HfSyncPlaceOrder(t *testing.T) { 36 | t.SkipNow() 37 | s := NewApiServiceFromEnv() 38 | clientOid := IntToString(time.Now().Unix()) 39 | p := map[string]string{ 40 | "clientOid": clientOid, 41 | "symbol": "MATIC-USDT", 42 | "type": "market", 43 | "side": "sell", 44 | "stp": "CN", 45 | "tags": "t", 46 | "remark": "r", 47 | "size": "0.1", 48 | } 49 | rsp, err := s.HfSyncPlaceOrder(context.Background(), p) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | v := &HfSyncPlaceOrderRes{} 54 | if err := rsp.ReadData(v); err != nil { 55 | t.Fatal(err) 56 | } 57 | t.Log(ToJsonString(v)) 58 | if v.OrderId == "" { 59 | t.Error("Empty key 'orderId'") 60 | } 61 | } 62 | 63 | func TestApiService_HfPlaceMultiOrders(t *testing.T) { 64 | t.SkipNow() 65 | s := NewApiServiceFromEnv() 66 | clientOid := IntToString(time.Now().Unix()) 67 | p := make([]*HFCreateMultiOrderModel, 0) 68 | p = append(p, &HFCreateMultiOrderModel{ 69 | ClientOid: clientOid, 70 | Symbol: "MATIC-USDT", 71 | OrderType: "market", 72 | Side: "sell", 73 | Size: "0.1", 74 | }) 75 | 76 | clientOid2 := IntToString(time.Now().Unix()) 77 | p = append(p, &HFCreateMultiOrderModel{ 78 | ClientOid: clientOid2, 79 | Symbol: "MATIC-USDT", 80 | OrderType: "market", 81 | Side: "sell", 82 | Size: "0.1", 83 | }) 84 | 85 | rsp, err := s.HfPlaceMultiOrders(context.Background(), p) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | v := &HfPlaceMultiOrdersRes{} 90 | if err := rsp.ReadData(v); err != nil { 91 | t.Fatal(err) 92 | } 93 | t.Log(ToJsonString(v)) 94 | } 95 | 96 | func TestApiService_HfSyncPlaceMultiOrders(t *testing.T) { 97 | t.SkipNow() 98 | s := NewApiServiceFromEnv() 99 | clientOid := IntToString(time.Now().Unix()) 100 | p := make([]*HFCreateMultiOrderModel, 0) 101 | p = append(p, &HFCreateMultiOrderModel{ 102 | ClientOid: clientOid, 103 | Symbol: "MATIC-USDT", 104 | OrderType: "market", 105 | Side: "buy", 106 | Size: "0.1", 107 | }) 108 | 109 | clientOid2 := IntToString(time.Now().Unix()) 110 | p = append(p, &HFCreateMultiOrderModel{ 111 | ClientOid: clientOid2, 112 | Symbol: "MATIC-USDT", 113 | OrderType: "market", 114 | Side: "buy", 115 | Size: "0.2", 116 | }) 117 | 118 | rsp, err := s.HfSyncPlaceMultiOrders(context.Background(), p) 119 | if err != nil { 120 | t.Fatal(err) 121 | } 122 | v := &HfSyncPlaceMultiOrdersRes{} 123 | if err := rsp.ReadData(v); err != nil { 124 | t.Fatal(err) 125 | } 126 | t.Log(ToJsonString(v)) 127 | } 128 | 129 | func TestApiService_HfObtainFilledOrders(t *testing.T) { 130 | s := NewApiServiceFromEnv() 131 | p := map[string]string{ 132 | "symbol": "MATIC-USDT", 133 | } 134 | rsp, err := s.HfObtainFilledOrders(context.Background(), p) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | v := &HfFilledOrdersModel{} 139 | if err := rsp.ReadData(v); err != nil { 140 | t.Fatal(err) 141 | } 142 | if v == nil { 143 | return 144 | } 145 | for _, o := range v.Items { 146 | t.Log(ToJsonString(o)) 147 | switch { 148 | case o.Id == "": 149 | t.Error("Empty key 'id'") 150 | case o.Symbol == "": 151 | t.Error("Empty key 'symbol'") 152 | case o.OpType == "": 153 | t.Error("Empty key 'opType'") 154 | case o.Type == "": 155 | t.Error("Empty key 'type'") 156 | case o.Side == "": 157 | t.Error("Empty key 'side'") 158 | } 159 | } 160 | } 161 | 162 | func TestApiService_HfObtainActiveOrders(t *testing.T) { 163 | s := NewApiServiceFromEnv() 164 | rsp, err := s.HfObtainActiveOrders(context.Background(), "MATIC-USDT") 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | v := HfOrdersModel{} 169 | if err := rsp.ReadData(&v); err != nil { 170 | t.Fatal(err) 171 | } 172 | for _, o := range v { 173 | t.Log(ToJsonString(o)) 174 | switch { 175 | case o.Id == "": 176 | t.Error("Empty key 'id'") 177 | case o.Symbol == "": 178 | t.Error("Empty key 'symbol'") 179 | case o.OpType == "": 180 | t.Error("Empty key 'opType'") 181 | case o.Type == "": 182 | t.Error("Empty key 'type'") 183 | case o.Side == "": 184 | t.Error("Empty key 'side'") 185 | } 186 | } 187 | } 188 | 189 | func TestApiService_HfObtainActiveSymbols(t *testing.T) { 190 | s := NewApiServiceFromEnv() 191 | rsp, err := s.HfObtainActiveSymbols(context.Background()) 192 | if err != nil { 193 | t.Fatal(err) 194 | } 195 | v := HfSymbolsModel{} 196 | if err := rsp.ReadData(&v); err != nil { 197 | t.Fatal(err) 198 | } 199 | t.Log(ToJsonString(v)) 200 | } 201 | 202 | func TestApiService_HfOrderDetail(t *testing.T) { 203 | s := NewApiServiceFromEnv() 204 | rsp, err := s.HfOrderDetail(context.Background(), "649a45d576174800019e44b4", "MATIC-USDT") 205 | if err != nil { 206 | t.Fatal(err) 207 | } 208 | v := &HfOrderModel{} 209 | if err := rsp.ReadData(v); err != nil { 210 | t.Fatal(err) 211 | } 212 | t.Log(ToJsonString(v)) 213 | } 214 | 215 | func TestApiService_HfOrderDetailByClientOid(t *testing.T) { 216 | s := NewApiServiceFromEnv() 217 | rsp, err := s.HfOrderDetailByClientOid(context.Background(), "1687832021", "MATIC-USDT") 218 | if err != nil { 219 | t.Fatal(err) 220 | } 221 | v := &HfOrderModel{} 222 | if err := rsp.ReadData(v); err != nil { 223 | t.Fatal(err) 224 | } 225 | t.Log(ToJsonString(v)) 226 | } 227 | 228 | // 649a45d576174800019e44b4 229 | 230 | func TestApiService_HfModifyOrder(t *testing.T) { 231 | t.SkipNow() 232 | s := NewApiServiceFromEnv() 233 | p := map[string]string{ 234 | "symbol": "MATIC-USDT", 235 | "orderId": "649a45d576174800019e44b4", 236 | "newPrice": "2.0", 237 | } 238 | rsp, err := s.HfModifyOrder(context.Background(), p) 239 | if err != nil { 240 | t.Fatal(err) 241 | } 242 | v := &HfModifyOrderRes{} 243 | if err := rsp.ReadData(v); err != nil { 244 | t.Fatal(err) 245 | } 246 | t.Log(ToJsonString(v)) 247 | } 248 | 249 | func TestApiService_HfQueryAutoCancelSetting(t *testing.T) { 250 | s := NewApiServiceFromEnv() 251 | rsp, err := s.HfQueryAutoCancelSetting(context.Background()) 252 | if err != nil { 253 | t.Fatal(err) 254 | } 255 | v := &AUtoCancelSettingModel{} 256 | if err := rsp.ReadData(v); err != nil { 257 | t.Fatal(err) 258 | } 259 | t.Log(ToJsonString(v)) 260 | } 261 | 262 | func TestApiService_HfAutoCancelSetting(t *testing.T) { 263 | t.SkipNow() 264 | s := NewApiServiceFromEnv() 265 | rsp, err := s.HfAutoCancelSetting(context.Background(), 10000, "MATIC-USDT") 266 | if err != nil { 267 | t.Fatal(err) 268 | } 269 | v := &HfAutoCancelSettingRes{} 270 | if err := rsp.ReadData(v); err != nil { 271 | t.Fatal(err) 272 | } 273 | t.Log(ToJsonString(v)) 274 | } 275 | 276 | func TestApiService_HfCancelOrder(t *testing.T) { 277 | t.SkipNow() 278 | s := NewApiServiceFromEnv() 279 | rsp, err := s.HfCancelOrder(context.Background(), "649a49201a39390001adcce8", "MATIC-USDT") 280 | if err != nil { 281 | t.Fatal(err) 282 | } 283 | v := &HfOrderIdModel{} 284 | if err := rsp.ReadData(v); err != nil { 285 | t.Fatal(err) 286 | } 287 | t.Log(ToJsonString(v)) 288 | } 289 | 290 | func TestApiService_HfSyncCancelOrder(t *testing.T) { 291 | t.SkipNow() 292 | s := NewApiServiceFromEnv() 293 | rsp, err := s.HfSyncCancelOrder(context.Background(), "649a49201a39390001adcce8", "MATIC-USDT") 294 | if err != nil { 295 | t.Fatal(err) 296 | } 297 | v := &HfSyncCancelOrderRes{} 298 | if err := rsp.ReadData(v); err != nil { 299 | t.Fatal(err) 300 | } 301 | t.Log(ToJsonString(v)) 302 | } 303 | 304 | func TestApiService_HfCancelOrderByClientId(t *testing.T) { 305 | t.SkipNow() 306 | s := NewApiServiceFromEnv() 307 | rsp, err := s.HfCancelOrderByClientId(context.Background(), "649a49201a39390001adcce8", "MATIC-USDT") 308 | if err != nil { 309 | t.Fatal(err) 310 | } 311 | v := &HfClientOidModel{} 312 | if err := rsp.ReadData(v); err != nil { 313 | t.Fatal(err) 314 | } 315 | t.Log(ToJsonString(v)) 316 | } 317 | 318 | func TestApiService_HfSyncCancelOrderByClientId(t *testing.T) { 319 | t.SkipNow() 320 | s := NewApiServiceFromEnv() 321 | rsp, err := s.HfSyncCancelOrderByClientId(context.Background(), "649a49201a39390001adcce8", "MATIC-USDT") 322 | if err != nil { 323 | t.Fatal(err) 324 | } 325 | v := &HfSyncCancelOrderRes{} 326 | if err := rsp.ReadData(v); err != nil { 327 | t.Fatal(err) 328 | } 329 | t.Log(ToJsonString(v)) 330 | } 331 | 332 | func TestApiService_HfSyncCancelOrderWithSize(t *testing.T) { 333 | t.SkipNow() 334 | s := NewApiServiceFromEnv() 335 | rsp, err := s.HfSyncCancelOrderWithSize(context.Background(), "649a49201a39390001adcce8", "MATIC-USDT", "0.3") 336 | if err != nil { 337 | t.Fatal(err) 338 | } 339 | v := &HfSyncCancelOrderWithSizeRes{} 340 | if err := rsp.ReadData(v); err != nil { 341 | t.Fatal(err) 342 | } 343 | t.Log(ToJsonString(v)) 344 | } 345 | func TestApiService_HfSyncCancelAllOrders(t *testing.T) { 346 | t.SkipNow() 347 | s := NewApiServiceFromEnv() 348 | rsp, err := s.HfSyncCancelAllOrders(context.Background(), "MATIC-USDT") 349 | if err != nil { 350 | t.Fatal(err) 351 | } 352 | data := new(string) 353 | if err := rsp.ReadData(data); err != nil { 354 | t.Fatal(err) 355 | } 356 | t.Log(ToJsonString(data)) 357 | } 358 | 359 | func TestApiService_HfTransactionDetails(t *testing.T) { 360 | s := NewApiServiceFromEnv() 361 | p := map[string]string{ 362 | "symbol": "MATIC-USDT", 363 | } 364 | rsp, err := s.HfTransactionDetails(context.Background(), p) 365 | if err != nil { 366 | t.Fatal(err) 367 | } 368 | v := &HfTransactionDetailsModel{} 369 | if err := rsp.ReadData(v); err != nil { 370 | t.Fatal(err) 371 | } 372 | if v == nil { 373 | return 374 | } 375 | for _, item := range v.Items { 376 | t.Log(ToJsonString(item)) 377 | } 378 | } 379 | func TestApiService_HfCancelOrders(t *testing.T) { 380 | 381 | s := NewApiServiceFromEnv() 382 | rsp, err := s.HfCancelOrders(context.Background()) 383 | if err != nil { 384 | t.Fatal(err) 385 | } 386 | o := &HfCancelOrdersResultModel{} 387 | if err := rsp.ReadData(o); err != nil { 388 | t.Fatal(err) 389 | } 390 | t.Log(ToJsonString(o)) 391 | 392 | } 393 | 394 | func TestApiService_HfMarginActiveSymbols(t *testing.T) { 395 | 396 | s := NewApiServiceFromEnv() 397 | rsp, err := s.HfMarginActiveSymbols(context.Background(), "MARGIN_ISOLATED_TRADE") 398 | if err != nil { 399 | t.Fatal(err) 400 | } 401 | o := &HFMarginActiveSymbolsModel{} 402 | if err := rsp.ReadData(o); err != nil { 403 | t.Fatal(err) 404 | } 405 | t.Log(ToJsonString(o)) 406 | 407 | } 408 | 409 | func TestApiService_HfMarginOrderV3(t *testing.T) { 410 | s := NewApiServiceFromEnv() 411 | 412 | // market order 413 | req := &HfMarginOrderV3Req{ 414 | ClientOid: IntToString(time.Now().Unix()), 415 | Side: "buy", 416 | Symbol: "PEPE-USDT", 417 | Type: "market", 418 | Stp: "CN", 419 | IsIsolated: false, 420 | AutoBorrow: true, 421 | AutoRepay: true, 422 | Funds: "8", 423 | } 424 | 425 | rsp, err := s.HfCreateMarinOrderV3(context.Background(), req) 426 | if err != nil { 427 | t.Fatal(err) 428 | } 429 | o := &HfMarginOrderV3Resp{} 430 | if err := rsp.ReadData(o); err != nil { 431 | t.Fatal(err) 432 | } 433 | t.Log(ToJsonString(o)) 434 | 435 | reqSell := &HfMarginOrderV3Req{ 436 | ClientOid: IntToString(time.Now().Unix()), 437 | Side: "sell", 438 | Symbol: "PEPE-USDT", 439 | Type: "market", 440 | Stp: "CN", 441 | IsIsolated: false, 442 | AutoBorrow: true, 443 | AutoRepay: true, 444 | Funds: "100000", 445 | } 446 | 447 | rsp, err = s.HfCreateMarinOrderV3(context.Background(), reqSell) 448 | if err != nil { 449 | t.Fatal(err) 450 | } 451 | o = &HfMarginOrderV3Resp{} 452 | if err := rsp.ReadData(o); err != nil { 453 | t.Fatal(err) 454 | } 455 | t.Log(ToJsonString(o)) 456 | 457 | // limit order 458 | reqLimit := &HfMarginOrderV3Req{ 459 | ClientOid: IntToString(time.Now().Unix()), 460 | Side: "buy", 461 | Symbol: "SHIB-USDT", 462 | Type: "limit", 463 | Stp: "CN", 464 | IsIsolated: false, 465 | AutoBorrow: true, 466 | AutoRepay: true, 467 | Price: "0.000001", 468 | Size: "1000000", 469 | } 470 | 471 | rspObj, err := s.HfCreateMarinOrderV3(context.Background(), reqLimit) 472 | if err != nil { 473 | t.Fatal(err) 474 | } 475 | o = &HfMarginOrderV3Resp{} 476 | if err := rspObj.ReadData(o); err != nil { 477 | t.Fatal(err) 478 | } 479 | t.Log(ToJsonString(o)) 480 | } 481 | 482 | func TestApiService_HfMarginOrderTestV3(t *testing.T) { 483 | s := NewApiServiceFromEnv() 484 | 485 | req := &HfMarginOrderV3Req{ 486 | ClientOid: IntToString(time.Now().Unix()), 487 | Side: "buy", 488 | Symbol: "PEPE-USDT", 489 | Type: "market", 490 | Stp: "CN", 491 | IsIsolated: false, 492 | AutoBorrow: true, 493 | AutoRepay: true, 494 | Funds: "8", 495 | } 496 | 497 | rsp, err := s.HfCreateMarinOrderTestV3(context.Background(), req) 498 | if err != nil { 499 | t.Fatal(err) 500 | } 501 | o := &HfMarginOrderV3Resp{} 502 | if err := rsp.ReadData(o); err != nil { 503 | t.Fatal(err) 504 | } 505 | t.Log(ToJsonString(o)) 506 | 507 | } 508 | 509 | func TestApiService_HfCancelMarinOrderV3(t *testing.T) { 510 | s := NewApiServiceFromEnv() 511 | 512 | req := &HfCancelMarinOrderV3Req{ 513 | OrderId: "66ab62c1693a4f000753b464", 514 | Symbol: "SHIB-USDT", 515 | } 516 | rsp, err := s.HfCancelMarinOrderV3(context.Background(), req) 517 | if err != nil { 518 | t.Fatal(err) 519 | } 520 | o := &HfCancelMarinOrderV3Resp{} 521 | if err := rsp.ReadData(o); err != nil { 522 | t.Fatal(err) 523 | } 524 | t.Log(ToJsonString(o)) 525 | } 526 | 527 | func TestApiService_HfCancelClientMarinOrderV3(t *testing.T) { 528 | s := NewApiServiceFromEnv() 529 | 530 | req := &HfCancelClientMarinOrderV3Req{ 531 | ClientOid: "1722508074", 532 | Symbol: "SHIB-USDT", 533 | } 534 | 535 | rsp, err := s.HfCancelClientMarinOrderV3(context.Background(), req) 536 | if err != nil { 537 | t.Fatal(err) 538 | } 539 | o := &HfCancelClientMarinOrderV3Resp{} 540 | if err := rsp.ReadData(o); err != nil { 541 | t.Fatal(err) 542 | } 543 | t.Log(ToJsonString(o)) 544 | 545 | } 546 | 547 | func TestApiService_HfCancelAllMarginOrdersV3(t *testing.T) { 548 | s := NewApiServiceFromEnv() 549 | 550 | req := &HfCancelAllMarginOrdersV3Req{ 551 | TradeType: "MARGIN_TRADE", 552 | Symbol: "SHIB-USDT", 553 | } 554 | 555 | rsp, err := s.HfCancelAllMarginOrdersV3(context.Background(), req) 556 | if err != nil { 557 | t.Fatal(err) 558 | } 559 | var o HfCancelAllMarginOrdersV3Resp 560 | if err := rsp.ReadData(&o); err != nil { 561 | t.Fatal(err) 562 | } 563 | t.Log(ToJsonString(o)) 564 | 565 | } 566 | 567 | func TestApiService_HfMarinActiveOrdersV3(t *testing.T) { 568 | s := NewApiServiceFromEnv() 569 | 570 | req := &HfMarinActiveOrdersV3Req{ 571 | TradeType: "MARGIN_TRADE", 572 | Symbol: "SHIB-USDT", 573 | } 574 | 575 | rsp, err := s.HfMarinActiveOrdersV3(context.Background(), req) 576 | if err != nil { 577 | t.Fatal(err) 578 | } 579 | o := &HfMarinActiveOrdersV3Resp{} 580 | if err := rsp.ReadData(o); err != nil { 581 | t.Fatal(err) 582 | } 583 | t.Log(ToJsonString(o)) 584 | 585 | } 586 | 587 | func TestApiService_HfMarinDoneOrdersV3(t *testing.T) { 588 | s := NewApiServiceFromEnv() 589 | 590 | { 591 | req := &HfMarinDoneOrdersV3Req{ 592 | TradeType: "MARGIN_TRADE", 593 | Symbol: "PEPE-USDT", 594 | } 595 | 596 | rsp, err := s.HfMarinDoneOrdersV3(context.Background(), req) 597 | if err != nil { 598 | t.Fatal(err) 599 | } 600 | o := &HfMarinDoneOrdersV3Resp{} 601 | if err := rsp.ReadData(o); err != nil { 602 | t.Fatal(err) 603 | } 604 | t.Log(ToJsonString(o)) 605 | } 606 | { 607 | req := &HfMarinDoneOrdersV3Req{ 608 | TradeType: "MARGIN_TRADE", 609 | Symbol: "PEPE-USDT", 610 | Side: "buy", 611 | Type: "market", 612 | StartAt: 1722482940355, 613 | } 614 | 615 | rsp, err := s.HfMarinDoneOrdersV3(context.Background(), req) 616 | if err != nil { 617 | t.Fatal(err) 618 | } 619 | o := &HfMarinDoneOrdersV3Resp{} 620 | if err := rsp.ReadData(o); err != nil { 621 | t.Fatal(err) 622 | } 623 | t.Log(ToJsonString(o)) 624 | } 625 | 626 | } 627 | 628 | func TestApiService_HfMarinOrderV3(t *testing.T) { 629 | s := NewApiServiceFromEnv() 630 | 631 | req := &HfMarinOrderV3Req{ 632 | OrderId: "66ab00fc693a4f0007ac03db", 633 | Symbol: "PEPE-USDT", 634 | } 635 | 636 | rsp, err := s.HfMarinOrderV3(context.Background(), req) 637 | if err != nil { 638 | t.Fatal(err) 639 | } 640 | o := &HfMarinOrderV3Resp{} 641 | if err := rsp.ReadData(o); err != nil { 642 | t.Fatal(err) 643 | } 644 | t.Log(ToJsonString(o)) 645 | 646 | } 647 | 648 | func TestApiService_HfMarinClientOrderV3(t *testing.T) { 649 | s := NewApiServiceFromEnv() 650 | 651 | req := &HfMarinClientOrderV3Req{ 652 | ClientOid: "1722482939", 653 | Symbol: "PEPE-USDT", 654 | } 655 | 656 | rsp, err := s.HfMarinClientOrderV3(context.Background(), req) 657 | if err != nil { 658 | t.Fatal(err) 659 | } 660 | o := &HfMarinClientOrderV3Resp{} 661 | if err := rsp.ReadData(o); err != nil { 662 | t.Fatal(err) 663 | } 664 | t.Log(ToJsonString(o)) 665 | } 666 | 667 | func TestApiService_HfMarinFillsV3(t *testing.T) { 668 | s := NewApiServiceFromEnv() 669 | 670 | { 671 | req := &HfMarinFillsV3Req{ 672 | Symbol: "PEPE-USDT", 673 | TradeType: "MARGIN_TRADE", 674 | Side: "buy", 675 | } 676 | 677 | rsp, err := s.HfMarinFillsV3(context.Background(), req) 678 | if err != nil { 679 | t.Fatal(err) 680 | } 681 | o := &HfMarinFillsV3Resp{} 682 | if err := rsp.ReadData(o); err != nil { 683 | t.Fatal(err) 684 | } 685 | t.Log(ToJsonString(o)) 686 | } 687 | 688 | { 689 | req := &HfMarinFillsV3Req{ 690 | Symbol: "PEPE-USDT", 691 | TradeType: "MARGIN_TRADE", 692 | Side: "buy", 693 | OrderId: "66ab00fc693a4f0007ac03db", 694 | } 695 | 696 | rsp, err := s.HfMarinFillsV3(context.Background(), req) 697 | if err != nil { 698 | t.Fatal(err) 699 | } 700 | o := &HfMarinFillsV3Resp{} 701 | if err := rsp.ReadData(o); err != nil { 702 | t.Fatal(err) 703 | } 704 | t.Log(ToJsonString(o)) 705 | } 706 | } 707 | --------------------------------------------------------------------------------