diff --git a/bybit_api_client.go b/bybit_api_client.go index d9b807d..8cbc2ee 100644 --- a/bybit_api_client.go +++ b/bybit_api_client.go @@ -5,23 +5,33 @@ import ( "context" "crypto/hmac" "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" - handlers "github.com/wuhewuhe/bybit.go.api/handlers" + "github.com/wuhewuhe/bybit.go.api/handlers" "io" "log" "net/http" "os" + "strconv" "time" ) +type ServerResponse struct { + RetCode int `json:"retCode"` + RetMsg string `json:"retMsg"` + Result interface{} `json:"result"` + RetExtInfo struct{} `json:"retExtInfo"` + Time int64 `json:"time"` +} + // TimeInForceType define time in force type of order type TimeInForceType string // Client define API client type Client struct { APIKey string - SecretKey string + APISecret string BaseURL string HTTPClient *http.Client Debug bool @@ -39,29 +49,13 @@ func WithDebug(debug bool) ClientOption { } } +// WithBaseURL is a client option to set the base URL of the Bybit HTTP client. func WithBaseURL(baseURL string) ClientOption { return func(c *Client) { c.BaseURL = baseURL } } -func currentTimestamp() int64 { - return FormatTimestamp(time.Now()) -} - -// FormatTimestamp formats a time into Unix timestamp in milliseconds, as requested by Bybit. -func FormatTimestamp(t time.Time) int64 { - return t.UnixNano() / int64(time.Millisecond) -} - -type ServerResponse struct { - RetCode int `json:"retCode"` - RetMsg string `json:"retMsg"` - Result interface{} `json:"result"` - RetExtInfo struct{} `json:"retExtInfo"` - Time int64 `json:"time"` -} - func PrettyPrint(i interface{}) string { s, _ := json.MarshalIndent(i, "", "\t") return string(s) @@ -74,11 +68,11 @@ func (c *Client) debug(format string, v ...interface{}) { } // NewBybitHttpClient NewClient Create client function for initialising new Bybit client -func NewBybitHttpClient(apiKey string, secretKey string, options ...ClientOption) *Client { +func NewBybitHttpClient(apiKey string, APISecret string, options ...ClientOption) *Client { c := &Client{ APIKey: apiKey, - SecretKey: secretKey, - BaseURL: "https://api.bybit.com", + APISecret: APISecret, + BaseURL: MAINNET, HTTPClient: http.DefaultClient, Logger: log.New(os.Stderr, Name, log.LstdFlags), } @@ -104,49 +98,47 @@ func (c *Client) parseRequest(r *request, opts ...RequestOption) (err error) { fullURL := fmt.Sprintf("%s%s", c.BaseURL, r.endpoint) queryString := r.query.Encode() - body := &bytes.Buffer{} - bodyString := r.form.Encode() header := http.Header{} + body := &bytes.Buffer{} + if r.params != nil { + body = bytes.NewBuffer(r.params) + } if r.header != nil { header = r.header.Clone() } header.Set("User-Agent", fmt.Sprintf("%s/%s", Name, Version)) - if bodyString != "" { - header.Set("Content-Type", "application/json") - body = bytes.NewBufferString(bodyString) - } + if r.secType == secTypeSigned { + now := time.Now() + unixNano := now.UnixNano() + timeStamp := unixNano / 1000000 header.Set(signTypeKey, "2") header.Set(apiRequestKey, c.APIKey) - header.Set(timestampKey, fmt.Sprintf("%d", currentTimestamp())) + header.Set(timestampKey, strconv.FormatInt(timeStamp, 10)) if r.recvWindow == "" { - header.Set(recvWindowKey, "5000") + r.recvWindow = "5000" + } + header.Set(recvWindowKey, r.recvWindow) + + var signatureBase []byte + if r.method == "POST" { + header.Set("Content-Type", "application/json") + signatureBase = []byte(strconv.FormatInt(timeStamp, 10) + c.APIKey + r.recvWindow + string(r.params[:])) } else { - header.Set(recvWindowKey, r.recvWindow) + signatureBase = []byte(strconv.FormatInt(timeStamp, 10) + c.APIKey + r.recvWindow + queryString) } - - var signatureBase string - if r.method == "GET" { - signatureBase = fmt.Sprintf("%d%s%s%s", FormatTimestamp(time.Now()), c.APIKey, r.recvWindow, queryString) - } else if r.method == "POST" { - signatureBase = fmt.Sprintf("%d%s%s%s", FormatTimestamp(time.Now()), c.APIKey, r.recvWindow, bodyString) - } - - mac := hmac.New(sha256.New, []byte(c.SecretKey)) - _, err = mac.Write([]byte(signatureBase)) - if err != nil { - return err - } - signatureValue := fmt.Sprintf("%x", mac.Sum(nil)) - header.Set(signatureKey, signatureValue) + hmac256 := hmac.New(sha256.New, []byte(c.APISecret)) + hmac256.Write(signatureBase) + signature := hex.EncodeToString(hmac256.Sum(nil)) + header.Set(signatureKey, signature) } if queryString != "" { fullURL = fmt.Sprintf("%s?%s", fullURL, queryString) } - c.debug("full url: %s, body: %s", fullURL, bodyString) + c.debug("full url: %s, body: %s", fullURL, body) r.fullURL = fullURL - r.header = header r.body = body + r.header = header return nil } @@ -200,3 +192,26 @@ func (c *Client) callAPI(ctx context.Context, r *request, opts ...RequestOption) func (c *Client) NewServerTimeService() *ServerTime { return &ServerTime{c: c} } + +// NewMarketKlineService Market Endpoints +func (c *Client) NewMarketKlineService(klineType, category, symbol, interval string) *Klines { + return &Klines{ + c: c, + category: category, + symbol: symbol, + interval: interval, + klineType: klineType, + } +} + +// NewPlaceOrderService Trade Endpoints +func (c *Client) NewPlaceOrderService(category, symbol, side, orderType, qty string) *Order { + return &Order{ + c: c, + category: category, + symbol: symbol, + side: side, + orderType: orderType, + qty: qty, + } +} diff --git a/consts.go b/consts.go index e8f1889..02028cc 100644 --- a/consts.go +++ b/consts.go @@ -3,6 +3,10 @@ package bybit_connector const ( Name = "bybit.api.go" Version = "1.0.0" + // Https + MAINNET = "https://api.bybit.com" + MAINNET_BACKT = "https://api.bytick.com" + TESTNET = "https://api-testnet.bybit.com" // WebSocket public channel - Mainnet SPOT_MAINNET = "wss://stream.bybit.com/v5/public/spot" LINEAR_MAINNET = "wss://stream.bybit.com/v5/public/linear" @@ -29,5 +33,5 @@ const ( signatureKey = "X-BAPI-SIGN" apiRequestKey = "X-BAPI-API-KEY" recvWindowKey = "X-BAPI-RECV-WINDOW" - signTypeKey = "2" + signTypeKey = "X-BAPI-SIGN-TYPE" ) diff --git a/examples/Trade/place_order.go b/examples/Trade/place_order.go new file mode 100644 index 0000000..e640850 --- /dev/null +++ b/examples/Trade/place_order.go @@ -0,0 +1,21 @@ +package main + +import ( + "context" + "fmt" + bybit "github.com/wuhewuhe/bybit.go.api" +) + +func main() { + PlaceOrder() +} + +func PlaceOrder() { + client := bybit.NewBybitHttpClient("8wYkmpLsMg10eNQyPm", "Ouxc34myDnXvei54XsBZgoQzfGxO4bkr2Zsj", bybit.WithBaseURL(bybit.TESTNET)) + orderResult, err := client.NewPlaceOrderService("linear", "XRPUSDT", "Buy", "Market", "10").Do(context.Background()) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(bybit.PrettyPrint(orderResult)) +} diff --git a/examples/market/market_kline.go b/examples/market/market_kline.go new file mode 100644 index 0000000..579b9fe --- /dev/null +++ b/examples/market/market_kline.go @@ -0,0 +1,24 @@ +package main + +import ( + "context" + "fmt" + bybit "github.com/wuhewuhe/bybit.go.api" +) + +func main() { + MarketKline() +} + +func MarketKline() { + + client := bybit.NewBybitHttpClient("", "") + + // NewServerTimeService + marketKline, err := client.NewMarketKlineService("premium-index-price-kline", "linear", "BTCUSDT", "1").Limit(10).Do(context.Background()) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(bybit.PrettyPrint(marketKline)) +} diff --git a/market.go b/market.go index aba27bd..b519089 100644 --- a/market.go +++ b/market.go @@ -34,3 +34,80 @@ func (s *ServerTime) Do(ctx context.Context, opts ...RequestOption) (res *Server } return res, nil } + +type MarketKlineCandle struct { + StartTime string `json:"startTime"` + OpenPrice string `json:"openPrice"` + HighPrice string `json:"highPrice"` + LowPrice string `json:"lowPrice"` + ClosePrice string `json:"closePrice"` + Volume string `json:"volume"` + Turnover string `json:"turnover"` +} + +type MarketKlineResponse struct { + Category string `json:"category"` + Symbol string `json:"symbol"` + List []MarketKlineCandle `json:"list"` +} + +// Klines Market Kline (GET /v5/market/kline) +type Klines struct { + c *Client + klineType string + category string + symbol string + interval string + limit *int + start *uint64 + end *uint64 +} + +// Limit set limit +func (s *Klines) Limit(limit int) *Klines { + s.limit = &limit + return s +} + +// Start set startTime +func (s *Klines) Start(startTime uint64) *Klines { + s.start = &startTime + return s +} + +// End set endTime +func (s *Klines) End(endTime uint64) *Klines { + s.end = &endTime + return s +} + +// Do Send the request +func (s *Klines) Do(ctx context.Context, opts ...RequestOption) (res *ServerResponse, err error) { + r := &request{ + method: http.MethodGet, + endpoint: "/v5/market/" + s.klineType, + secType: secTypeNone, + } + r.setParam("category", s.category) + r.setParam("symbol", s.symbol) + r.setParam("interval", s.interval) + if s.limit != nil { + r.setParam("limit", *s.limit) + } + if s.start != nil { + r.setParam("start", *s.start) + } + if s.end != nil { + r.setParam("end", *s.end) + } + data, err := s.c.callAPI(ctx, r, opts...) + if err != nil { + return nil, err + } + res = new(ServerResponse) + err = json.Unmarshal(data, res) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/request.go b/request.go index da54f9a..49ecd9e 100644 --- a/request.go +++ b/request.go @@ -1,8 +1,10 @@ package bybit_connector import ( + "encoding/json" "fmt" "io" + "log" "net/http" "net/url" ) @@ -21,12 +23,12 @@ type request struct { method string endpoint string query url.Values - form url.Values recvWindow string secType secType header http.Header - body io.Reader + params []byte fullURL string + body io.Reader } // addParam add param with key/value to query string @@ -47,11 +49,20 @@ func (r *request) setParam(key string, value interface{}) *request { return r } -// setParams set params with key/values to query string +// setParams set params with key/values to query string or body func (r *request) setParams(m params) *request { - for k, v := range m { - r.setParam(k, v) + if r.method == http.MethodGet { + for k, v := range m { + r.setParam(k, v) + } + } else if r.method == http.MethodPost { + jsonData, err := json.Marshal(m) + if err != nil { + log.Fatal(err) + } + r.params = jsonData } + return r } @@ -59,13 +70,10 @@ func (r *request) validate() (err error) { if r.query == nil { r.query = url.Values{} } - if r.form == nil { - r.form = url.Values{} - } return nil } -// Append `WithRecvWindow(insert_recvwindow)` to request to modify the default recvWindow value +// WithRecvWindow Append `WithRecvWindow(insert_recvWindow)` to request to modify the default recvWindow value func WithRecvWindow(recvWindow string) RequestOption { return func(r *request) { r.recvWindow = recvWindow diff --git a/trade.go b/trade.go index 06c9e41..530c031 100644 --- a/trade.go +++ b/trade.go @@ -1 +1,251 @@ package bybit_connector + +import ( + "context" + "encoding/json" + "net/http" +) + +type OrderResult struct { + OrderId string `json:"orderId"` + OrderLinkId string `json:"orderLinkId"` +} + +type Order struct { + c *Client + category string + symbol string + isLeverage *int + side string + orderType string + qty string + price *string + triggerDirection *int + orderFilter *string + triggerPrice *string + triggerBy *string + orderIv *string + timeInForce *string + positionIdx *int + orderLinkId *string + takeProfit *string + stopLoss *string + tpTriggerBy *string + slTriggerBy *string + reduceOnly *bool + closeOnTrigger *bool + smpType *string + mmp *bool + tpslMode *string + tpLimitPrice *string + slLimitPrice *string + tpOrderType *string + slOrderType *string +} + +func (o *Order) TimeInForce(tif string) *Order { + o.timeInForce = &tif + return o +} + +func (s *Order) IsLeverage(isLeverage int) *Order { + s.isLeverage = &isLeverage + return s +} + +func (s *Order) TriggerPrice(triggerPrice string) *Order { + s.triggerPrice = &triggerPrice + return s +} + +func (s *Order) OrderLinkId(orderLinkId string) *Order { + s.orderLinkId = &orderLinkId + return s +} + +func (o *Order) Price(price string) *Order { + o.price = &price + return o +} + +func (o *Order) TriggerDirection(direction int) *Order { + o.triggerDirection = &direction + return o +} + +func (o *Order) OrderFilter(filter string) *Order { + o.orderFilter = &filter + return o +} + +func (o *Order) TriggerBy(triggerBy string) *Order { + o.triggerBy = &triggerBy + return o +} + +func (o *Order) OrderIv(iv string) *Order { + o.orderIv = &iv + return o +} + +func (o *Order) PositionIdx(idx int) *Order { + o.positionIdx = &idx + return o +} + +func (o *Order) TakeProfit(profit string) *Order { + o.takeProfit = &profit + return o +} + +func (o *Order) StopLoss(loss string) *Order { + o.stopLoss = &loss + return o +} + +func (o *Order) TpTriggerBy(triggerBy string) *Order { + o.tpTriggerBy = &triggerBy + return o +} + +func (o *Order) SlTriggerBy(triggerBy string) *Order { + o.slTriggerBy = &triggerBy + return o +} + +func (o *Order) ReduceOnly(reduce bool) *Order { + o.reduceOnly = &reduce + return o +} + +func (o *Order) CloseOnTrigger(close bool) *Order { + o.closeOnTrigger = &close + return o +} + +func (o *Order) SmpType(smp string) *Order { + o.smpType = &smp + return o +} + +func (o *Order) Mmp(mmp bool) *Order { + o.mmp = &mmp + return o +} + +func (o *Order) TpslMode(mode string) *Order { + o.tpslMode = &mode + return o +} + +func (o *Order) TpLimitPrice(price string) *Order { + o.tpLimitPrice = &price + return o +} + +func (o *Order) SlLimitPrice(price string) *Order { + o.slLimitPrice = &price + return o +} + +func (o *Order) TpOrderType(orderType string) *Order { + o.tpOrderType = &orderType + return o +} + +func (o *Order) SlOrderType(orderType string) *Order { + o.slOrderType = &orderType + return o +} + +func (s *Order) Do(ctx context.Context, opts ...RequestOption) (*ServerResponse, error) { + r := &request{ + method: http.MethodPost, + endpoint: "/v5/order/create", + secType: secTypeSigned, + } + m := params{ + "category": s.category, + "symbol": s.symbol, + "side": s.side, + "orderType": s.orderType, + "qty": s.qty, + } + if s.price != nil { + m["price"] = *s.price + } + if s.triggerDirection != nil { + m["triggerDirection"] = *s.triggerDirection + } + if s.orderFilter != nil { + m["orderFilter"] = *s.orderFilter + } + if s.triggerPrice != nil { + m["triggerPrice"] = *s.triggerPrice + } + if s.triggerBy != nil { + m["triggerBy"] = *s.triggerBy + } + if s.orderIv != nil { + m["orderIv"] = *s.orderIv + } + if s.timeInForce != nil { + m["timeInForce"] = *s.timeInForce + } + if s.positionIdx != nil { + m["positionIdx"] = *s.positionIdx + } + if s.orderLinkId != nil { + m["orderLinkId"] = *s.orderLinkId + } + if s.takeProfit != nil { + m["takeProfit"] = *s.takeProfit + } + if s.stopLoss != nil { + m["stopLoss"] = *s.stopLoss + } + if s.tpTriggerBy != nil { + m["tpTriggerBy"] = *s.tpTriggerBy + } + if s.slTriggerBy != nil { + m["slTriggerBy"] = *s.slTriggerBy + } + if s.reduceOnly != nil { + m["reduceOnly"] = *s.reduceOnly + } + if s.closeOnTrigger != nil { + m["closeOnTrigger"] = *s.closeOnTrigger + } + if s.smpType != nil { + m["smpType"] = *s.smpType + } + if s.mmp != nil { + m["mmp"] = *s.mmp + } + if s.tpslMode != nil { + m["tpslMode"] = *s.tpslMode + } + if s.tpLimitPrice != nil { + m["tpLimitPrice"] = *s.tpLimitPrice + } + if s.slLimitPrice != nil { + m["slLimitPrice"] = *s.slLimitPrice + } + if s.tpOrderType != nil { + m["tpOrderType"] = *s.tpOrderType + } + if s.slOrderType != nil { + m["slOrderType"] = *s.slOrderType + } + r.setParams(m) + data, err := s.c.callAPI(ctx, r, opts...) + if err != nil { + return nil, err + } + res := new(ServerResponse) + err = json.Unmarshal(data, res) + if err != nil { + return nil, err + } + return res, nil +}