mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #1679 from c9s/c9s/xalign/notification
FIX: [xalign] fix max withdraw history api query
This commit is contained in:
commit
00091e1ce1
|
@ -201,14 +201,14 @@ type Withdraw struct {
|
||||||
Notes string `json:"notes"`
|
Notes string `json:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:generate GetRequest -url "v2/withdrawals" -type GetWithdrawHistoryRequest -responseType []Withdraw
|
//go:generate GetRequest -url "v3/withdrawals" -type GetWithdrawHistoryRequest -responseType []Withdraw
|
||||||
type GetWithdrawHistoryRequest struct {
|
type GetWithdrawHistoryRequest struct {
|
||||||
client requestgen.AuthenticatedAPIClient
|
client requestgen.AuthenticatedAPIClient
|
||||||
|
|
||||||
currency string `param:"currency"`
|
currency *string `param:"currency"`
|
||||||
|
state *string `param:"state"` // submitting, submitted, rejected, accepted, checking, refunded, canceled, suspect
|
||||||
from *time.Time `param:"from,seconds"` // seconds
|
from *time.Time `param:"from,seconds"` // seconds
|
||||||
to *time.Time `param:"to,seconds"` // seconds
|
to *time.Time `param:"to,seconds"` // seconds
|
||||||
state *string `param:"state"` // submitting, submitted, rejected, accepted, checking, refunded, canceled, suspect
|
|
||||||
limit *int `param:"limit"`
|
limit *int `param:"limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -119,13 +119,21 @@ func (g *GetAccountRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetAccountRequest) GetPath() string {
|
||||||
|
return "v2/members/accounts/:currency"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetAccountRequest) Do(ctx context.Context) (*Account, error) {
|
func (g *GetAccountRequest) Do(ctx context.Context) (*Account, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
var params interface{}
|
var params interface{}
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
|
|
||||||
apiURL := "v2/members/accounts/:currency"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
slugs, err := g.GetSlugsMap()
|
slugs, err := g.GetSlugsMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -144,8 +152,32 @@ func (g *GetAccountRequest) Do(ctx context.Context) (*Account, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiResponse Account
|
var apiResponse Account
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
|
||||||
return nil, err
|
type responseUnmarshaler interface {
|
||||||
|
Unmarshal(data []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
|
||||||
|
if err := unmarshaler.Unmarshal(response.Body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The line below checks the content type, however, some API server might not send the correct content type header,
|
||||||
|
// Hence, this is commented for backward compatibility
|
||||||
|
// response.IsJSON()
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &apiResponse, nil
|
return &apiResponse, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,13 +109,21 @@ func (g *GetAccountsRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetAccountsRequest) GetPath() string {
|
||||||
|
return "v2/members/accounts"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetAccountsRequest) Do(ctx context.Context) ([]Account, error) {
|
func (g *GetAccountsRequest) Do(ctx context.Context) ([]Account, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
var params interface{}
|
var params interface{}
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
|
|
||||||
apiURL := "v2/members/accounts"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -128,8 +136,32 @@ func (g *GetAccountsRequest) Do(ctx context.Context) ([]Account, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiResponse []Account
|
var apiResponse []Account
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
|
||||||
return nil, err
|
type responseUnmarshaler interface {
|
||||||
|
Unmarshal(data []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
|
||||||
|
if err := unmarshaler.Unmarshal(response.Body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The line below checks the content type, however, some API server might not send the correct content type header,
|
||||||
|
// Hence, this is commented for backward compatibility
|
||||||
|
// response.IsJSON()
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return apiResponse, nil
|
return apiResponse, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,6 +178,12 @@ func (g *GetDepositHistoryRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetDepositHistoryRequest) GetPath() string {
|
||||||
|
return "v2/deposits"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetDepositHistoryRequest) Do(ctx context.Context) ([]Deposit, error) {
|
func (g *GetDepositHistoryRequest) Do(ctx context.Context) ([]Deposit, error) {
|
||||||
|
|
||||||
// empty params for GET operation
|
// empty params for GET operation
|
||||||
|
@ -187,7 +193,9 @@ func (g *GetDepositHistoryRequest) Do(ctx context.Context) ([]Deposit, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "v2/deposits"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -200,8 +208,32 @@ func (g *GetDepositHistoryRequest) Do(ctx context.Context) ([]Deposit, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiResponse []Deposit
|
var apiResponse []Deposit
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
|
||||||
return nil, err
|
type responseUnmarshaler interface {
|
||||||
|
Unmarshal(data []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
|
||||||
|
if err := unmarshaler.Unmarshal(response.Body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The line below checks the content type, however, some API server might not send the correct content type header,
|
||||||
|
// Hence, this is commented for backward compatibility
|
||||||
|
// response.IsJSON()
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return apiResponse, nil
|
return apiResponse, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,13 +109,21 @@ func (g *GetVipLevelRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetVipLevelRequest) GetPath() string {
|
||||||
|
return "v2/members/vip_level"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetVipLevelRequest) Do(ctx context.Context) (*VipLevel, error) {
|
func (g *GetVipLevelRequest) Do(ctx context.Context) (*VipLevel, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
var params interface{}
|
var params interface{}
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
|
|
||||||
apiURL := "v2/members/vip_level"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -128,8 +136,32 @@ func (g *GetVipLevelRequest) Do(ctx context.Context) (*VipLevel, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiResponse VipLevel
|
var apiResponse VipLevel
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
|
||||||
return nil, err
|
type responseUnmarshaler interface {
|
||||||
|
Unmarshal(data []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
|
||||||
|
if err := unmarshaler.Unmarshal(response.Body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The line below checks the content type, however, some API server might not send the correct content type header,
|
||||||
|
// Hence, this is commented for backward compatibility
|
||||||
|
// response.IsJSON()
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &apiResponse, nil
|
return &apiResponse, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Code generated by "requestgen -method GET -url v2/withdrawals -type GetWithdrawHistoryRequest -responseType []Withdraw"; DO NOT EDIT.
|
// Code generated by "requestgen -method GET -url v3/withdrawals -type GetWithdrawHistoryRequest -responseType []Withdraw"; DO NOT EDIT.
|
||||||
|
|
||||||
package max
|
package max
|
||||||
|
|
||||||
|
@ -14,7 +14,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (g *GetWithdrawHistoryRequest) Currency(currency string) *GetWithdrawHistoryRequest {
|
func (g *GetWithdrawHistoryRequest) Currency(currency string) *GetWithdrawHistoryRequest {
|
||||||
g.currency = currency
|
g.currency = ¤cy
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetWithdrawHistoryRequest) State(state string) *GetWithdrawHistoryRequest {
|
||||||
|
g.state = &state
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,11 +33,6 @@ func (g *GetWithdrawHistoryRequest) To(to time.Time) *GetWithdrawHistoryRequest
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GetWithdrawHistoryRequest) State(state string) *GetWithdrawHistoryRequest {
|
|
||||||
g.state = &state
|
|
||||||
return g
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GetWithdrawHistoryRequest) Limit(limit int) *GetWithdrawHistoryRequest {
|
func (g *GetWithdrawHistoryRequest) Limit(limit int) *GetWithdrawHistoryRequest {
|
||||||
g.limit = &limit
|
g.limit = &limit
|
||||||
return g
|
return g
|
||||||
|
@ -54,10 +54,21 @@ func (g *GetWithdrawHistoryRequest) GetQueryParameters() (url.Values, error) {
|
||||||
func (g *GetWithdrawHistoryRequest) GetParameters() (map[string]interface{}, error) {
|
func (g *GetWithdrawHistoryRequest) GetParameters() (map[string]interface{}, error) {
|
||||||
var params = map[string]interface{}{}
|
var params = map[string]interface{}{}
|
||||||
// check currency field -> json key currency
|
// check currency field -> json key currency
|
||||||
currency := g.currency
|
if g.currency != nil {
|
||||||
|
currency := *g.currency
|
||||||
|
|
||||||
// assign parameter of currency
|
// assign parameter of currency
|
||||||
params["currency"] = currency
|
params["currency"] = currency
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check state field -> json key state
|
||||||
|
if g.state != nil {
|
||||||
|
state := *g.state
|
||||||
|
|
||||||
|
// assign parameter of state
|
||||||
|
params["state"] = state
|
||||||
|
} else {
|
||||||
|
}
|
||||||
// check from field -> json key from
|
// check from field -> json key from
|
||||||
if g.from != nil {
|
if g.from != nil {
|
||||||
from := *g.from
|
from := *g.from
|
||||||
|
@ -76,14 +87,6 @@ func (g *GetWithdrawHistoryRequest) GetParameters() (map[string]interface{}, err
|
||||||
params["to"] = strconv.FormatInt(to.Unix(), 10)
|
params["to"] = strconv.FormatInt(to.Unix(), 10)
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
// check state field -> json key state
|
|
||||||
if g.state != nil {
|
|
||||||
state := *g.state
|
|
||||||
|
|
||||||
// assign parameter of state
|
|
||||||
params["state"] = state
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
// check limit field -> json key limit
|
// check limit field -> json key limit
|
||||||
if g.limit != nil {
|
if g.limit != nil {
|
||||||
limit := *g.limit
|
limit := *g.limit
|
||||||
|
@ -175,6 +178,12 @@ func (g *GetWithdrawHistoryRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetWithdrawHistoryRequest) GetPath() string {
|
||||||
|
return "v3/withdrawals"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]Withdraw, error) {
|
func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]Withdraw, error) {
|
||||||
|
|
||||||
// empty params for GET operation
|
// empty params for GET operation
|
||||||
|
@ -184,7 +193,9 @@ func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]Withdraw, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "v2/withdrawals"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -197,8 +208,32 @@ func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]Withdraw, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiResponse []Withdraw
|
var apiResponse []Withdraw
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
|
||||||
return nil, err
|
type responseUnmarshaler interface {
|
||||||
|
Unmarshal(data []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
|
||||||
|
if err := unmarshaler.Unmarshal(response.Body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The line below checks the content type, however, some API server might not send the correct content type header,
|
||||||
|
// Hence, this is commented for backward compatibility
|
||||||
|
// response.IsJSON()
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return apiResponse, nil
|
return apiResponse, nil
|
||||||
}
|
}
|
||||||
|
|
119
pkg/priceresolver/simple.go
Normal file
119
pkg/priceresolver/simple.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package priceresolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SimplePriceResolver implements a map-structure-based price index
|
||||||
|
type SimplePriceResolver struct {
|
||||||
|
// symbolPrices stores the latest trade price by mapping symbol to price
|
||||||
|
symbolPrices map[string]fixedpoint.Value
|
||||||
|
markets types.MarketMap
|
||||||
|
|
||||||
|
// pricesByBase stores the prices by currency names as a 2-level map
|
||||||
|
// BTC -> USDT -> 48000.0
|
||||||
|
// BTC -> TWD -> 1536000
|
||||||
|
pricesByBase map[string]map[string]fixedpoint.Value
|
||||||
|
|
||||||
|
// pricesByQuote is for reversed pairs, like USDT/TWD or BNB/BTC
|
||||||
|
// the reason that we don't store the reverse pricing in the same map is:
|
||||||
|
// expression like (1/price) could produce precision issue since the data type is fixed-point, only 8 fraction numbers are supported.
|
||||||
|
pricesByQuote map[string]map[string]fixedpoint.Value
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSimplePriceResolver(markets types.MarketMap) *SimplePriceResolver {
|
||||||
|
return &SimplePriceResolver{
|
||||||
|
markets: markets,
|
||||||
|
symbolPrices: make(map[string]fixedpoint.Value),
|
||||||
|
pricesByBase: make(map[string]map[string]fixedpoint.Value),
|
||||||
|
pricesByQuote: make(map[string]map[string]fixedpoint.Value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceResolver) Update(symbol string, price fixedpoint.Value) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
m.symbolPrices[symbol] = price
|
||||||
|
market, ok := m.markets[symbol]
|
||||||
|
if !ok {
|
||||||
|
log.Warnf("market info %s not found, unable to update price", symbol)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteMap, ok2 := m.pricesByBase[market.BaseCurrency]
|
||||||
|
if !ok2 {
|
||||||
|
quoteMap = make(map[string]fixedpoint.Value)
|
||||||
|
m.pricesByBase[market.BaseCurrency] = quoteMap
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteMap[market.QuoteCurrency] = price
|
||||||
|
|
||||||
|
baseMap, ok3 := m.pricesByQuote[market.QuoteCurrency]
|
||||||
|
if !ok3 {
|
||||||
|
baseMap = make(map[string]fixedpoint.Value)
|
||||||
|
m.pricesByQuote[market.QuoteCurrency] = baseMap
|
||||||
|
}
|
||||||
|
|
||||||
|
baseMap[market.BaseCurrency] = price
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceResolver) UpdateFromTrade(trade types.Trade) {
|
||||||
|
m.Update(trade.Symbol, trade.Price)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceResolver) inferencePrice(asset string, assetPrice fixedpoint.Value, preferredFiats ...string) (fixedpoint.Value, bool) {
|
||||||
|
// log.Infof("inferencePrice %s = %f", asset, assetPrice.Float64())
|
||||||
|
quotePrices, ok := m.pricesByBase[asset]
|
||||||
|
if ok {
|
||||||
|
for quote, price := range quotePrices {
|
||||||
|
for _, fiat := range preferredFiats {
|
||||||
|
if quote == fiat {
|
||||||
|
return price.Mul(assetPrice), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for quote, price := range quotePrices {
|
||||||
|
if infPrice, ok := m.inferencePrice(quote, price.Mul(assetPrice), preferredFiats...); ok {
|
||||||
|
return infPrice, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for example, quote = TWD here, we can get a price map with:
|
||||||
|
// USDT: 32.0 (for USDT/TWD at 32.0)
|
||||||
|
basePrices, ok := m.pricesByQuote[asset]
|
||||||
|
if ok {
|
||||||
|
for base, basePrice := range basePrices {
|
||||||
|
// log.Infof("base %s @ %s", base, basePrice.String())
|
||||||
|
for _, fiat := range preferredFiats {
|
||||||
|
if base == fiat {
|
||||||
|
// log.Infof("ret %f / %f = %f", assetPrice.Float64(), basePrice.Float64(), assetPrice.Div(basePrice).Float64())
|
||||||
|
return assetPrice.Div(basePrice), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for base, basePrice := range basePrices {
|
||||||
|
if infPrice, ok2 := m.inferencePrice(base, assetPrice.Div(basePrice), preferredFiats...); ok2 {
|
||||||
|
return infPrice, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixedpoint.Zero, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SimplePriceResolver) ResolvePrice(asset string, preferredFiats ...string) (fixedpoint.Value, bool) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
return m.inferencePrice(asset, fixedpoint.One, preferredFiats...)
|
||||||
|
}
|
146
pkg/priceresolver/simple_test.go
Normal file
146
pkg/priceresolver/simple_test.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
package priceresolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/c9s/bbgo/pkg/testing/testhelper"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSimplePriceResolver(t *testing.T) {
|
||||||
|
markets := types.MarketMap{
|
||||||
|
"BTCUSDT": types.Market{
|
||||||
|
BaseCurrency: "BTC",
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
},
|
||||||
|
"ETHUSDT": types.Market{
|
||||||
|
BaseCurrency: "ETH",
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
},
|
||||||
|
"BTCTWD": types.Market{
|
||||||
|
BaseCurrency: "BTC",
|
||||||
|
QuoteCurrency: "TWD",
|
||||||
|
},
|
||||||
|
"ETHTWD": types.Market{
|
||||||
|
BaseCurrency: "ETH",
|
||||||
|
QuoteCurrency: "TWD",
|
||||||
|
},
|
||||||
|
"USDTTWD": types.Market{
|
||||||
|
BaseCurrency: "USDT",
|
||||||
|
QuoteCurrency: "TWD",
|
||||||
|
},
|
||||||
|
"ETHBTC": types.Market{
|
||||||
|
BaseCurrency: "ETH",
|
||||||
|
QuoteCurrency: "BTC",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("direct reference", func(t *testing.T) {
|
||||||
|
pm := NewSimplePriceResolver(markets)
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Price: Number(48000.0),
|
||||||
|
})
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "ETHUSDT",
|
||||||
|
Price: Number(2800.0),
|
||||||
|
})
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "USDTTWD",
|
||||||
|
Price: Number(32.0),
|
||||||
|
})
|
||||||
|
|
||||||
|
finalPrice, ok := pm.ResolvePrice("BTC", "USDT")
|
||||||
|
if assert.True(t, ok) {
|
||||||
|
assert.Equal(t, "48000", finalPrice.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPrice, ok = pm.ResolvePrice("ETH", "USDT")
|
||||||
|
if assert.True(t, ok) {
|
||||||
|
assert.Equal(t, "2800", finalPrice.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPrice, ok = pm.ResolvePrice("USDT", "TWD")
|
||||||
|
if assert.True(t, ok) {
|
||||||
|
assert.Equal(t, "32", finalPrice.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("simple reference", func(t *testing.T) {
|
||||||
|
pm := NewSimplePriceResolver(markets)
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Price: Number(48000.0),
|
||||||
|
})
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "ETHUSDT",
|
||||||
|
Price: Number(2800.0),
|
||||||
|
})
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "USDTTWD",
|
||||||
|
Price: Number(32.0),
|
||||||
|
})
|
||||||
|
|
||||||
|
finalPrice, ok := pm.ResolvePrice("BTC", "TWD")
|
||||||
|
if assert.True(t, ok) {
|
||||||
|
assert.Equal(t, "1536000", finalPrice.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("crypto reference", func(t *testing.T) {
|
||||||
|
pm := NewSimplePriceResolver(markets)
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Price: Number(52000.0),
|
||||||
|
})
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "ETHBTC",
|
||||||
|
Price: Number(0.055),
|
||||||
|
})
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "USDTTWD",
|
||||||
|
Price: Number(32.0),
|
||||||
|
})
|
||||||
|
|
||||||
|
finalPrice, ok := pm.ResolvePrice("ETH", "USDT")
|
||||||
|
if assert.True(t, ok) {
|
||||||
|
assert.Equal(t, "2860", finalPrice.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inverse reference", func(t *testing.T) {
|
||||||
|
pm := NewSimplePriceResolver(markets)
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "BTCTWD",
|
||||||
|
Price: Number(1536000.0),
|
||||||
|
})
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "USDTTWD",
|
||||||
|
Price: Number(32.0),
|
||||||
|
})
|
||||||
|
|
||||||
|
finalPrice, ok := pm.ResolvePrice("BTC", "USDT")
|
||||||
|
if assert.True(t, ok) {
|
||||||
|
assert.Equal(t, "48000", finalPrice.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inverse reference", func(t *testing.T) {
|
||||||
|
pm := NewSimplePriceResolver(markets)
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "BTCTWD",
|
||||||
|
Price: Number(1536000.0),
|
||||||
|
})
|
||||||
|
pm.UpdateFromTrade(types.Trade{
|
||||||
|
Symbol: "USDTTWD",
|
||||||
|
Price: Number(32.0),
|
||||||
|
})
|
||||||
|
|
||||||
|
finalPrice, ok := pm.ResolvePrice("TWD", "USDT")
|
||||||
|
if assert.True(t, ok) {
|
||||||
|
assert.InDelta(t, 0.03125, finalPrice.Float64(), 0.0001)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -9,10 +9,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
"github.com/c9s/bbgo/pkg/core"
|
"github.com/c9s/bbgo/pkg/core"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/priceresolver"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,6 +22,8 @@ const ID = "xalign"
|
||||||
|
|
||||||
var log = logrus.WithField("strategy", ID)
|
var log = logrus.WithField("strategy", ID)
|
||||||
|
|
||||||
|
var activeTransferNotificationLimiter = rate.NewLimiter(rate.Every(5*time.Minute), 1)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
bbgo.RegisterStrategy(ID, &Strategy{})
|
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||||
}
|
}
|
||||||
|
@ -47,8 +51,14 @@ type Strategy struct {
|
||||||
Duration types.Duration `json:"for"`
|
Duration types.Duration `json:"for"`
|
||||||
MaxAmounts map[string]fixedpoint.Value `json:"maxAmounts"`
|
MaxAmounts map[string]fixedpoint.Value `json:"maxAmounts"`
|
||||||
|
|
||||||
|
SlackNotify bool `json:"slackNotify"`
|
||||||
|
SlackNotifyMentions []string `json:"slackNotifyMentions"`
|
||||||
|
SlackNotifyThresholdAmount fixedpoint.Value `json:"slackNotifyThresholdAmount,omitempty"`
|
||||||
|
|
||||||
faultBalanceRecords map[string][]TimeBalance
|
faultBalanceRecords map[string][]TimeBalance
|
||||||
|
|
||||||
|
priceResolver *priceresolver.SimplePriceResolver
|
||||||
|
|
||||||
sessions map[string]*bbgo.ExchangeSession
|
sessions map[string]*bbgo.ExchangeSession
|
||||||
orderBooks map[string]*bbgo.ActiveOrderBook
|
orderBooks map[string]*bbgo.ActiveOrderBook
|
||||||
|
|
||||||
|
@ -114,7 +124,10 @@ func (s *Strategy) aggregateBalances(
|
||||||
return totalBalances, sessionBalances
|
return totalBalances, sessionBalances
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) detectActiveTransfers(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) (bool, error) {
|
func (s *Strategy) detectActiveWithdraw(
|
||||||
|
ctx context.Context,
|
||||||
|
sessions map[string]*bbgo.ExchangeSession,
|
||||||
|
) (*types.Withdraw, error) {
|
||||||
var err2 error
|
var err2 error
|
||||||
until := time.Now()
|
until := time.Now()
|
||||||
since := until.Add(-time.Hour * 24)
|
since := until.Add(-time.Hour * 24)
|
||||||
|
@ -134,12 +147,12 @@ func (s *Strategy) detectActiveTransfers(ctx context.Context, sessions map[strin
|
||||||
for _, withdraw := range withdraws {
|
for _, withdraw := range withdraws {
|
||||||
switch withdraw.Status {
|
switch withdraw.Status {
|
||||||
case types.WithdrawStatusProcessing, types.WithdrawStatusSent, types.WithdrawStatusAwaitingApproval:
|
case types.WithdrawStatusProcessing, types.WithdrawStatusSent, types.WithdrawStatusAwaitingApproval:
|
||||||
return true, nil
|
return &withdraw, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, err2
|
return nil, err2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) selectSessionForCurrency(
|
func (s *Strategy) selectSessionForCurrency(
|
||||||
|
@ -340,6 +353,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
|
||||||
|
|
||||||
s.orderStore = core.NewOrderStore("")
|
s.orderStore = core.NewOrderStore("")
|
||||||
|
|
||||||
|
markets := types.MarketMap{}
|
||||||
for _, sessionName := range s.PreferredSessions {
|
for _, sessionName := range s.PreferredSessions {
|
||||||
session, ok := sessions[sessionName]
|
session, ok := sessions[sessionName]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -353,8 +367,12 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
|
||||||
s.orderBooks[sessionName] = orderBook
|
s.orderBooks[sessionName] = orderBook
|
||||||
|
|
||||||
s.sessions[sessionName] = session
|
s.sessions[sessionName] = session
|
||||||
|
|
||||||
|
// session.Market(symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.priceResolver = priceresolver.NewSimplePriceResolver(markets)
|
||||||
|
|
||||||
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
|
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for n, session := range s.sessions {
|
for n, session := range s.sessions {
|
||||||
|
@ -405,7 +423,6 @@ func (s *Strategy) recordBalance(totalBalances types.BalanceMap) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) {
|
func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) {
|
||||||
|
|
||||||
for sessionName, session := range sessions {
|
for sessionName, session := range sessions {
|
||||||
ob, ok := s.orderBooks[sessionName]
|
ob, ok := s.orderBooks[sessionName]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -414,16 +431,20 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
if err := ob.GracefulCancel(ctx, session.Exchange); err != nil {
|
if err := ob.GracefulCancel(ctx, session.Exchange); err != nil {
|
||||||
log.WithError(err).Errorf("can not cancel order")
|
log.WithError(err).Errorf("unable to cancel order")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foundActiveTransfer, err := s.detectActiveTransfers(ctx, sessions)
|
pendingWithdraw, err := s.detectActiveWithdraw(ctx, sessions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Errorf("unable to check active transfers")
|
log.WithError(err).Errorf("unable to check active transfers")
|
||||||
} else if foundActiveTransfer {
|
} else if pendingWithdraw != nil {
|
||||||
log.Warnf("found active transfer, skip balance align check")
|
log.Warnf("found active transfer, skip balance align check")
|
||||||
|
|
||||||
|
if activeTransferNotificationLimiter.Allow() {
|
||||||
|
bbgo.Notify("Found active withdraw, skip balance align", pendingWithdraw)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user