From aec9de8dd61bc14cc07ac1e2b593a77a22b4b464 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 28 May 2022 17:34:29 +0800 Subject: [PATCH 01/10] types: define global margin history types --- pkg/types/margin.go | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pkg/types/margin.go b/pkg/types/margin.go index 10e24e9e9..dde770514 100644 --- a/pkg/types/margin.go +++ b/pkg/types/margin.go @@ -51,12 +51,59 @@ type MarginExchange interface { GetMarginSettings() MarginSettings } +// MarginBorrowRepay provides repay and borrow actions of an crypto exchange type MarginBorrowRepay interface { RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error BorrowMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error QueryMarginAssetMaxBorrowable(ctx context.Context, asset string) (amount fixedpoint.Value, err error) } +type MarginInterest struct { + Asset string `json:"asset" db:"asset"` + Principle fixedpoint.Value `json:"principle" db:"principle"` + Interest fixedpoint.Value `json:"interest" db:"interest"` + InterestRate fixedpoint.Value `json:"interestRate" db:"interest_rate"` + IsolatedSymbol string `json:"isolatedSymbol" db:"isolated_symbol"` + Time Time `json:"time" db:"time"` +} + +type MarginLoanRecord struct { + TransactionID uint64 `json:"transactionID" db:"transaction_id"` + Asset string `json:"asset" db:"asset"` + Principle fixedpoint.Value `json:"principle" db:"principle"` + Time Time `json:"time" db:"time"` + IsolatedSymbol string `json:"isolatedSymbol" db:"isolated_symbol"` +} + +type MarginRepayRecord struct { + TransactionID uint64 `json:"transactionID" db:"transaction_id"` + Asset string `json:"asset" db:"asset"` + Principle fixedpoint.Value `json:"principle" db:"principle"` + Time Time `json:"time" db:"time"` + IsolatedSymbol string `json:"isolatedSymbol" db:"isolated_symbol"` +} + +type MarginLiquidationRecord struct { + AveragePrice fixedpoint.Value `json:"avgPrice"` + ExecutedQuantity fixedpoint.Value `json:"executedQty"` + OrderId uint64 `json:"orderId"` + Price fixedpoint.Value `json:"price"` + Qty fixedpoint.Value `json:"qty"` + Side SideType `json:"side"` + Symbol string `json:"symbol"` + TimeInForce TimeInForce `json:"timeInForce"` + IsIsolated bool `json:"isIsolated"` + UpdatedTime Time `json:"updatedTime"` +} + +// MarginHistory provides the service of querying loan history and repay history +type MarginHistory interface { + QueryLoanHistory() ([]MarginLoanRecord, error) + QueryRepayHistory() ([]MarginRepayRecord, error) + QueryLiquidationHistory() ([]MarginLiquidationRecord, error) + QueryInterestHistory() ([]MarginInterest, error) +} + type MarginSettings struct { IsMargin bool IsIsolatedMargin bool From 1ab10eb574aa882e5e2782c2976ba8d0f2cfc461 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 29 May 2022 00:52:22 +0800 Subject: [PATCH 02/10] binance: fix and add loan/repay history test --- pkg/exchange/binance/binanceapi/client.go | 52 ++-- .../get_margin_loan_history_request.go | 54 ++++ ..._margin_loan_history_request_requestgen.go | 234 ++++++++++++++++++ .../get_margin_loan_history_request_test.go | 29 +++ .../get_margin_repay_history_request.go | 44 ++++ ...margin_repay_history_request_requestgen.go | 234 ++++++++++++++++++ .../get_margin_repay_history_request_test.go | 29 +++ .../get_spot_rebate_history_request.go | 14 +- ..._spot_rebate_history_request_requestgen.go | 4 +- pkg/exchange/binance/binanceapi/page.go | 15 ++ pkg/exchange/binance/binanceapi/rows.go | 8 + 11 files changed, 672 insertions(+), 45 deletions(-) create mode 100644 pkg/exchange/binance/binanceapi/get_margin_loan_history_request.go create mode 100644 pkg/exchange/binance/binanceapi/get_margin_loan_history_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/get_margin_loan_history_request_test.go create mode 100644 pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go create mode 100644 pkg/exchange/binance/binanceapi/get_margin_repay_history_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/get_margin_repay_history_request_test.go create mode 100644 pkg/exchange/binance/binanceapi/page.go create mode 100644 pkg/exchange/binance/binanceapi/rows.go diff --git a/pkg/exchange/binance/binanceapi/client.go b/pkg/exchange/binance/binanceapi/client.go index fefc85042..f23973574 100644 --- a/pkg/exchange/binance/binanceapi/client.go +++ b/pkg/exchange/binance/binanceapi/client.go @@ -14,6 +14,7 @@ import ( "github.com/c9s/requestgen" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/types" ) @@ -21,14 +22,16 @@ import ( const defaultHTTPTimeout = time.Second * 15 const RestBaseURL = "https://api.binance.com" const SandboxRestBaseURL = "https://testnet.binance.vision" +const DebugRequestResponse = false + +var defaultHttpClient = &http.Client{ + Timeout: defaultHTTPTimeout, +} type RestClient struct { - BaseURL *url.URL + requestgen.BaseAPIClient - client *http.Client - - Key, Secret, Passphrase string - KeyVersion string + Key, Secret string recvWindow int timeOffset int64 @@ -41,10 +44,9 @@ func NewClient() *RestClient { } client := &RestClient{ - BaseURL: u, - KeyVersion: "2", - client: &http.Client{ - Timeout: defaultHTTPTimeout, + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: defaultHttpClient, }, } @@ -77,27 +79,6 @@ func (c *RestClient) NewRequest(ctx context.Context, method, refURL string, para return http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) } -// sendRequest sends the request to the API server and handle the response -func (c *RestClient) SendRequest(req *http.Request) (*requestgen.Response, error) { - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - // newResponse reads the response body and return a new Response object - response, err := requestgen.NewResponse(resp) - if err != nil { - return response, err - } - - // Check error, if there is an error, return the ErrorResponse struct type - if response.IsError() { - return response, errors.New(string(response.Body)) - } - - return response, nil -} - func (c *RestClient) SetTimeOffsetFromServer(ctx context.Context) error { req, err := c.NewRequest(ctx, "GET", "/api/v3/time", nil, nil) if err != nil { @@ -122,6 +103,17 @@ func (c *RestClient) SetTimeOffsetFromServer(ctx context.Context) error { return nil } +func (c *RestClient) SendRequest(req *http.Request) (*requestgen.Response, error) { + if DebugRequestResponse { + logrus.Debugf("-> request: %+v", req) + response, err := c.BaseAPIClient.SendRequest(req) + logrus.Debugf("<- response: %s", string(response.Body)) + return response, err + } + + return c.BaseAPIClient.SendRequest(req) +} + // newAuthenticatedRequest creates new http request for authenticated routes. func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { if len(c.Key) == 0 { diff --git a/pkg/exchange/binance/binanceapi/get_margin_loan_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request.go new file mode 100644 index 000000000..e7a801a9d --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request.go @@ -0,0 +1,54 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// one of PENDING (pending execution), CONFIRMED (successfully loaned), FAILED (execution failed, nothing happened to your account); +type LoanStatus string + +const ( + LoanStatusPending LoanStatus = "PENDING" + LoanStatusConfirmed LoanStatus = "CONFIRMED" + LoanStatusFailed LoanStatus = "FAILED" +) + +type MarginLoanRecord struct { + IsolatedSymbol string `json:"isolatedSymbol"` + TxId int64 `json:"txId"` + Asset string `json:"asset"` + Principal fixedpoint.Value `json:"principal"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` + Status LoanStatus `json:"status"` +} + +// GetMarginLoanHistoryRequest +// +// txId or startTime must be sent. txId takes precedence. +// Response in descending order +// If isolatedSymbol is not sent, crossed margin data will be returned +// The max interval between startTime and endTime is 30 days. +// If startTime and endTime not sent, return records of the last 7 days by default +// Set archived to true to query data from 6 months ago +// +//go:generate requestgen -method GET -url "/sapi/v1/margin/loan" -type GetMarginLoanHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginLoanRecord +type GetMarginLoanHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + isolatedSymbol *string `param:"isolatedSymbol"` + archived *bool `param:"archived"` + size *int `param:"size"` + current *int `param:"current"` +} + +func (c *RestClient) NewGetMarginLoanHistoryRequest() *GetMarginLoanHistoryRequest { + return &GetMarginLoanHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_requestgen.go new file mode 100644 index 000000000..d893d55f5 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_requestgen.go @@ -0,0 +1,234 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/margin/loan -type GetMarginLoanHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginLoanRecord"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginLoanHistoryRequest) Asset(asset string) *GetMarginLoanHistoryRequest { + g.asset = asset + return g +} + +func (g *GetMarginLoanHistoryRequest) StartTime(startTime time.Time) *GetMarginLoanHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginLoanHistoryRequest) EndTime(endTime time.Time) *GetMarginLoanHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginLoanHistoryRequest) IsolatedSymbol(isolatedSymbol string) *GetMarginLoanHistoryRequest { + g.isolatedSymbol = &isolatedSymbol + return g +} + +func (g *GetMarginLoanHistoryRequest) Archived(archived bool) *GetMarginLoanHistoryRequest { + g.archived = &archived + return g +} + +func (g *GetMarginLoanHistoryRequest) Size(size int) *GetMarginLoanHistoryRequest { + g.size = &size + return g +} + +func (g *GetMarginLoanHistoryRequest) Current(current int) *GetMarginLoanHistoryRequest { + g.current = ¤t + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginLoanHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginLoanHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := g.asset + + // assign parameter of asset + params["asset"] = asset + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check isolatedSymbol field -> json key isolatedSymbol + if g.isolatedSymbol != nil { + isolatedSymbol := *g.isolatedSymbol + + // assign parameter of isolatedSymbol + params["isolatedSymbol"] = isolatedSymbol + } else { + } + // check archived field -> json key archived + if g.archived != nil { + archived := *g.archived + + // assign parameter of archived + params["archived"] = archived + } else { + } + // check size field -> json key size + if g.size != nil { + size := *g.size + + // assign parameter of size + params["size"] = size + } else { + } + // check current field -> json key current + if g.current != nil { + current := *g.current + + // assign parameter of current + params["current"] = current + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginLoanHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginLoanHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginLoanHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginLoanHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginLoanHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginLoanHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginLoanHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginLoanHistoryRequest) Do(ctx context.Context) ([]MarginLoanRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/margin/loan" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse RowsResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []MarginLoanRecord + if err := json.Unmarshal(apiResponse.Rows, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_test.go b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_test.go new file mode 100644 index 000000000..c9daa028f --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_test.go @@ -0,0 +1,29 @@ +package binanceapi + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_GetMarginLoanHistoryRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetMarginLoanHistoryRequest() + req.Asset("USDT") + req.IsolatedSymbol("DOTUSDT") + req.StartTime(time.Date(2022, time.February, 1, 0, 0, 0, 0, time.UTC)) + req.EndTime(time.Date(2022, time.March, 1, 0, 0, 0, 0, time.UTC)) + req.Size(100) + + records, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, records) + t.Logf("loans: %+v", records) +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go new file mode 100644 index 000000000..e6c481cfe --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go @@ -0,0 +1,44 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" +) + +// RepayStatus one of PENDING (pending execution), CONFIRMED (successfully loaned), FAILED (execution failed, nothing happened to your account); +type RepayStatus string + +const ( + RepayStatusPending LoanStatus = "PENDING" + RepayStatusConfirmed LoanStatus = "CONFIRMED" + RepayStatusFailed LoanStatus = "FAILED" +) + +type MarginRepayRecord struct { + IsolatedSymbol string `json:"isolatedSymbol"` + Amount string `json:"amount"` + Asset string `json:"asset"` + Interest string `json:"interest"` + Principal string `json:"principal"` + Status string `json:"status"` + Timestamp int64 `json:"timestamp"` + TxId int64 `json:"txId"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/margin/repay" -type GetMarginRepayHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginRepayRecord +type GetMarginRepayHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + isolatedSymbol *string `param:"isolatedSymbol"` + archived *bool `param:"archived"` + size *int `param:"size"` + current *int `param:"current"` +} + +func (c *RestClient) NewGetMarginRepayHistoryRequest() *GetMarginRepayHistoryRequest { + return &GetMarginRepayHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_requestgen.go new file mode 100644 index 000000000..17e536415 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_requestgen.go @@ -0,0 +1,234 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/margin/repay -type GetMarginRepayHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginRepayRecord"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginRepayHistoryRequest) Asset(asset string) *GetMarginRepayHistoryRequest { + g.asset = asset + return g +} + +func (g *GetMarginRepayHistoryRequest) StartTime(startTime time.Time) *GetMarginRepayHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginRepayHistoryRequest) EndTime(endTime time.Time) *GetMarginRepayHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginRepayHistoryRequest) IsolatedSymbol(isolatedSymbol string) *GetMarginRepayHistoryRequest { + g.isolatedSymbol = &isolatedSymbol + return g +} + +func (g *GetMarginRepayHistoryRequest) Archived(archived bool) *GetMarginRepayHistoryRequest { + g.archived = &archived + return g +} + +func (g *GetMarginRepayHistoryRequest) Size(size int) *GetMarginRepayHistoryRequest { + g.size = &size + return g +} + +func (g *GetMarginRepayHistoryRequest) Current(current int) *GetMarginRepayHistoryRequest { + g.current = ¤t + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginRepayHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginRepayHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := g.asset + + // assign parameter of asset + params["asset"] = asset + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check isolatedSymbol field -> json key isolatedSymbol + if g.isolatedSymbol != nil { + isolatedSymbol := *g.isolatedSymbol + + // assign parameter of isolatedSymbol + params["isolatedSymbol"] = isolatedSymbol + } else { + } + // check archived field -> json key archived + if g.archived != nil { + archived := *g.archived + + // assign parameter of archived + params["archived"] = archived + } else { + } + // check size field -> json key size + if g.size != nil { + size := *g.size + + // assign parameter of size + params["size"] = size + } else { + } + // check current field -> json key current + if g.current != nil { + current := *g.current + + // assign parameter of current + params["current"] = current + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginRepayHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginRepayHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginRepayHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginRepayHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginRepayHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginRepayHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginRepayHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginRepayHistoryRequest) Do(ctx context.Context) ([]MarginRepayRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/margin/repay" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse RowsResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []MarginRepayRecord + if err := json.Unmarshal(apiResponse.Rows, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_test.go b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_test.go new file mode 100644 index 000000000..5161d32ff --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_test.go @@ -0,0 +1,29 @@ +package binanceapi + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_GetMarginRepayHistoryRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetMarginRepayHistoryRequest() + req.Asset("USDT") + req.IsolatedSymbol("DOTUSDT") + req.StartTime(time.Date(2022, time.February, 1, 0, 0, 0, 0, time.UTC)) + req.EndTime(time.Date(2022, time.March, 1, 0, 0, 0, 0, time.UTC)) + req.Size(100) + + records, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, records) + t.Logf("loans: %+v", records) +} diff --git a/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go b/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go index a9a822eab..eb3db5ab8 100644 --- a/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go +++ b/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go @@ -1,7 +1,6 @@ package binanceapi import ( - "encoding/json" "time" "github.com/c9s/requestgen" @@ -25,23 +24,12 @@ type SpotRebate struct { UpdateTime types.MillisecondTimestamp `json:"updateTime"` } -type PagedResponse struct { - Status string `json:"status"` - Type string `json:"type"` - Code string `json:"code"` - Data struct { - Page int `json:"page"` - TotalRecords int `json:"totalRecords"` - TotalPageNum int `json:"totalPageNum"` - Data json.RawMessage `json:"data"` - } `json:"data"` -} // GetSpotRebateHistoryRequest // The max interval between startTime and endTime is 30 days. // If startTime and endTime are not sent, the recent 7 days' data will be returned. // The earliest startTime is supported on June 10, 2020 -//go:generate requestgen -method GET -url "/sapi/v1/rebate/taxQuery" -type GetSpotRebateHistoryRequest -responseType PagedResponse -responseDataField Data.Data -responseDataType []SpotRebate +//go:generate requestgen -method GET -url "/sapi/v1/rebate/taxQuery" -type GetSpotRebateHistoryRequest -responseType PagedDataResponse -responseDataField Data.Data -responseDataType []SpotRebate type GetSpotRebateHistoryRequest struct { client requestgen.AuthenticatedAPIClient diff --git a/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request_requestgen.go index cb9e29a03..05cc5b67e 100644 --- a/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request_requestgen.go +++ b/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method GET -url /sapi/v1/rebate/taxQuery -type GetSpotRebateHistoryRequest -responseType PagedResponse -responseDataField Data.Data -responseDataType []SpotRebate"; DO NOT EDIT. +// Code generated by "requestgen -method GET -url /sapi/v1/rebate/taxQuery -type GetSpotRebateHistoryRequest -responseType PagedDataResponse -responseDataField Data.Data -responseDataType []SpotRebate"; DO NOT EDIT. package binanceapi @@ -160,7 +160,7 @@ func (g *GetSpotRebateHistoryRequest) Do(ctx context.Context) ([]SpotRebate, err return nil, err } - var apiResponse PagedResponse + var apiResponse PagedDataResponse if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } diff --git a/pkg/exchange/binance/binanceapi/page.go b/pkg/exchange/binance/binanceapi/page.go new file mode 100644 index 000000000..1daec6472 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/page.go @@ -0,0 +1,15 @@ +package binanceapi + +import "encoding/json" + +type PagedDataResponse struct { + Status string `json:"status"` + Type string `json:"type"` + Code string `json:"code"` + Data struct { + Page int `json:"page"` + TotalRecords int `json:"totalRecords"` + TotalPageNum int `json:"totalPageNum"` + Data json.RawMessage `json:"data"` + } `json:"data"` +} diff --git a/pkg/exchange/binance/binanceapi/rows.go b/pkg/exchange/binance/binanceapi/rows.go new file mode 100644 index 000000000..60398419a --- /dev/null +++ b/pkg/exchange/binance/binanceapi/rows.go @@ -0,0 +1,8 @@ +package binanceapi + +import "encoding/json" + +type RowsResponse struct { + Rows json.RawMessage `json:"rows"` + Total int `json:"total"` +} From e72f8bcd1556b1a994f0412367056ca681f0517a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 29 May 2022 00:57:46 +0800 Subject: [PATCH 03/10] binance: fix and rename margin liquidation history request --- .../get_force_liquidation_record_request.go | 43 ---- .../get_margin_liquidation_history_request.go | 38 ++++ ..._liquidation_history_request_requestgen.go | 211 ++++++++++++++++++ 3 files changed, 249 insertions(+), 43 deletions(-) delete mode 100644 pkg/exchange/binance/binanceapi/get_force_liquidation_record_request.go create mode 100644 pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go create mode 100644 pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/get_force_liquidation_record_request.go b/pkg/exchange/binance/binanceapi/get_force_liquidation_record_request.go deleted file mode 100644 index a4ee7268f..000000000 --- a/pkg/exchange/binance/binanceapi/get_force_liquidation_record_request.go +++ /dev/null @@ -1,43 +0,0 @@ -package binanceapi - -import ( - "time" - - "github.com/c9s/requestgen" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -type ForceLiquidationRecord2 struct { - Asset string `json:"asset"` - DailyInterestRate fixedpoint.Value `json:"dailyInterestRate"` - Timestamp types.MillisecondTimestamp `json:"timestamp"` - VipLevel int `json:"vipLevel"` -} - -type ForceLiquidationRecord struct { - AvgPrice fixedpoint.Value `json:"avgPrice"` - ExecutedQty fixedpoint.Value `json:"executedQty"` - OrderId uint64 `json:"orderId"` - Price fixedpoint.Value `json:"price"` - Qty fixedpoint.Value `json:"qty"` - Side SideType `json:"side"` - Symbol string `json:"symbol"` - TimeInForce string `json:"timeInForce"` - IsIsolated bool `json:"isIsolated"` - UpdatedTime types.MillisecondTimestamp `json:"updatedTime"` -} - -//go:generate requestgen -method GET -url "/sapi/v1/margin/interestRateHistory" -type GetForceLiquidationRecordRequest -responseType []ForceLiquidationRecord -type GetForceLiquidationRecordRequest struct { - client requestgen.AuthenticatedAPIClient - - asset string `param:"asset"` - startTime *time.Time `param:"startTime,milliseconds"` - endTime *time.Time `param:"endTime,milliseconds"` -} - -func (c *RestClient) NewGetForceLiquidationRecordRequest() *GetForceLiquidationRecordRequest { - return &GetForceLiquidationRecordRequest{client: c} -} diff --git a/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go new file mode 100644 index 000000000..1872d4ac4 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go @@ -0,0 +1,38 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type MarginLiquidationRecord struct { + AvgPrice fixedpoint.Value `json:"avgPrice"` + ExecutedQty fixedpoint.Value `json:"executedQty"` + OrderId uint64 `json:"orderId"` + Price fixedpoint.Value `json:"price"` + Qty fixedpoint.Value `json:"qty"` + Side SideType `json:"side"` + Symbol string `json:"symbol"` + TimeInForce string `json:"timeInForce"` + IsIsolated bool `json:"isIsolated"` + UpdatedTime types.MillisecondTimestamp `json:"updatedTime"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/margin/forceLiquidationRec" -type GetMarginLiquidationHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginLiquidationRecord +type GetMarginLiquidationHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + isolatedSymbol *string `param:"isolatedSymbol"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + size *int `param:"size"` + current *int `param:"current"` +} + +func (c *RestClient) NewGetMarginLiquidationHistoryRequest() *GetMarginLiquidationHistoryRequest { + return &GetMarginLiquidationHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request_requestgen.go new file mode 100644 index 000000000..942491998 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request_requestgen.go @@ -0,0 +1,211 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/margin/forceLiquidationRec -type GetMarginLiquidationHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginLiquidationRecord"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginLiquidationHistoryRequest) IsolatedSymbol(isolatedSymbol string) *GetMarginLiquidationHistoryRequest { + g.isolatedSymbol = &isolatedSymbol + return g +} + +func (g *GetMarginLiquidationHistoryRequest) StartTime(startTime time.Time) *GetMarginLiquidationHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginLiquidationHistoryRequest) EndTime(endTime time.Time) *GetMarginLiquidationHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginLiquidationHistoryRequest) Size(size int) *GetMarginLiquidationHistoryRequest { + g.size = &size + return g +} + +func (g *GetMarginLiquidationHistoryRequest) Current(current int) *GetMarginLiquidationHistoryRequest { + g.current = ¤t + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginLiquidationHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginLiquidationHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check isolatedSymbol field -> json key isolatedSymbol + if g.isolatedSymbol != nil { + isolatedSymbol := *g.isolatedSymbol + + // assign parameter of isolatedSymbol + params["isolatedSymbol"] = isolatedSymbol + } else { + } + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check size field -> json key size + if g.size != nil { + size := *g.size + + // assign parameter of size + params["size"] = size + } else { + } + // check current field -> json key current + if g.current != nil { + current := *g.current + + // assign parameter of current + params["current"] = current + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginLiquidationHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginLiquidationHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginLiquidationHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginLiquidationHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginLiquidationHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginLiquidationHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginLiquidationHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginLiquidationHistoryRequest) Do(ctx context.Context) ([]MarginLiquidationRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/margin/forceLiquidationRec" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse RowsResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []MarginLiquidationRecord + if err := json.Unmarshal(apiResponse.Rows, &data); err != nil { + return nil, err + } + return data, nil +} From 4c30fce917740843a8314303978fc6da9324f792 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 29 May 2022 01:13:33 +0800 Subject: [PATCH 04/10] binance: add GetMarginInterestHistoryRequest api --- .../get_margin_interest_history_request.go | 52 ++++ ...gin_interest_history_request_requestgen.go | 234 ++++++++++++++++++ ...et_margin_interest_history_request_test.go | 29 +++ ...et_margin_interest_rate_history_request.go | 2 +- ...nterest_rate_history_request_requestgen.go | 2 +- .../get_margin_liquidation_history_request.go | 20 +- 6 files changed, 327 insertions(+), 12 deletions(-) create mode 100644 pkg/exchange/binance/binanceapi/get_margin_interest_history_request.go create mode 100644 pkg/exchange/binance/binanceapi/get_margin_interest_history_request_requestgen.go create mode 100644 pkg/exchange/binance/binanceapi/get_margin_interest_history_request_test.go diff --git a/pkg/exchange/binance/binanceapi/get_margin_interest_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request.go new file mode 100644 index 000000000..59d241d1b --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request.go @@ -0,0 +1,52 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// interest type in response has 4 enums: +// PERIODIC interest charged per hour +// ON_BORROW first interest charged on borrow +// PERIODIC_CONVERTED interest charged per hour converted into BNB +// ON_BORROW_CONVERTED first interest charged on borrow converted into BNB +type InterestType string + +const ( + InterestTypePeriodic InterestType = "PERIODIC" + InterestTypeOnBorrow InterestType = "ON_BORROW" + InterestTypePeriodicConverted InterestType = "PERIODIC_CONVERTED" + InterestTypeOnBorrowConverted InterestType = "ON_BORROW_CONVERTED" +) + +// MarginInterest is the user margin interest record +type MarginInterest struct { + IsolatedSymbol string `json:"isolatedSymbol"` + Asset string `json:"asset"` + Interest fixedpoint.Value `json:"interest"` + InterestAccuredTime types.MillisecondTimestamp `json:"interestAccuredTime"` + InterestRate fixedpoint.Value `json:"interestRate"` + Principal fixedpoint.Value `json:"principal"` + Type InterestType `json:"type"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/margin/interestHistory" -type GetMarginInterestHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginInterest +type GetMarginInterestHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + isolatedSymbol *string `param:"isolatedSymbol"` + archived *bool `param:"archived"` + size *int `param:"size"` + current *int `param:"current"` +} + +func (c *RestClient) NewGetMarginInterestHistoryRequest() *GetMarginInterestHistoryRequest { + return &GetMarginInterestHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_requestgen.go new file mode 100644 index 000000000..b73d167f8 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_requestgen.go @@ -0,0 +1,234 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/margin/interestHistory -type GetMarginInterestHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginInterest"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginInterestHistoryRequest) Asset(asset string) *GetMarginInterestHistoryRequest { + g.asset = asset + return g +} + +func (g *GetMarginInterestHistoryRequest) StartTime(startTime time.Time) *GetMarginInterestHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginInterestHistoryRequest) EndTime(endTime time.Time) *GetMarginInterestHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginInterestHistoryRequest) IsolatedSymbol(isolatedSymbol string) *GetMarginInterestHistoryRequest { + g.isolatedSymbol = &isolatedSymbol + return g +} + +func (g *GetMarginInterestHistoryRequest) Archived(archived bool) *GetMarginInterestHistoryRequest { + g.archived = &archived + return g +} + +func (g *GetMarginInterestHistoryRequest) Size(size int) *GetMarginInterestHistoryRequest { + g.size = &size + return g +} + +func (g *GetMarginInterestHistoryRequest) Current(current int) *GetMarginInterestHistoryRequest { + g.current = ¤t + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginInterestHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginInterestHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := g.asset + + // assign parameter of asset + params["asset"] = asset + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check isolatedSymbol field -> json key isolatedSymbol + if g.isolatedSymbol != nil { + isolatedSymbol := *g.isolatedSymbol + + // assign parameter of isolatedSymbol + params["isolatedSymbol"] = isolatedSymbol + } else { + } + // check archived field -> json key archived + if g.archived != nil { + archived := *g.archived + + // assign parameter of archived + params["archived"] = archived + } else { + } + // check size field -> json key size + if g.size != nil { + size := *g.size + + // assign parameter of size + params["size"] = size + } else { + } + // check current field -> json key current + if g.current != nil { + current := *g.current + + // assign parameter of current + params["current"] = current + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginInterestHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginInterestHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginInterestHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginInterestHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginInterestHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginInterestHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginInterestHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginInterestHistoryRequest) Do(ctx context.Context) ([]MarginInterest, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/margin/interestHistory" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse RowsResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []MarginInterest + if err := json.Unmarshal(apiResponse.Rows, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_test.go b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_test.go new file mode 100644 index 000000000..60540c35c --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_test.go @@ -0,0 +1,29 @@ +package binanceapi + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_GetMarginInterestHistoryRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetMarginInterestHistoryRequest() + req.Asset("USDT") + req.IsolatedSymbol("DOTUSDT") + req.StartTime(time.Date(2022, time.February, 1, 0, 0, 0, 0, time.UTC)) + req.EndTime(time.Date(2022, time.March, 1, 0, 0, 0, 0, time.UTC)) + req.Size(100) + + records, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, records) + t.Logf("interest: %+v", records) +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request.go index e0a786acb..86d05dd72 100644 --- a/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request.go +++ b/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request.go @@ -18,7 +18,7 @@ type MarginInterestRate struct { //go:generate requestgen -method GET -url "/sapi/v1/margin/interestRateHistory" -type GetMarginInterestRateHistoryRequest -responseType []MarginInterestRate type GetMarginInterestRateHistoryRequest struct { - client requestgen.AuthenticatedAPIClient + client requestgen.APIClient asset string `param:"asset"` startTime *time.Time `param:"startTime,milliseconds"` diff --git a/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request_requestgen.go index cd569e773..1f80665cc 100644 --- a/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request_requestgen.go +++ b/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request_requestgen.go @@ -160,7 +160,7 @@ func (g *GetMarginInterestRateHistoryRequest) Do(ctx context.Context) ([]MarginI apiURL := "/sapi/v1/margin/interestRateHistory" - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) if err != nil { return nil, err } diff --git a/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go index 1872d4ac4..31ce6c73d 100644 --- a/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go +++ b/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go @@ -10,16 +10,16 @@ import ( ) type MarginLiquidationRecord struct { - AvgPrice fixedpoint.Value `json:"avgPrice"` - ExecutedQty fixedpoint.Value `json:"executedQty"` - OrderId uint64 `json:"orderId"` - Price fixedpoint.Value `json:"price"` - Qty fixedpoint.Value `json:"qty"` - Side SideType `json:"side"` - Symbol string `json:"symbol"` - TimeInForce string `json:"timeInForce"` - IsIsolated bool `json:"isIsolated"` - UpdatedTime types.MillisecondTimestamp `json:"updatedTime"` + AveragePrice fixedpoint.Value `json:"avgPrice"` + ExecutedQuantity fixedpoint.Value `json:"executedQty"` + OrderId uint64 `json:"orderId"` + Price fixedpoint.Value `json:"price"` + Quantity fixedpoint.Value `json:"qty"` + Side SideType `json:"side"` + Symbol string `json:"symbol"` + TimeInForce string `json:"timeInForce"` + IsIsolated bool `json:"isIsolated"` + UpdatedTime types.MillisecondTimestamp `json:"updatedTime"` } //go:generate requestgen -method GET -url "/sapi/v1/margin/forceLiquidationRec" -type GetMarginLiquidationHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginLiquidationRecord From f58f44ffd8d96ec0edeb50408136e1211622bff8 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 29 May 2022 01:21:43 +0800 Subject: [PATCH 05/10] binance: refactor query methods --- pkg/exchange/binance/exchange.go | 320 +++++++++++++++---------------- 1 file changed, 151 insertions(+), 169 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 19ad5e42a..87fc67280 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -1284,145 +1284,157 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type return kLines, nil } +func (e *Exchange) queryMarginTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { + var remoteTrades []*binance.TradeV3 + req := e.client.NewListMarginTradesService(). + IsIsolated(e.IsIsolatedMargin). + Symbol(symbol) + + if options.Limit > 0 { + req.Limit(int(options.Limit)) + } else { + req.Limit(1000) + } + + // BINANCE uses inclusive last trade ID + if options.LastTradeID > 0 { + req.FromID(int64(options.LastTradeID)) + } + + if options.StartTime != nil && options.EndTime != nil { + if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { + req.StartTime(options.StartTime.UnixMilli()) + req.EndTime(options.EndTime.UnixMilli()) + } else { + req.StartTime(options.StartTime.UnixMilli()) + } + } else if options.StartTime != nil { + req.StartTime(options.StartTime.UnixMilli()) + } else if options.EndTime != nil { + req.EndTime(options.EndTime.UnixMilli()) + } + + remoteTrades, err = req.Do(ctx) + if err != nil { + return nil, err + } + for _, t := range remoteTrades { + localTrade, err := toGlobalTrade(*t, e.IsMargin) + if err != nil { + log.WithError(err).Errorf("can not convert binance trade: %+v", t) + continue + } + + trades = append(trades, *localTrade) + } + + trades = types.SortTradesAscending(trades) + return trades, nil +} + +func (e *Exchange) queryFuturesTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { + + var remoteTrades []*futures.AccountTrade + req := e.futuresClient.NewListAccountTradeService(). + Symbol(symbol) + if options.Limit > 0 { + req.Limit(int(options.Limit)) + } else { + req.Limit(1000) + } + + // BINANCE uses inclusive last trade ID + if options.LastTradeID > 0 { + req.FromID(int64(options.LastTradeID)) + } + + // The parameter fromId cannot be sent with startTime or endTime. + // Mentioned in binance futures docs + if options.LastTradeID <= 0 { + if options.StartTime != nil && options.EndTime != nil { + if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { + req.StartTime(options.StartTime.UnixMilli()) + req.EndTime(options.EndTime.UnixMilli()) + } else { + req.StartTime(options.StartTime.UnixMilli()) + } + } else if options.EndTime != nil { + req.EndTime(options.EndTime.UnixMilli()) + } + } + + remoteTrades, err = req.Do(ctx) + if err != nil { + return nil, err + } + for _, t := range remoteTrades { + localTrade, err := toGlobalFuturesTrade(*t) + if err != nil { + log.WithError(err).Errorf("can not convert binance futures trade: %+v", t) + continue + } + + trades = append(trades, *localTrade) + } + + trades = types.SortTradesAscending(trades) + return trades, nil +} + +func (e *Exchange) querySpotTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { + var remoteTrades []*binance.TradeV3 + req := e.client.NewListTradesService(). + Symbol(symbol) + + if options.Limit > 0 { + req.Limit(int(options.Limit)) + } else { + req.Limit(1000) + } + + // BINANCE uses inclusive last trade ID + if options.LastTradeID > 0 { + req.FromID(int64(options.LastTradeID)) + } + + if options.StartTime != nil && options.EndTime != nil { + if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { + req.StartTime(options.StartTime.UnixMilli()) + req.EndTime(options.EndTime.UnixMilli()) + } else { + req.StartTime(options.StartTime.UnixMilli()) + } + } else if options.StartTime != nil { + req.StartTime(options.StartTime.UnixMilli()) + } else if options.EndTime != nil { + req.EndTime(options.EndTime.UnixMilli()) + } + + remoteTrades, err = req.Do(ctx) + if err != nil { + return nil, err + } + for _, t := range remoteTrades { + localTrade, err := toGlobalTrade(*t, e.IsMargin) + if err != nil { + log.WithError(err).Errorf("can not convert binance trade: %+v", t) + continue + } + + trades = append(trades, *localTrade) + } + + trades = types.SortTradesAscending(trades) + return trades, nil +} + func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { if e.IsMargin { - var remoteTrades []*binance.TradeV3 - req := e.client.NewListMarginTradesService(). - IsIsolated(e.IsIsolatedMargin). - Symbol(symbol) - - if options.Limit > 0 { - req.Limit(int(options.Limit)) - } else { - req.Limit(1000) - } - - // BINANCE uses inclusive last trade ID - if options.LastTradeID > 0 { - req.FromID(int64(options.LastTradeID)) - } - - if options.StartTime != nil && options.EndTime != nil { - if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { - req.StartTime(options.StartTime.UnixMilli()) - req.EndTime(options.EndTime.UnixMilli()) - } else { - req.StartTime(options.StartTime.UnixMilli()) - } - } else if options.StartTime != nil { - req.StartTime(options.StartTime.UnixMilli()) - } else if options.EndTime != nil { - req.EndTime(options.EndTime.UnixMilli()) - } - - remoteTrades, err = req.Do(ctx) - if err != nil { - return nil, err - } - for _, t := range remoteTrades { - localTrade, err := toGlobalTrade(*t, e.IsMargin) - if err != nil { - log.WithError(err).Errorf("can not convert binance trade: %+v", t) - continue - } - - trades = append(trades, *localTrade) - } - - trades = types.SortTradesAscending(trades) - - return trades, nil + return e.queryMarginTrades(ctx, symbol, options) } else if e.IsFutures { - var remoteTrades []*futures.AccountTrade - req := e.futuresClient.NewListAccountTradeService(). - Symbol(symbol) - if options.Limit > 0 { - req.Limit(int(options.Limit)) - } else { - req.Limit(1000) - } - - // BINANCE uses inclusive last trade ID - if options.LastTradeID > 0 { - req.FromID(int64(options.LastTradeID)) - } - - // The parameter fromId cannot be sent with startTime or endTime. - // Mentioned in binance futures docs - if options.LastTradeID <= 0 { - if options.StartTime != nil && options.EndTime != nil { - if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { - req.StartTime(options.StartTime.UnixMilli()) - req.EndTime(options.EndTime.UnixMilli()) - } else { - req.StartTime(options.StartTime.UnixMilli()) - } - } else if options.EndTime != nil { - req.EndTime(options.EndTime.UnixMilli()) - } - } - - remoteTrades, err = req.Do(ctx) - if err != nil { - return nil, err - } - for _, t := range remoteTrades { - localTrade, err := toGlobalFuturesTrade(*t) - if err != nil { - log.WithError(err).Errorf("can not convert binance futures trade: %+v", t) - continue - } - - trades = append(trades, *localTrade) - } - - trades = types.SortTradesAscending(trades) - return trades, nil + return e.queryFuturesTrades(ctx, symbol, options) } else { - var remoteTrades []*binance.TradeV3 - req := e.client.NewListTradesService(). - Symbol(symbol) - - if options.Limit > 0 { - req.Limit(int(options.Limit)) - } else { - req.Limit(1000) - } - - // BINANCE uses inclusive last trade ID - if options.LastTradeID > 0 { - req.FromID(int64(options.LastTradeID)) - } - - if options.StartTime != nil && options.EndTime != nil { - if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { - req.StartTime(options.StartTime.UnixMilli()) - req.EndTime(options.EndTime.UnixMilli()) - } else { - req.StartTime(options.StartTime.UnixMilli()) - } - } else if options.StartTime != nil { - req.StartTime(options.StartTime.UnixMilli()) - } else if options.EndTime != nil { - req.EndTime(options.EndTime.UnixMilli()) - } - - remoteTrades, err = req.Do(ctx) - if err != nil { - return nil, err - } - for _, t := range remoteTrades { - localTrade, err := toGlobalTrade(*t, e.IsMargin) - if err != nil { - log.WithError(err).Errorf("can not convert binance trade: %+v", t) - continue - } - - trades = append(trades, *localTrade) - } - - trades = types.SortTradesAscending(trades) - return trades, nil + return e.querySpotTrades(ctx, symbol, options) } } @@ -1480,37 +1492,10 @@ func (e *Exchange) QueryDepth(ctx context.Context, symbol string) (snapshot type return snapshot, finalUpdateID, nil } -func (e *Exchange) BatchQueryKLines(ctx context.Context, symbol string, interval types.Interval, startTime, endTime time.Time) ([]types.KLine, error) { - var allKLines []types.KLine - - for startTime.Before(endTime) { - klines, err := e.QueryKLines(ctx, symbol, interval, types.KLineQueryOptions{ - StartTime: &startTime, - Limit: 1000, - }) - - if err != nil { - return nil, err - } - - for _, kline := range klines { - if kline.EndTime.After(endTime) { - return allKLines, nil - } - - allKLines = append(allKLines, kline) - startTime = kline.EndTime.Time() - } - } - - return allKLines, nil -} - +// QueryPremiumIndex is only for futures func (e *Exchange) QueryPremiumIndex(ctx context.Context, symbol string) (*types.PremiumIndex, error) { - futuresClient := binance.NewFuturesClient(e.key, e.secret) - // when symbol is set, only one index will be returned. - indexes, err := futuresClient.NewPremiumIndexService().Symbol(symbol).Do(ctx) + indexes, err := e.futuresClient.NewPremiumIndexService().Symbol(symbol).Do(ctx) if err != nil { return nil, err } @@ -1519,8 +1504,7 @@ func (e *Exchange) QueryPremiumIndex(ctx context.Context, symbol string) (*types } func (e *Exchange) QueryFundingRateHistory(ctx context.Context, symbol string) (*types.FundingRate, error) { - futuresClient := binance.NewFuturesClient(e.key, e.secret) - rates, err := futuresClient.NewFundingRateService(). + rates, err := e.futuresClient.NewFundingRateService(). Symbol(symbol). Limit(1). Do(ctx) @@ -1546,10 +1530,8 @@ func (e *Exchange) QueryFundingRateHistory(ctx context.Context, symbol string) ( } func (e *Exchange) QueryPositionRisk(ctx context.Context, symbol string) (*types.PositionRisk, error) { - futuresClient := binance.NewFuturesClient(e.key, e.secret) - // when symbol is set, only one position risk will be returned. - risks, err := futuresClient.NewGetPositionRiskService().Symbol(symbol).Do(ctx) + risks, err := e.futuresClient.NewGetPositionRiskService().Symbol(symbol).Do(ctx) if err != nil { return nil, err } From 409ad9b75cffbac335f328d46a664d5118a62fae Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 29 May 2022 01:42:08 +0800 Subject: [PATCH 06/10] binance: adjust margin history interface --- pkg/exchange/binance/binanceapi/client.go | 12 ++- .../binance/binanceapi/client_test.go | 6 +- .../get_spot_rebate_history_request.go | 1 - pkg/exchange/binance/exchange.go | 14 ++- pkg/exchange/binance/margin_history.go | 85 +++++++++++++++++++ pkg/types/margin.go | 9 +- 6 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 pkg/exchange/binance/margin_history.go diff --git a/pkg/exchange/binance/binanceapi/client.go b/pkg/exchange/binance/binanceapi/client.go index f23973574..45c39e6f1 100644 --- a/pkg/exchange/binance/binanceapi/client.go +++ b/pkg/exchange/binance/binanceapi/client.go @@ -24,7 +24,7 @@ const RestBaseURL = "https://api.binance.com" const SandboxRestBaseURL = "https://testnet.binance.vision" const DebugRequestResponse = false -var defaultHttpClient = &http.Client{ +var DefaultHttpClient = &http.Client{ Timeout: defaultHTTPTimeout, } @@ -37,8 +37,12 @@ type RestClient struct { timeOffset int64 } -func NewClient() *RestClient { - u, err := url.Parse(RestBaseURL) +func NewClient(baseURL string) *RestClient { + if len(baseURL) == 0 { + baseURL = RestBaseURL + } + + u, err := url.Parse(baseURL) if err != nil { panic(err) } @@ -46,7 +50,7 @@ func NewClient() *RestClient { client := &RestClient{ BaseAPIClient: requestgen.BaseAPIClient{ BaseURL: u, - HttpClient: defaultHttpClient, + HttpClient: DefaultHttpClient, }, } diff --git a/pkg/exchange/binance/binanceapi/client_test.go b/pkg/exchange/binance/binanceapi/client_test.go index 51c6d07df..84a6542f8 100644 --- a/pkg/exchange/binance/binanceapi/client_test.go +++ b/pkg/exchange/binance/binanceapi/client_test.go @@ -35,7 +35,7 @@ func getTestClientOrSkip(t *testing.T) *RestClient { return nil } - client := NewClient() + client := NewClient("") client.Auth(key, secret) return client } @@ -124,7 +124,7 @@ func TestClient_privateCall(t *testing.T) { t.SkipNow() } - client := NewClient() + client := NewClient("") client.Auth(key, secret) ctx := context.Background() @@ -154,7 +154,7 @@ func TestClient_privateCall(t *testing.T) { } func TestClient_setTimeOffsetFromServer(t *testing.T) { - client := NewClient() + client := NewClient("") err := client.SetTimeOffsetFromServer(context.Background()) assert.NoError(t, err) } diff --git a/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go b/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go index eb3db5ab8..7fff74ffb 100644 --- a/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go +++ b/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go @@ -24,7 +24,6 @@ type SpotRebate struct { UpdateTime types.MillisecondTimestamp `json:"updateTime"` } - // GetSpotRebateHistoryRequest // The max interval between startTime and endTime is 30 days. // If startTime and endTime are not sent, the recent 7 days' data will be returned. diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 87fc67280..5a1db26fa 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -3,7 +3,6 @@ package binance import ( "context" "fmt" - "net/http" "os" "strconv" "strings" @@ -22,6 +21,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" @@ -77,17 +77,21 @@ type Exchange struct { // futuresClient is used for usdt-m futures futuresClient *futures.Client // USDT-M Futures // deliveryClient *delivery.Client // Coin-M Futures + + // client2 is a newer version of the binance api client implemented by ourselves. + client2 *binanceapi.RestClient } var timeSetter sync.Once func New(key, secret string) *Exchange { var client = binance.NewClient(key, secret) - client.HTTPClient = &http.Client{Timeout: 15 * time.Second} + client.HTTPClient = binanceapi.DefaultHttpClient client.Debug = viper.GetBool("debug-binance-client") var futuresClient = binance.NewFuturesClient(key, secret) - futuresClient.HTTPClient = &http.Client{Timeout: 15 * time.Second} + futuresClient.HTTPClient = binanceapi.DefaultHttpClient + futuresClient.Debug = viper.GetBool("debug-binance-futures-client") if isBinanceUs() { client.BaseURL = BinanceUSBaseURL @@ -98,6 +102,8 @@ func New(key, secret string) *Exchange { futuresClient.BaseURL = FutureTestBaseURL } + client2 := binanceapi.NewClient(client.BaseURL) + var err error if len(key) > 0 && len(secret) > 0 { timeSetter.Do(func() { @@ -118,7 +124,7 @@ func New(key, secret string) *Exchange { secret: secret, client: client, futuresClient: futuresClient, - // deliveryClient: deliveryClient, + client2: client2, } } diff --git a/pkg/exchange/binance/margin_history.go b/pkg/exchange/binance/margin_history.go new file mode 100644 index 000000000..6eb679369 --- /dev/null +++ b/pkg/exchange/binance/margin_history.go @@ -0,0 +1,85 @@ +package binance + +import ( + "context" + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +func (e *Exchange) QueryLoanHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginLoanRecord, error) { + req := e.client2.NewGetMarginLoanHistoryRequest() + req.Asset(asset) + + if startTime != nil { + req.StartTime(*startTime) + } + if endTime != nil { + req.EndTime(*endTime) + } + + if e.MarginSettings.IsIsolatedMargin { + req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) + } + + loans, err := req.Do(ctx) + _ = loans + return nil, err +} + +func (e *Exchange) QueryRepayHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginRepayRecord, error) { + req := e.client2.NewGetMarginRepayHistoryRequest() + req.Asset(asset) + + if startTime != nil { + req.StartTime(*startTime) + } + if endTime != nil { + req.EndTime(*endTime) + } + + if e.MarginSettings.IsIsolatedMargin { + req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) + } + + _, err := req.Do(ctx) + return nil, err +} + +func (e *Exchange) QueryLiquidationHistory(ctx context.Context, startTime, endTime *time.Time) ([]types.MarginLiquidationRecord, error) { + req := e.client2.NewGetMarginLiquidationHistoryRequest() + + if startTime != nil { + req.StartTime(*startTime) + } + if endTime != nil { + req.EndTime(*endTime) + } + + if e.MarginSettings.IsIsolatedMargin { + req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) + } + + _, err := req.Do(ctx) + return nil, err +} + +func (e *Exchange) QueryInterestHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginInterest, error) { + req := e.client2.NewGetMarginInterestHistoryRequest() + req.Asset(asset) + + if startTime != nil { + req.StartTime(*startTime) + } + + if endTime != nil { + req.EndTime(*endTime) + } + + if e.MarginSettings.IsIsolatedMargin { + req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) + } + + _, err := req.Do(ctx) + return nil, err +} diff --git a/pkg/types/margin.go b/pkg/types/margin.go index dde770514..3739ad2c8 100644 --- a/pkg/types/margin.go +++ b/pkg/types/margin.go @@ -2,6 +2,7 @@ package types import ( "context" + "time" "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -98,10 +99,10 @@ type MarginLiquidationRecord struct { // MarginHistory provides the service of querying loan history and repay history type MarginHistory interface { - QueryLoanHistory() ([]MarginLoanRecord, error) - QueryRepayHistory() ([]MarginRepayRecord, error) - QueryLiquidationHistory() ([]MarginLiquidationRecord, error) - QueryInterestHistory() ([]MarginInterest, error) + QueryLoanHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]MarginLoanRecord, error) + QueryRepayHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]MarginRepayRecord, error) + QueryLiquidationHistory(ctx context.Context, startTime, endTime *time.Time) ([]MarginLiquidationRecord, error) + QueryInterestHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]MarginInterest, error) } type MarginSettings struct { From 70f0dccb9ffeb5a128d8133d6b6c4a9b2edde4f2 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 29 May 2022 11:52:25 +0800 Subject: [PATCH 07/10] binance: convert loans and repays to global types --- config/binance-margin.yaml | 12 ++ pkg/cmd/margin.go | 103 ++++++++++++++++++ pkg/cmd/root.go | 94 +++++++++------- .../get_margin_repay_history_request.go | 19 ++-- pkg/exchange/binance/exchange.go | 2 + pkg/exchange/binance/margin_history.go | 75 ++++++++++++- 6 files changed, 250 insertions(+), 55 deletions(-) create mode 100644 pkg/cmd/margin.go diff --git a/config/binance-margin.yaml b/config/binance-margin.yaml index 169f2e58f..61cb4aea5 100644 --- a/config/binance-margin.yaml +++ b/config/binance-margin.yaml @@ -1,11 +1,23 @@ --- sessions: + # cross margin + binance_margin: + exchange: binance + margin: true + + # isolated margin binance_margin_linkusdt: exchange: binance margin: true isolatedMargin: true isolatedMarginSymbol: LINKUSDT + binance_margin_dotusdt: + exchange: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: DOTUSDT + exchangeStrategies: - on: binance_margin_linkusdt diff --git a/pkg/cmd/margin.go b/pkg/cmd/margin.go new file mode 100644 index 000000000..74d4b8735 --- /dev/null +++ b/pkg/cmd/margin.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "time" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +var selectedSession *bbgo.ExchangeSession + +func init() { + marginLoansCmd.Flags().String("asset", "", "asset") + marginLoansCmd.Flags().String("session", "", "exchange session name") + marginCmd.AddCommand(marginLoansCmd) + + RootCmd.AddCommand(marginCmd) +} + +// go run ./cmd/bbgo margin --session=binance +var marginCmd = &cobra.Command{ + Use: "margin", + Short: "margin related history", + SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := cobraLoadDotenv(cmd, args); err != nil { + return err + } + + if err := cobraLoadConfig(cmd, args); err != nil { + return err + } + + // ctx := context.Background() + environ := bbgo.NewEnvironment() + + if userConfig == nil { + return errors.New("user config is not loaded") + } + + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { + return err + } + + sessionName, err := cmd.Flags().GetString("session") + if err != nil { + return err + } + + session, ok := environ.Session(sessionName) + if !ok { + return fmt.Errorf("session %s not found", sessionName) + } + + selectedSession = session + return nil + }, +} + +// go run ./cmd/bbgo margin loans --session=binance +var marginLoansCmd = &cobra.Command{ + Use: "loans --session=SESSION_NAME --asset=ASSET", + Short: "query loans history", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + asset, err := cmd.Flags().GetString("asset") + if err != nil { + return fmt.Errorf("can't get the symbol from flags: %w", err) + } + + if selectedSession == nil { + return errors.New("session is not set") + } + + marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistory) + if !ok { + return fmt.Errorf("exchange %s does not support MarginHistory service", selectedSession.ExchangeName) + } + + now := time.Now() + startTime := now.AddDate(0, -5, 0) + endTime := now + loans, err := marginHistoryService.QueryLoanHistory(ctx, asset, &startTime, &endTime) + if err != nil { + return err + } + + log.Infof("%d loans", len(loans)) + for _, loan := range loans { + log.Infof("LOAN %+v", loan) + } + + return nil + }, +} diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 687f9a4bf..694440e71 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -32,24 +32,10 @@ var RootCmd = &cobra.Command{ SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - disableDotEnv, err := cmd.Flags().GetBool("no-dotenv") - if err != nil { + if err := cobraLoadDotenv(cmd, args) ; err != nil { return err } - if !disableDotEnv { - dotenvFile, err := cmd.Flags().GetString("dotenv") - if err != nil { - return err - } - - if _, err := os.Stat(dotenvFile); err == nil { - if err := godotenv.Load(dotenvFile); err != nil { - return errors.Wrap(err, "error loading dotenv file") - } - } - } - if viper.GetBool("debug") { log.Infof("debug mode is enabled") log.SetLevel(log.DebugLevel) @@ -67,39 +53,63 @@ var RootCmd = &cobra.Command{ }() } - configFile, err := cmd.Flags().GetString("config") - if err != nil { - return errors.Wrapf(err, "failed to get the config flag") - } - - // load config file nicely - if len(configFile) > 0 { - // if config file exists, use the config loaded from the config file. - // otherwise, use a empty config object - if _, err := os.Stat(configFile); err == nil { - // load successfully - userConfig, err = bbgo.Load(configFile, false) - if err != nil { - return errors.Wrapf(err, "can not load config file: %s", configFile) - } - - } else if os.IsNotExist(err) { - // config file doesn't exist, we should use the empty config - userConfig = &bbgo.Config{} - } else { - // other error - return errors.Wrapf(err, "config file load error: %s", configFile) - } - } - - return nil + return cobraLoadConfig(cmd, args) }, - RunE: func(cmd *cobra.Command, args []string) error { return nil }, } +func cobraLoadDotenv(cmd *cobra.Command, args []string) error { + disableDotEnv, err := cmd.Flags().GetBool("no-dotenv") + if err != nil { + return err + } + + if !disableDotEnv { + dotenvFile, err := cmd.Flags().GetString("dotenv") + if err != nil { + return err + } + + if _, err := os.Stat(dotenvFile); err == nil { + if err := godotenv.Load(dotenvFile); err != nil { + return errors.Wrap(err, "error loading dotenv file") + } + } + } + return nil +} + +func cobraLoadConfig(cmd *cobra.Command, args []string) error { + configFile, err := cmd.Flags().GetString("config") + if err != nil { + return errors.Wrapf(err, "failed to get the config flag") + } + + // load config file nicely + if len(configFile) > 0 { + // if config file exists, use the config loaded from the config file. + // otherwise, use an empty config object + if _, err := os.Stat(configFile); err == nil { + // load successfully + userConfig, err = bbgo.Load(configFile, false) + if err != nil { + return errors.Wrapf(err, "can not load config file: %s", configFile) + } + + } else if os.IsNotExist(err) { + // config file doesn't exist, we should use the empty config + userConfig = &bbgo.Config{} + } else { + // other error + return errors.Wrapf(err, "config file load error: %s", configFile) + } + } + + return nil +} + func init() { RootCmd.PersistentFlags().Bool("debug", false, "debug mode") RootCmd.PersistentFlags().Bool("metrics", false, "enable prometheus metrics") diff --git a/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go index e6c481cfe..6d9a13448 100644 --- a/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go +++ b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go @@ -4,6 +4,9 @@ import ( "time" "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) // RepayStatus one of PENDING (pending execution), CONFIRMED (successfully loaned), FAILED (execution failed, nothing happened to your account); @@ -16,14 +19,14 @@ const ( ) type MarginRepayRecord struct { - IsolatedSymbol string `json:"isolatedSymbol"` - Amount string `json:"amount"` - Asset string `json:"asset"` - Interest string `json:"interest"` - Principal string `json:"principal"` - Status string `json:"status"` - Timestamp int64 `json:"timestamp"` - TxId int64 `json:"txId"` + IsolatedSymbol string `json:"isolatedSymbol"` + Amount fixedpoint.Value `json:"amount"` + Asset string `json:"asset"` + Interest fixedpoint.Value `json:"interest"` + Principal fixedpoint.Value `json:"principal"` + Status string `json:"status"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` + TxId uint64 `json:"txId"` } //go:generate requestgen -method GET -url "/sapi/v1/margin/repay" -type GetMarginRepayHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginRepayRecord diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 5a1db26fa..bd7632ddd 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -106,6 +106,8 @@ func New(key, secret string) *Exchange { var err error if len(key) > 0 && len(secret) > 0 { + client2.Auth(key, secret) + timeSetter.Do(func() { _, err = client.NewSetServerTimeService().Do(context.Background()) if err != nil { diff --git a/pkg/exchange/binance/margin_history.go b/pkg/exchange/binance/margin_history.go index 6eb679369..544bfda20 100644 --- a/pkg/exchange/binance/margin_history.go +++ b/pkg/exchange/binance/margin_history.go @@ -4,16 +4,32 @@ import ( "context" "time" + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" "github.com/c9s/bbgo/pkg/types" ) func (e *Exchange) QueryLoanHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginLoanRecord, error) { req := e.client2.NewGetMarginLoanHistoryRequest() req.Asset(asset) + req.Size(100) if startTime != nil { req.StartTime(*startTime) + + // 6 months + if time.Since(*startTime) > time.Hour*24*30*6 { + req.Archived(true) + } } + + if startTime != nil && endTime != nil { + duration := endTime.Sub(*startTime) + if duration > time.Hour*24*30 { + t := startTime.Add(time.Hour * 24 * 30) + endTime = &t + } + } + if endTime != nil { req.EndTime(*endTime) } @@ -22,18 +38,51 @@ func (e *Exchange) QueryLoanHistory(ctx context.Context, asset string, startTime req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) } - loans, err := req.Do(ctx) - _ = loans - return nil, err + records, err := req.Do(ctx) + if err != nil { + return nil, err + } + + var loans []types.MarginLoanRecord + for _, record := range records { + loans = append(loans, toGlobalLoan(record)) + } + + return loans, err +} + +func toGlobalLoan(record binanceapi.MarginLoanRecord) types.MarginLoanRecord { + return types.MarginLoanRecord{ + TransactionID: uint64(record.TxId), + Asset: record.Asset, + Principle: record.Principal, + Time: types.Time(record.Timestamp), + IsolatedSymbol: record.IsolatedSymbol, + } } func (e *Exchange) QueryRepayHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginRepayRecord, error) { req := e.client2.NewGetMarginRepayHistoryRequest() req.Asset(asset) + req.Size(100) if startTime != nil { req.StartTime(*startTime) + + // 6 months + if time.Since(*startTime) > time.Hour*24*30*6 { + req.Archived(true) + } } + + if startTime != nil && endTime != nil { + duration := endTime.Sub(*startTime) + if duration > time.Hour*24*30 { + t := startTime.Add(time.Hour * 24 * 30) + endTime = &t + } + } + if endTime != nil { req.EndTime(*endTime) } @@ -42,8 +91,24 @@ func (e *Exchange) QueryRepayHistory(ctx context.Context, asset string, startTim req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) } - _, err := req.Do(ctx) - return nil, err + records, err := req.Do(ctx) + + var repays []types.MarginRepayRecord + for _, record := range records { + repays = append(repays, toGlobalRepay(record)) + } + + return repays, err +} + +func toGlobalRepay(record binanceapi.MarginRepayRecord) types.MarginRepayRecord { + return types.MarginRepayRecord{ + TransactionID: record.TxId, + Asset: record.Asset, + Principle: record.Principal, + Time: types.Time(record.Timestamp), + IsolatedSymbol: record.IsolatedSymbol, + } } func (e *Exchange) QueryLiquidationHistory(ctx context.Context, startTime, endTime *time.Time) ([]types.MarginLiquidationRecord, error) { From 4a4699a4bc6c535da081753c6953d586a0abc279 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 29 May 2022 11:53:36 +0800 Subject: [PATCH 08/10] cmd: add margin repays cmd --- pkg/cmd/margin.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/pkg/cmd/margin.go b/pkg/cmd/margin.go index 74d4b8735..561b1d8ce 100644 --- a/pkg/cmd/margin.go +++ b/pkg/cmd/margin.go @@ -20,6 +20,10 @@ func init() { marginLoansCmd.Flags().String("session", "", "exchange session name") marginCmd.AddCommand(marginLoansCmd) + marginRepaysCmd.Flags().String("asset", "", "asset") + marginRepaysCmd.Flags().String("session", "", "exchange session name") + marginCmd.AddCommand(marginRepaysCmd) + RootCmd.AddCommand(marginCmd) } @@ -101,3 +105,42 @@ var marginLoansCmd = &cobra.Command{ return nil }, } + +// go run ./cmd/bbgo margin loans --session=binance +var marginRepaysCmd = &cobra.Command{ + Use: "repays --session=SESSION_NAME --asset=ASSET", + Short: "query repay history", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + asset, err := cmd.Flags().GetString("asset") + if err != nil { + return fmt.Errorf("can't get the symbol from flags: %w", err) + } + + if selectedSession == nil { + return errors.New("session is not set") + } + + marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistory) + if !ok { + return fmt.Errorf("exchange %s does not support MarginHistory service", selectedSession.ExchangeName) + } + + now := time.Now() + startTime := now.AddDate(0, -5, 0) + endTime := now + loans, err := marginHistoryService.QueryLoanHistory(ctx, asset, &startTime, &endTime) + if err != nil { + return err + } + + log.Infof("%d loans", len(loans)) + for _, loan := range loans { + log.Infof("LOAN %+v", loan) + } + + return nil + }, +} From 11075b0d1a96e145355c73ff6ba416ac7152e225 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 29 May 2022 12:01:20 +0800 Subject: [PATCH 09/10] cmd: add marginInterestsCmd --- pkg/cmd/margin.go | 53 +++++++++++++++++++++++--- pkg/exchange/binance/margin_history.go | 39 ++++++++++++++++++- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/margin.go b/pkg/cmd/margin.go index 561b1d8ce..1ad608f90 100644 --- a/pkg/cmd/margin.go +++ b/pkg/cmd/margin.go @@ -24,6 +24,10 @@ func init() { marginRepaysCmd.Flags().String("session", "", "exchange session name") marginCmd.AddCommand(marginRepaysCmd) + marginInterestsCmd.Flags().String("asset", "", "asset") + marginInterestsCmd.Flags().String("session", "", "exchange session name") + marginCmd.AddCommand(marginInterestsCmd) + RootCmd.AddCommand(marginCmd) } @@ -77,7 +81,7 @@ var marginLoansCmd = &cobra.Command{ asset, err := cmd.Flags().GetString("asset") if err != nil { - return fmt.Errorf("can't get the symbol from flags: %w", err) + return err } if selectedSession == nil { @@ -114,6 +118,45 @@ var marginRepaysCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() + asset, err := cmd.Flags().GetString("asset") + if err != nil { + return err + } + + if selectedSession == nil { + return errors.New("session is not set") + } + + marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistory) + if !ok { + return fmt.Errorf("exchange %s does not support MarginHistory service", selectedSession.ExchangeName) + } + + now := time.Now() + startTime := now.AddDate(0, -5, 0) + endTime := now + repays, err := marginHistoryService.QueryLoanHistory(ctx, asset, &startTime, &endTime) + if err != nil { + return err + } + + log.Infof("%d repays", len(repays)) + for _, repay := range repays { + log.Infof("REPAY %+v", repay) + } + + return nil + }, +} + +// go run ./cmd/bbgo margin interests --session=binance +var marginInterestsCmd = &cobra.Command{ + Use: "interests --session=SESSION_NAME --asset=ASSET", + Short: "query interests history", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + asset, err := cmd.Flags().GetString("asset") if err != nil { return fmt.Errorf("can't get the symbol from flags: %w", err) @@ -131,14 +174,14 @@ var marginRepaysCmd = &cobra.Command{ now := time.Now() startTime := now.AddDate(0, -5, 0) endTime := now - loans, err := marginHistoryService.QueryLoanHistory(ctx, asset, &startTime, &endTime) + interests, err := marginHistoryService.QueryInterestHistory(ctx, asset, &startTime, &endTime) if err != nil { return err } - log.Infof("%d loans", len(loans)) - for _, loan := range loans { - log.Infof("LOAN %+v", loan) + log.Infof("%d interests", len(interests)) + for _, interest := range interests { + log.Infof("INTEREST %+v", interest) } return nil diff --git a/pkg/exchange/binance/margin_history.go b/pkg/exchange/binance/margin_history.go index 544bfda20..5e8a0321f 100644 --- a/pkg/exchange/binance/margin_history.go +++ b/pkg/exchange/binance/margin_history.go @@ -132,9 +132,23 @@ func (e *Exchange) QueryLiquidationHistory(ctx context.Context, startTime, endTi func (e *Exchange) QueryInterestHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginInterest, error) { req := e.client2.NewGetMarginInterestHistoryRequest() req.Asset(asset) + req.Size(100) if startTime != nil { req.StartTime(*startTime) + + // 6 months + if time.Since(*startTime) > time.Hour*24*30*6 { + req.Archived(true) + } + } + + if startTime != nil && endTime != nil { + duration := endTime.Sub(*startTime) + if duration > time.Hour*24*30 { + t := startTime.Add(time.Hour * 24 * 30) + endTime = &t + } } if endTime != nil { @@ -145,6 +159,27 @@ func (e *Exchange) QueryInterestHistory(ctx context.Context, asset string, start req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) } - _, err := req.Do(ctx) - return nil, err + records, err := req.Do(ctx) + if err != nil { + return nil, err + } + + var interests []types.MarginInterest + for _, record := range records { + interests = append(interests, toGlobalInterest(record)) + } + + return interests, err +} + +func toGlobalInterest(record binanceapi.MarginInterest) types.MarginInterest { + return types.MarginInterest{ + Asset: record.Asset, + Principle: record.Principal, + Interest: record.Interest, + InterestRate: record.InterestRate, + IsolatedSymbol: record.IsolatedSymbol, + Time: types.Time(record.InterestAccuredTime), + } + } From 61a53947ee8a9e451f16155da3685b09e4db474a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 29 May 2022 12:03:13 +0800 Subject: [PATCH 10/10] binance: re-organize convert functions --- pkg/exchange/binance/convert.go | 344 ------------------------ pkg/exchange/binance/convert_futures.go | 279 +++++++++++++++++++ pkg/exchange/binance/convert_margin.go | 119 ++++++++ pkg/exchange/binance/margin_history.go | 32 --- 4 files changed, 398 insertions(+), 376 deletions(-) create mode 100644 pkg/exchange/binance/convert_futures.go create mode 100644 pkg/exchange/binance/convert_margin.go diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go index 1805db858..2a885667e 100644 --- a/pkg/exchange/binance/convert.go +++ b/pkg/exchange/binance/convert.go @@ -84,46 +84,6 @@ func toGlobalFuturesMarket(symbol futures.Symbol) types.Market { return market } -func toGlobalIsolatedUserAsset(userAsset binance.IsolatedUserAsset) types.IsolatedUserAsset { - return types.IsolatedUserAsset{ - Asset: userAsset.Asset, - Borrowed: fixedpoint.MustNewFromString(userAsset.Borrowed), - Free: fixedpoint.MustNewFromString(userAsset.Free), - Interest: fixedpoint.MustNewFromString(userAsset.Interest), - Locked: fixedpoint.MustNewFromString(userAsset.Locked), - NetAsset: fixedpoint.MustNewFromString(userAsset.NetAsset), - NetAssetOfBtc: fixedpoint.MustNewFromString(userAsset.NetAssetOfBtc), - BorrowEnabled: userAsset.BorrowEnabled, - RepayEnabled: userAsset.RepayEnabled, - TotalAsset: fixedpoint.MustNewFromString(userAsset.TotalAsset), - } -} - -func toGlobalIsolatedMarginAsset(asset binance.IsolatedMarginAsset) types.IsolatedMarginAsset { - return types.IsolatedMarginAsset{ - Symbol: asset.Symbol, - QuoteAsset: toGlobalIsolatedUserAsset(asset.QuoteAsset), - BaseAsset: toGlobalIsolatedUserAsset(asset.BaseAsset), - IsolatedCreated: asset.IsolatedCreated, - MarginLevel: fixedpoint.MustNewFromString(asset.MarginLevel), - MarginLevelStatus: asset.MarginLevelStatus, - MarginRatio: fixedpoint.MustNewFromString(asset.MarginRatio), - IndexPrice: fixedpoint.MustNewFromString(asset.IndexPrice), - LiquidatePrice: fixedpoint.MustNewFromString(asset.LiquidatePrice), - LiquidateRate: fixedpoint.MustNewFromString(asset.LiquidateRate), - TradeEnabled: false, - } -} - -func toGlobalIsolatedMarginAssets(assets []binance.IsolatedMarginAsset) (retAssets types.IsolatedMarginAssetMap) { - retMarginAssets := make(types.IsolatedMarginAssetMap) - for _, marginAsset := range assets { - retMarginAssets[marginAsset.Symbol] = toGlobalIsolatedMarginAsset(marginAsset) - } - - return retMarginAssets -} - //func toGlobalIsolatedMarginAccount(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccount { // return &types.IsolatedMarginAccount{ // TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), @@ -133,105 +93,6 @@ func toGlobalIsolatedMarginAssets(assets []binance.IsolatedMarginAsset) (retAsse // } //} -func toGlobalMarginUserAssets(assets []binance.UserAsset) types.MarginAssetMap { - retMarginAssets := make(types.MarginAssetMap) - for _, marginAsset := range assets { - retMarginAssets[marginAsset.Asset] = types.MarginUserAsset{ - Asset: marginAsset.Asset, - Borrowed: fixedpoint.MustNewFromString(marginAsset.Borrowed), - Free: fixedpoint.MustNewFromString(marginAsset.Free), - Interest: fixedpoint.MustNewFromString(marginAsset.Interest), - Locked: fixedpoint.MustNewFromString(marginAsset.Locked), - NetAsset: fixedpoint.MustNewFromString(marginAsset.NetAsset), - } - } - - return retMarginAssets -} - -func toGlobalMarginAccountInfo(account *binance.MarginAccount) *types.MarginAccountInfo { - return &types.MarginAccountInfo{ - BorrowEnabled: account.BorrowEnabled, - MarginLevel: fixedpoint.MustNewFromString(account.MarginLevel), - TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalAssetOfBTC), - TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC), - TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), - TradeEnabled: account.TradeEnabled, - TransferEnabled: account.TransferEnabled, - Assets: toGlobalMarginUserAssets(account.UserAssets), - } -} - -func toGlobalIsolatedMarginAccountInfo(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccountInfo { - return &types.IsolatedMarginAccountInfo{ - TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalAssetOfBTC), - TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC), - TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), - Assets: toGlobalIsolatedMarginAssets(account.Assets), - } -} - -func toGlobalFuturesAccountInfo(account *futures.Account) *types.FuturesAccountInfo { - return &types.FuturesAccountInfo{ - Assets: toGlobalFuturesUserAssets(account.Assets), - Positions: toGlobalFuturesPositions(account.Positions), - TotalInitialMargin: fixedpoint.MustNewFromString(account.TotalInitialMargin), - TotalMaintMargin: fixedpoint.MustNewFromString(account.TotalMaintMargin), - TotalMarginBalance: fixedpoint.MustNewFromString(account.TotalMarginBalance), - TotalOpenOrderInitialMargin: fixedpoint.MustNewFromString(account.TotalOpenOrderInitialMargin), - TotalPositionInitialMargin: fixedpoint.MustNewFromString(account.TotalPositionInitialMargin), - TotalUnrealizedProfit: fixedpoint.MustNewFromString(account.TotalUnrealizedProfit), - TotalWalletBalance: fixedpoint.MustNewFromString(account.TotalWalletBalance), - UpdateTime: account.UpdateTime, - } -} - -func toGlobalFuturesBalance(balances []*futures.Balance) types.BalanceMap { - retBalances := make(types.BalanceMap) - for _, balance := range balances { - retBalances[balance.Asset] = types.Balance{ - Currency: balance.Asset, - Available: fixedpoint.MustNewFromString(balance.AvailableBalance), - } - } - return retBalances -} - -func toGlobalFuturesPositions(futuresPositions []*futures.AccountPosition) types.FuturesPositionMap { - retFuturesPositions := make(types.FuturesPositionMap) - for _, futuresPosition := range futuresPositions { - retFuturesPositions[futuresPosition.Symbol] = types.FuturesPosition{ // TODO: types.FuturesPosition - Isolated: futuresPosition.Isolated, - PositionRisk: &types.PositionRisk{ - Leverage: fixedpoint.MustNewFromString(futuresPosition.Leverage), - }, - Symbol: futuresPosition.Symbol, - UpdateTime: futuresPosition.UpdateTime, - } - } - - return retFuturesPositions -} - -func toGlobalFuturesUserAssets(assets []*futures.AccountAsset) (retAssets types.FuturesAssetMap) { - retFuturesAssets := make(types.FuturesAssetMap) - for _, futuresAsset := range assets { - retFuturesAssets[futuresAsset.Asset] = types.FuturesUserAsset{ - Asset: futuresAsset.Asset, - InitialMargin: fixedpoint.MustNewFromString(futuresAsset.InitialMargin), - MaintMargin: fixedpoint.MustNewFromString(futuresAsset.MaintMargin), - MarginBalance: fixedpoint.MustNewFromString(futuresAsset.MarginBalance), - MaxWithdrawAmount: fixedpoint.MustNewFromString(futuresAsset.MaxWithdrawAmount), - OpenOrderInitialMargin: fixedpoint.MustNewFromString(futuresAsset.OpenOrderInitialMargin), - PositionInitialMargin: fixedpoint.MustNewFromString(futuresAsset.PositionInitialMargin), - UnrealizedProfit: fixedpoint.MustNewFromString(futuresAsset.UnrealizedProfit), - WalletBalance: fixedpoint.MustNewFromString(futuresAsset.WalletBalance), - } - } - - return retFuturesAssets -} - func toGlobalTicker(stats *binance.PriceChangeStats) (*types.Ticker, error) { return &types.Ticker{ Volume: fixedpoint.MustNewFromString(stats.Volume), @@ -267,28 +128,6 @@ func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) { return "", fmt.Errorf("can not convert to local order, order type %s not supported", orderType) } -func toLocalFuturesOrderType(orderType types.OrderType) (futures.OrderType, error) { - switch orderType { - - // case types.OrderTypeLimitMaker: - // return futures.OrderTypeLimitMaker, nil //TODO - - case types.OrderTypeLimit, types.OrderTypeLimitMaker: - return futures.OrderTypeLimit, nil - - // case types.OrderTypeStopLimit: - // return futures.OrderTypeStopLossLimit, nil //TODO - - // case types.OrderTypeStopMarket: - // return futures.OrderTypeStopLoss, nil //TODO - - case types.OrderTypeMarket: - return futures.OrderTypeMarket, nil - } - - return "", fmt.Errorf("can not convert to local order, order type %s not supported", orderType) -} - func toGlobalOrders(binanceOrders []*binance.Order) (orders []types.Order, err error) { for _, binanceOrder := range binanceOrders { order, err := toGlobalOrder(binanceOrder, false) @@ -302,19 +141,6 @@ func toGlobalOrders(binanceOrders []*binance.Order) (orders []types.Order, err e return orders, err } -func toGlobalFuturesOrders(futuresOrders []*futures.Order) (orders []types.Order, err error) { - for _, futuresOrder := range futuresOrders { - order, err := toGlobalFuturesOrder(futuresOrder, false) - if err != nil { - return orders, err - } - - orders = append(orders, *order) - } - - return orders, err -} - func toGlobalOrder(binanceOrder *binance.Order, isMargin bool) (*types.Order, error) { return &types.Order{ SubmitOrder: types.SubmitOrder{ @@ -338,29 +164,6 @@ func toGlobalOrder(binanceOrder *binance.Order, isMargin bool) (*types.Order, er }, nil } -func toGlobalFuturesOrder(futuresOrder *futures.Order, isMargin bool) (*types.Order, error) { - return &types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: futuresOrder.ClientOrderID, - Symbol: futuresOrder.Symbol, - Side: toGlobalFuturesSideType(futuresOrder.Side), - Type: toGlobalFuturesOrderType(futuresOrder.Type), - ReduceOnly: futuresOrder.ReduceOnly, - ClosePosition: futuresOrder.ClosePosition, - Quantity: fixedpoint.MustNewFromString(futuresOrder.OrigQuantity), - Price: fixedpoint.MustNewFromString(futuresOrder.Price), - TimeInForce: types.TimeInForce(futuresOrder.TimeInForce), - }, - Exchange: types.ExchangeBinance, - OrderID: uint64(futuresOrder.OrderID), - Status: toGlobalFuturesOrderStatus(futuresOrder.Status), - ExecutedQuantity: fixedpoint.MustNewFromString(futuresOrder.ExecutedQuantity), - CreationTime: types.Time(millisecondTime(futuresOrder.Time)), - UpdateTime: types.Time(millisecondTime(futuresOrder.UpdateTime)), - IsMargin: isMargin, - }, nil -} - func millisecondTime(t int64) time.Time { return time.Unix(0, t*int64(time.Millisecond)) } @@ -418,58 +221,6 @@ func toGlobalTrade(t binance.TradeV3, isMargin bool) (*types.Trade, error) { }, nil } -func toGlobalFuturesTrade(t futures.AccountTrade) (*types.Trade, error) { - // skip trade ID that is the same. however this should not happen - var side types.SideType - if t.Buyer { - side = types.SideTypeBuy - } else { - side = types.SideTypeSell - } - - price, err := fixedpoint.NewFromString(t.Price) - if err != nil { - return nil, errors.Wrapf(err, "price parse error, price: %+v", t.Price) - } - - quantity, err := fixedpoint.NewFromString(t.Quantity) - if err != nil { - return nil, errors.Wrapf(err, "quantity parse error, quantity: %+v", t.Quantity) - } - - var quoteQuantity fixedpoint.Value - if len(t.QuoteQuantity) > 0 { - quoteQuantity, err = fixedpoint.NewFromString(t.QuoteQuantity) - if err != nil { - return nil, errors.Wrapf(err, "quote quantity parse error, quoteQuantity: %+v", t.QuoteQuantity) - } - } else { - quoteQuantity = price.Mul(quantity) - } - - fee, err := fixedpoint.NewFromString(t.Commission) - if err != nil { - return nil, errors.Wrapf(err, "commission parse error, commission: %+v", t.Commission) - } - - return &types.Trade{ - ID: uint64(t.ID), - OrderID: uint64(t.OrderID), - Price: price, - Symbol: t.Symbol, - Exchange: "binance", - Quantity: quantity, - QuoteQuantity: quoteQuantity, - Side: side, - IsBuyer: t.Buyer, - IsMaker: t.Maker, - Fee: fee, - FeeCurrency: t.CommissionAsset, - Time: types.Time(millisecondTime(t.Time)), - IsFutures: true, - }, nil -} - func toGlobalSideType(side binance.SideType) types.SideType { switch side { case binance.SideTypeBuy: @@ -484,20 +235,6 @@ func toGlobalSideType(side binance.SideType) types.SideType { } } -func toGlobalFuturesSideType(side futures.SideType) types.SideType { - switch side { - case futures.SideTypeBuy: - return types.SideTypeBuy - - case futures.SideTypeSell: - return types.SideTypeSell - - default: - log.Errorf("can not convert futures side type, unknown side type: %q", side) - return "" - } -} - func toGlobalOrderType(orderType binance.OrderType) types.OrderType { switch orderType { @@ -520,27 +257,6 @@ func toGlobalOrderType(orderType binance.OrderType) types.OrderType { } } -func toGlobalFuturesOrderType(orderType futures.OrderType) types.OrderType { - switch orderType { - // TODO - case futures.OrderTypeLimit: // , futures.OrderTypeLimitMaker, futures.OrderTypeTakeProfitLimit: - return types.OrderTypeLimit - - case futures.OrderTypeMarket: - return types.OrderTypeMarket - // TODO - // case futures.OrderTypeStopLossLimit: - // return types.OrderTypeStopLimit - // TODO - // case futures.OrderTypeStopLoss: - // return types.OrderTypeStopMarket - - default: - log.Errorf("unsupported order type: %v", orderType) - return "" - } -} - func toGlobalOrderStatus(orderStatus binance.OrderStatusType) types.OrderStatus { switch orderStatus { case binance.OrderStatusTypeNew: @@ -562,27 +278,6 @@ func toGlobalOrderStatus(orderStatus binance.OrderStatusType) types.OrderStatus return types.OrderStatus(orderStatus) } -func toGlobalFuturesOrderStatus(orderStatus futures.OrderStatusType) types.OrderStatus { - switch orderStatus { - case futures.OrderStatusTypeNew: - return types.OrderStatusNew - - case futures.OrderStatusTypeRejected: - return types.OrderStatusRejected - - case futures.OrderStatusTypeCanceled: - return types.OrderStatusCanceled - - case futures.OrderStatusTypePartiallyFilled: - return types.OrderStatusPartiallyFilled - - case futures.OrderStatusTypeFilled: - return types.OrderStatusFilled - } - - return types.OrderStatus(orderStatus) -} - func convertSubscription(s types.Subscription) string { // binance uses lower case symbol name, // for kline, it's "@kline_" @@ -623,42 +318,3 @@ func convertSubscription(s types.Subscription) string { return fmt.Sprintf("%s@%s", strings.ToLower(s.Symbol), s.Channel) } -func convertPremiumIndex(index *futures.PremiumIndex) (*types.PremiumIndex, error) { - markPrice, err := fixedpoint.NewFromString(index.MarkPrice) - if err != nil { - return nil, err - } - - lastFundingRate, err := fixedpoint.NewFromString(index.LastFundingRate) - if err != nil { - return nil, err - } - - nextFundingTime := time.Unix(0, index.NextFundingTime*int64(time.Millisecond)) - t := time.Unix(0, index.Time*int64(time.Millisecond)) - - return &types.PremiumIndex{ - Symbol: index.Symbol, - MarkPrice: markPrice, - NextFundingTime: nextFundingTime, - LastFundingRate: lastFundingRate, - Time: t, - }, nil -} - -func convertPositionRisk(risk *futures.PositionRisk) (*types.PositionRisk, error) { - leverage, err := fixedpoint.NewFromString(risk.Leverage) - if err != nil { - return nil, err - } - - liquidationPrice, err := fixedpoint.NewFromString(risk.LiquidationPrice) - if err != nil { - return nil, err - } - - return &types.PositionRisk{ - Leverage: leverage, - LiquidationPrice: liquidationPrice, - }, nil -} diff --git a/pkg/exchange/binance/convert_futures.go b/pkg/exchange/binance/convert_futures.go new file mode 100644 index 000000000..4a26f248c --- /dev/null +++ b/pkg/exchange/binance/convert_futures.go @@ -0,0 +1,279 @@ +package binance + +import ( + "fmt" + "time" + + "github.com/adshao/go-binance/v2/futures" + "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func toGlobalFuturesAccountInfo(account *futures.Account) *types.FuturesAccountInfo { + return &types.FuturesAccountInfo{ + Assets: toGlobalFuturesUserAssets(account.Assets), + Positions: toGlobalFuturesPositions(account.Positions), + TotalInitialMargin: fixedpoint.MustNewFromString(account.TotalInitialMargin), + TotalMaintMargin: fixedpoint.MustNewFromString(account.TotalMaintMargin), + TotalMarginBalance: fixedpoint.MustNewFromString(account.TotalMarginBalance), + TotalOpenOrderInitialMargin: fixedpoint.MustNewFromString(account.TotalOpenOrderInitialMargin), + TotalPositionInitialMargin: fixedpoint.MustNewFromString(account.TotalPositionInitialMargin), + TotalUnrealizedProfit: fixedpoint.MustNewFromString(account.TotalUnrealizedProfit), + TotalWalletBalance: fixedpoint.MustNewFromString(account.TotalWalletBalance), + UpdateTime: account.UpdateTime, + } +} + +func toGlobalFuturesBalance(balances []*futures.Balance) types.BalanceMap { + retBalances := make(types.BalanceMap) + for _, balance := range balances { + retBalances[balance.Asset] = types.Balance{ + Currency: balance.Asset, + Available: fixedpoint.MustNewFromString(balance.AvailableBalance), + } + } + return retBalances +} + +func toGlobalFuturesPositions(futuresPositions []*futures.AccountPosition) types.FuturesPositionMap { + retFuturesPositions := make(types.FuturesPositionMap) + for _, futuresPosition := range futuresPositions { + retFuturesPositions[futuresPosition.Symbol] = types.FuturesPosition{ // TODO: types.FuturesPosition + Isolated: futuresPosition.Isolated, + PositionRisk: &types.PositionRisk{ + Leverage: fixedpoint.MustNewFromString(futuresPosition.Leverage), + }, + Symbol: futuresPosition.Symbol, + UpdateTime: futuresPosition.UpdateTime, + } + } + + return retFuturesPositions +} + +func toGlobalFuturesUserAssets(assets []*futures.AccountAsset) (retAssets types.FuturesAssetMap) { + retFuturesAssets := make(types.FuturesAssetMap) + for _, futuresAsset := range assets { + retFuturesAssets[futuresAsset.Asset] = types.FuturesUserAsset{ + Asset: futuresAsset.Asset, + InitialMargin: fixedpoint.MustNewFromString(futuresAsset.InitialMargin), + MaintMargin: fixedpoint.MustNewFromString(futuresAsset.MaintMargin), + MarginBalance: fixedpoint.MustNewFromString(futuresAsset.MarginBalance), + MaxWithdrawAmount: fixedpoint.MustNewFromString(futuresAsset.MaxWithdrawAmount), + OpenOrderInitialMargin: fixedpoint.MustNewFromString(futuresAsset.OpenOrderInitialMargin), + PositionInitialMargin: fixedpoint.MustNewFromString(futuresAsset.PositionInitialMargin), + UnrealizedProfit: fixedpoint.MustNewFromString(futuresAsset.UnrealizedProfit), + WalletBalance: fixedpoint.MustNewFromString(futuresAsset.WalletBalance), + } + } + + return retFuturesAssets +} + +func toLocalFuturesOrderType(orderType types.OrderType) (futures.OrderType, error) { + switch orderType { + + // case types.OrderTypeLimitMaker: + // return futures.OrderTypeLimitMaker, nil //TODO + + case types.OrderTypeLimit, types.OrderTypeLimitMaker: + return futures.OrderTypeLimit, nil + + // case types.OrderTypeStopLimit: + // return futures.OrderTypeStopLossLimit, nil //TODO + + // case types.OrderTypeStopMarket: + // return futures.OrderTypeStopLoss, nil //TODO + + case types.OrderTypeMarket: + return futures.OrderTypeMarket, nil + } + + return "", fmt.Errorf("can not convert to local order, order type %s not supported", orderType) +} + +func toGlobalFuturesOrders(futuresOrders []*futures.Order) (orders []types.Order, err error) { + for _, futuresOrder := range futuresOrders { + order, err := toGlobalFuturesOrder(futuresOrder, false) + if err != nil { + return orders, err + } + + orders = append(orders, *order) + } + + return orders, err +} + +func toGlobalFuturesOrder(futuresOrder *futures.Order, isMargin bool) (*types.Order, error) { + return &types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: futuresOrder.ClientOrderID, + Symbol: futuresOrder.Symbol, + Side: toGlobalFuturesSideType(futuresOrder.Side), + Type: toGlobalFuturesOrderType(futuresOrder.Type), + ReduceOnly: futuresOrder.ReduceOnly, + ClosePosition: futuresOrder.ClosePosition, + Quantity: fixedpoint.MustNewFromString(futuresOrder.OrigQuantity), + Price: fixedpoint.MustNewFromString(futuresOrder.Price), + TimeInForce: types.TimeInForce(futuresOrder.TimeInForce), + }, + Exchange: types.ExchangeBinance, + OrderID: uint64(futuresOrder.OrderID), + Status: toGlobalFuturesOrderStatus(futuresOrder.Status), + ExecutedQuantity: fixedpoint.MustNewFromString(futuresOrder.ExecutedQuantity), + CreationTime: types.Time(millisecondTime(futuresOrder.Time)), + UpdateTime: types.Time(millisecondTime(futuresOrder.UpdateTime)), + IsMargin: isMargin, + }, nil +} + +func toGlobalFuturesTrade(t futures.AccountTrade) (*types.Trade, error) { + // skip trade ID that is the same. however this should not happen + var side types.SideType + if t.Buyer { + side = types.SideTypeBuy + } else { + side = types.SideTypeSell + } + + price, err := fixedpoint.NewFromString(t.Price) + if err != nil { + return nil, errors.Wrapf(err, "price parse error, price: %+v", t.Price) + } + + quantity, err := fixedpoint.NewFromString(t.Quantity) + if err != nil { + return nil, errors.Wrapf(err, "quantity parse error, quantity: %+v", t.Quantity) + } + + var quoteQuantity fixedpoint.Value + if len(t.QuoteQuantity) > 0 { + quoteQuantity, err = fixedpoint.NewFromString(t.QuoteQuantity) + if err != nil { + return nil, errors.Wrapf(err, "quote quantity parse error, quoteQuantity: %+v", t.QuoteQuantity) + } + } else { + quoteQuantity = price.Mul(quantity) + } + + fee, err := fixedpoint.NewFromString(t.Commission) + if err != nil { + return nil, errors.Wrapf(err, "commission parse error, commission: %+v", t.Commission) + } + + return &types.Trade{ + ID: uint64(t.ID), + OrderID: uint64(t.OrderID), + Price: price, + Symbol: t.Symbol, + Exchange: "binance", + Quantity: quantity, + QuoteQuantity: quoteQuantity, + Side: side, + IsBuyer: t.Buyer, + IsMaker: t.Maker, + Fee: fee, + FeeCurrency: t.CommissionAsset, + Time: types.Time(millisecondTime(t.Time)), + IsFutures: true, + }, nil +} + +func toGlobalFuturesSideType(side futures.SideType) types.SideType { + switch side { + case futures.SideTypeBuy: + return types.SideTypeBuy + + case futures.SideTypeSell: + return types.SideTypeSell + + default: + log.Errorf("can not convert futures side type, unknown side type: %q", side) + return "" + } +} + +func toGlobalFuturesOrderType(orderType futures.OrderType) types.OrderType { + switch orderType { + // TODO + case futures.OrderTypeLimit: // , futures.OrderTypeLimitMaker, futures.OrderTypeTakeProfitLimit: + return types.OrderTypeLimit + + case futures.OrderTypeMarket: + return types.OrderTypeMarket + // TODO + // case futures.OrderTypeStopLossLimit: + // return types.OrderTypeStopLimit + // TODO + // case futures.OrderTypeStopLoss: + // return types.OrderTypeStopMarket + + default: + log.Errorf("unsupported order type: %v", orderType) + return "" + } +} + +func toGlobalFuturesOrderStatus(orderStatus futures.OrderStatusType) types.OrderStatus { + switch orderStatus { + case futures.OrderStatusTypeNew: + return types.OrderStatusNew + + case futures.OrderStatusTypeRejected: + return types.OrderStatusRejected + + case futures.OrderStatusTypeCanceled: + return types.OrderStatusCanceled + + case futures.OrderStatusTypePartiallyFilled: + return types.OrderStatusPartiallyFilled + + case futures.OrderStatusTypeFilled: + return types.OrderStatusFilled + } + + return types.OrderStatus(orderStatus) +} + +func convertPremiumIndex(index *futures.PremiumIndex) (*types.PremiumIndex, error) { + markPrice, err := fixedpoint.NewFromString(index.MarkPrice) + if err != nil { + return nil, err + } + + lastFundingRate, err := fixedpoint.NewFromString(index.LastFundingRate) + if err != nil { + return nil, err + } + + nextFundingTime := time.Unix(0, index.NextFundingTime*int64(time.Millisecond)) + t := time.Unix(0, index.Time*int64(time.Millisecond)) + + return &types.PremiumIndex{ + Symbol: index.Symbol, + MarkPrice: markPrice, + NextFundingTime: nextFundingTime, + LastFundingRate: lastFundingRate, + Time: t, + }, nil +} + +func convertPositionRisk(risk *futures.PositionRisk) (*types.PositionRisk, error) { + leverage, err := fixedpoint.NewFromString(risk.Leverage) + if err != nil { + return nil, err + } + + liquidationPrice, err := fixedpoint.NewFromString(risk.LiquidationPrice) + if err != nil { + return nil, err + } + + return &types.PositionRisk{ + Leverage: leverage, + LiquidationPrice: liquidationPrice, + }, nil +} diff --git a/pkg/exchange/binance/convert_margin.go b/pkg/exchange/binance/convert_margin.go new file mode 100644 index 000000000..40aa56e4e --- /dev/null +++ b/pkg/exchange/binance/convert_margin.go @@ -0,0 +1,119 @@ +package binance + +import ( + "github.com/adshao/go-binance/v2" + + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func toGlobalLoan(record binanceapi.MarginLoanRecord) types.MarginLoanRecord { + return types.MarginLoanRecord{ + TransactionID: uint64(record.TxId), + Asset: record.Asset, + Principle: record.Principal, + Time: types.Time(record.Timestamp), + IsolatedSymbol: record.IsolatedSymbol, + } +} + +func toGlobalRepay(record binanceapi.MarginRepayRecord) types.MarginRepayRecord { + return types.MarginRepayRecord{ + TransactionID: record.TxId, + Asset: record.Asset, + Principle: record.Principal, + Time: types.Time(record.Timestamp), + IsolatedSymbol: record.IsolatedSymbol, + } +} + +func toGlobalInterest(record binanceapi.MarginInterest) types.MarginInterest { + return types.MarginInterest{ + Asset: record.Asset, + Principle: record.Principal, + Interest: record.Interest, + InterestRate: record.InterestRate, + IsolatedSymbol: record.IsolatedSymbol, + Time: types.Time(record.InterestAccuredTime), + } + +} + +func toGlobalIsolatedUserAsset(userAsset binance.IsolatedUserAsset) types.IsolatedUserAsset { + return types.IsolatedUserAsset{ + Asset: userAsset.Asset, + Borrowed: fixedpoint.MustNewFromString(userAsset.Borrowed), + Free: fixedpoint.MustNewFromString(userAsset.Free), + Interest: fixedpoint.MustNewFromString(userAsset.Interest), + Locked: fixedpoint.MustNewFromString(userAsset.Locked), + NetAsset: fixedpoint.MustNewFromString(userAsset.NetAsset), + NetAssetOfBtc: fixedpoint.MustNewFromString(userAsset.NetAssetOfBtc), + BorrowEnabled: userAsset.BorrowEnabled, + RepayEnabled: userAsset.RepayEnabled, + TotalAsset: fixedpoint.MustNewFromString(userAsset.TotalAsset), + } +} + +func toGlobalIsolatedMarginAsset(asset binance.IsolatedMarginAsset) types.IsolatedMarginAsset { + return types.IsolatedMarginAsset{ + Symbol: asset.Symbol, + QuoteAsset: toGlobalIsolatedUserAsset(asset.QuoteAsset), + BaseAsset: toGlobalIsolatedUserAsset(asset.BaseAsset), + IsolatedCreated: asset.IsolatedCreated, + MarginLevel: fixedpoint.MustNewFromString(asset.MarginLevel), + MarginLevelStatus: asset.MarginLevelStatus, + MarginRatio: fixedpoint.MustNewFromString(asset.MarginRatio), + IndexPrice: fixedpoint.MustNewFromString(asset.IndexPrice), + LiquidatePrice: fixedpoint.MustNewFromString(asset.LiquidatePrice), + LiquidateRate: fixedpoint.MustNewFromString(asset.LiquidateRate), + TradeEnabled: false, + } +} + +func toGlobalIsolatedMarginAssets(assets []binance.IsolatedMarginAsset) (retAssets types.IsolatedMarginAssetMap) { + retMarginAssets := make(types.IsolatedMarginAssetMap) + for _, marginAsset := range assets { + retMarginAssets[marginAsset.Symbol] = toGlobalIsolatedMarginAsset(marginAsset) + } + + return retMarginAssets +} + +func toGlobalMarginUserAssets(assets []binance.UserAsset) types.MarginAssetMap { + retMarginAssets := make(types.MarginAssetMap) + for _, marginAsset := range assets { + retMarginAssets[marginAsset.Asset] = types.MarginUserAsset{ + Asset: marginAsset.Asset, + Borrowed: fixedpoint.MustNewFromString(marginAsset.Borrowed), + Free: fixedpoint.MustNewFromString(marginAsset.Free), + Interest: fixedpoint.MustNewFromString(marginAsset.Interest), + Locked: fixedpoint.MustNewFromString(marginAsset.Locked), + NetAsset: fixedpoint.MustNewFromString(marginAsset.NetAsset), + } + } + + return retMarginAssets +} + +func toGlobalMarginAccountInfo(account *binance.MarginAccount) *types.MarginAccountInfo { + return &types.MarginAccountInfo{ + BorrowEnabled: account.BorrowEnabled, + MarginLevel: fixedpoint.MustNewFromString(account.MarginLevel), + TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalAssetOfBTC), + TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC), + TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), + TradeEnabled: account.TradeEnabled, + TransferEnabled: account.TransferEnabled, + Assets: toGlobalMarginUserAssets(account.UserAssets), + } +} + +func toGlobalIsolatedMarginAccountInfo(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccountInfo { + return &types.IsolatedMarginAccountInfo{ + TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalAssetOfBTC), + TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC), + TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), + Assets: toGlobalIsolatedMarginAssets(account.Assets), + } +} diff --git a/pkg/exchange/binance/margin_history.go b/pkg/exchange/binance/margin_history.go index 5e8a0321f..59361fab2 100644 --- a/pkg/exchange/binance/margin_history.go +++ b/pkg/exchange/binance/margin_history.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" "github.com/c9s/bbgo/pkg/types" ) @@ -51,16 +50,6 @@ func (e *Exchange) QueryLoanHistory(ctx context.Context, asset string, startTime return loans, err } -func toGlobalLoan(record binanceapi.MarginLoanRecord) types.MarginLoanRecord { - return types.MarginLoanRecord{ - TransactionID: uint64(record.TxId), - Asset: record.Asset, - Principle: record.Principal, - Time: types.Time(record.Timestamp), - IsolatedSymbol: record.IsolatedSymbol, - } -} - func (e *Exchange) QueryRepayHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginRepayRecord, error) { req := e.client2.NewGetMarginRepayHistoryRequest() req.Asset(asset) @@ -101,16 +90,6 @@ func (e *Exchange) QueryRepayHistory(ctx context.Context, asset string, startTim return repays, err } -func toGlobalRepay(record binanceapi.MarginRepayRecord) types.MarginRepayRecord { - return types.MarginRepayRecord{ - TransactionID: record.TxId, - Asset: record.Asset, - Principle: record.Principal, - Time: types.Time(record.Timestamp), - IsolatedSymbol: record.IsolatedSymbol, - } -} - func (e *Exchange) QueryLiquidationHistory(ctx context.Context, startTime, endTime *time.Time) ([]types.MarginLiquidationRecord, error) { req := e.client2.NewGetMarginLiquidationHistoryRequest() @@ -172,14 +151,3 @@ func (e *Exchange) QueryInterestHistory(ctx context.Context, asset string, start return interests, err } -func toGlobalInterest(record binanceapi.MarginInterest) types.MarginInterest { - return types.MarginInterest{ - Asset: record.Asset, - Principle: record.Principal, - Interest: record.Interest, - InterestRate: record.InterestRate, - IsolatedSymbol: record.IsolatedSymbol, - Time: types.Time(record.InterestAccuredTime), - } - -}