Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mm/binance: Use filters and withdraw multiple #3093

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 104 additions & 29 deletions client/mm/libxc/binance.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,11 @@ type tradeInfo struct {
qty uint64
}

type withdrawInfo struct {
minimum uint64
lotSize uint64
}

type binance struct {
log dex.Logger
marketsURL string
Expand All @@ -467,7 +472,7 @@ type binance struct {
// for each chain for which deposits and withdrawals are enabled on
// binance.
tokenIDs atomic.Value // map[string][]uint32, binance coin ID string -> assset IDs
minWithdraw atomic.Value // map[uint32]map[uint32]uint64
minWithdraw atomic.Value // map[uint32]map[uint32]*withdrawInfo

marketSnapshotMtx sync.Mutex
marketSnapshot struct {
Expand Down Expand Up @@ -601,7 +606,7 @@ func (bnc *binance) refreshBalances(ctx context.Context) error {
// enabled on binance and sets the minWithdraw map.
func (bnc *binance) readCoins(coins []*bntypes.CoinInfo) {
tokenIDs := make(map[string][]uint32)
minWithdraw := make(map[uint32]uint64)
minWithdraw := make(map[uint32]*withdrawInfo)
for _, nfo := range coins {
for _, netInfo := range nfo.NetworkList {
symbol := binanceCoinNetworkToDexSymbol(nfo.Coin, netInfo.Network)
Expand All @@ -621,7 +626,11 @@ func (bnc *binance) readCoins(coins []*bntypes.CoinInfo) {
if tkn := asset.TokenInfo(assetID); tkn != nil {
tokenIDs[nfo.Coin] = append(tokenIDs[nfo.Coin], assetID)
}
minWithdraw[assetID] = uint64(math.Round(float64(ui.Conventional.ConversionFactor) * netInfo.WithdrawMin))
minimum := uint64(math.Round(float64(ui.Conventional.ConversionFactor) * netInfo.WithdrawMin))
minWithdraw[assetID] = &withdrawInfo{
minimum: minimum,
lotSize: uint64(math.Round(netInfo.WithdrawIntegerMultiple * float64(ui.Conventional.ConversionFactor))),
}
}
}
bnc.tokenIDs.Store(tokenIDs)
Expand Down Expand Up @@ -649,12 +658,44 @@ func (bnc *binance) getMarkets(ctx context.Context) (map[string]*bntypes.Market,
}

marketsMap := make(map[string]*bntypes.Market, len(exchangeInfo.Symbols))
tokenIDs := bnc.tokenIDs.Load().(map[string][]uint32)
for _, market := range exchangeInfo.Symbols {
dexMarkets := binanceMarketToDexMarkets(market.BaseAsset, market.QuoteAsset, tokenIDs, bnc.isUS)
if len(dexMarkets) == 0 {
continue
}
dexMkt := dexMarkets[0]

bui, _ := asset.UnitInfo(dexMkt.BaseID)
qui, _ := asset.UnitInfo(dexMkt.QuoteID)

var rateStepFound, lotSizeFound bool
for _, filter := range market.Filters {
if filter.Type == "PRICE_FILTER" {
rateStepFound = true
conv := float64(qui.Conventional.ConversionFactor) / float64(bui.Conventional.ConversionFactor) * calc.RateEncodingFactor
market.RateStep = uint64(math.Round(filter.TickSize * conv))
market.MinPrice = uint64(math.Round(filter.MinPrice * conv))
market.MaxPrice = uint64(math.Round(filter.MaxPrice * conv))
} else if filter.Type == "LOT_SIZE" {
lotSizeFound = true
market.LotSize = uint64(math.Round(filter.StepSize * float64(bui.Conventional.ConversionFactor)))
market.MinQty = uint64(math.Round(filter.MinQty * float64(bui.Conventional.ConversionFactor)))
market.MaxQty = uint64(math.Round(filter.MaxQty * float64(bui.Conventional.ConversionFactor)))
}
if rateStepFound && lotSizeFound {
break
}
}
if !rateStepFound || !lotSizeFound {
bnc.log.Errorf("missing filter for market %s, rate step found = %t, lot size found = %t", dexMkt.MarketID, rateStepFound, lotSizeFound)
continue
}

marketsMap[market.Symbol] = market
}

bnc.markets.Store(marketsMap)

return marketsMap, nil
}

Expand Down Expand Up @@ -767,6 +808,16 @@ func (bnc *binance) generateTradeID() string {
return hex.EncodeToString(append(bnc.tradeIDNoncePrefix, nonceB...))
}

// steppedRate rounds the rate to the nearest integer multiple of the step.
// The minimum returned value is step.
func steppedRate(r, step uint64) uint64 {
steps := math.Round(float64(r) / float64(step))
if steps == 0 {
return step
}
return uint64(math.Round(steps * float64(step)))
}

// Trade executes a trade on the CEX. subscriptionID takes an ID returned from
// SubscribeTradeUpdates.
func (bnc *binance) Trade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64, subscriptionID int) (*Trade, error) {
Expand All @@ -793,8 +844,22 @@ func (bnc *binance) Trade(ctx context.Context, baseID, quoteID uint32, sell bool
return nil, fmt.Errorf("market not found: %v", slug)
}

price := calc.ConventionalRateAlt(rate, baseCfg.conversionFactor, quoteCfg.conversionFactor)
amt := float64(qty) / float64(baseCfg.conversionFactor)
if rate < market.MinPrice || rate > market.MaxPrice {
return nil, fmt.Errorf("rate %v is out of bounds for market %v", rate, slug)
}
rate = steppedRate(rate, market.RateStep)
convRate := calc.ConventionalRateAlt(rate, baseCfg.conversionFactor, quoteCfg.conversionFactor)
ratePrec := int(math.Round(math.Log10(calc.RateEncodingFactor * float64(baseCfg.conversionFactor) / float64(quoteCfg.conversionFactor) / float64(market.RateStep))))
rateStr := strconv.FormatFloat(convRate, 'f', ratePrec, 64)

if qty < market.MinQty || qty > market.MaxQty {
return nil, fmt.Errorf("quantity %v is out of bounds for market %v", qty, slug)
}
steppedQty := steppedRate(qty, market.LotSize)
convQty := float64(steppedQty) / float64(baseCfg.conversionFactor)
qtyPrec := int(math.Round(math.Log10(float64(baseCfg.conversionFactor) / float64(market.LotSize))))
qtyStr := strconv.FormatFloat(convQty, 'f', qtyPrec, 64)

tradeID := bnc.generateTradeID()

v := make(url.Values)
Expand All @@ -803,8 +868,8 @@ func (bnc *binance) Trade(ctx context.Context, baseID, quoteID uint32, sell bool
v.Add("type", "LIMIT")
v.Add("timeInForce", "GTC")
v.Add("newClientOrderId", tradeID)
v.Add("quantity", strconv.FormatFloat(amt, 'f', market.BaseAssetPrecision, 64))
v.Add("price", strconv.FormatFloat(price, 'f', market.QuoteAssetPrecision, 64))
v.Add("quantity", qtyStr)
v.Add("price", rateStr)

bnc.tradeUpdaterMtx.Lock()
_, found = bnc.tradeUpdaters[subscriptionID]
Expand Down Expand Up @@ -852,18 +917,6 @@ func (bnc *binance) Trade(ctx context.Context, baseID, quoteID uint32, sell bool
}, err
}

func (bnc *binance) assetPrecision(coin string) (int, error) {
for _, market := range bnc.markets.Load().(map[string]*bntypes.Market) {
if market.BaseAsset == coin {
return market.BaseAssetPrecision, nil
}
if market.QuoteAsset == coin {
return market.QuoteAssetPrecision, nil
}
}
return 0, fmt.Errorf("asset %s not found", coin)
}

// ConfirmWithdrawal checks whether a withdrawal has been completed. If the
// withdrawal has not yet been sent, ErrWithdrawalPending is returned.
func (bnc *binance) ConfirmWithdrawal(ctx context.Context, withdrawalID string, assetID uint32) (uint64, string, error) {
Expand Down Expand Up @@ -917,17 +970,21 @@ func (bnc *binance) Withdraw(ctx context.Context, assetID uint32, qty uint64, ad
return "", fmt.Errorf("error getting symbol data for %d: %w", assetID, err)
}

precision, err := bnc.assetPrecision(assetCfg.coin)
lotSize, err := bnc.withdrawLotSize(assetID)
if err != nil {
return "", fmt.Errorf("error getting precision for %s: %w", assetCfg.coin, err)
return "", fmt.Errorf("error getting withdraw lot size for %d: %w", assetID, err)
}

amt := float64(qty) / float64(assetCfg.conversionFactor)
steppedQty := steppedRate(qty, lotSize)
convQty := float64(steppedQty) / float64(assetCfg.conversionFactor)
prec := int(math.Round(math.Log10(float64(assetCfg.conversionFactor) / float64(lotSize))))
qtyStr := strconv.FormatFloat(convQty, 'f', prec, 64)

v := make(url.Values)
v.Add("coin", assetCfg.coin)
v.Add("network", assetCfg.chain)
v.Add("address", address)
v.Add("amount", strconv.FormatFloat(amt, 'f', precision, 64))
v.Add("amount", qtyStr)

withdrawResp := struct {
ID string `json:"id"`
Expand Down Expand Up @@ -1091,13 +1148,31 @@ func (bnc *binance) Balances(ctx context.Context) (map[uint32]*ExchangeBalance,
return balances, nil
}

func (bnc *binance) minimumWithdraws(baseID, quoteID uint32) (uint64, uint64) {
func (bnc *binance) minimumWithdraws(baseID, quoteID uint32) (base uint64, quote uint64) {
minsI := bnc.minWithdraw.Load()
if minsI == nil {
return 0, 0
}
mins := minsI.(map[uint32]uint64)
return mins[baseID], mins[quoteID]
mins := minsI.(map[uint32]*withdrawInfo)
if baseInfo, found := mins[baseID]; found {
base = baseInfo.minimum
}
if quoteInfo, found := mins[quoteID]; found {
quote = quoteInfo.minimum
}
return
}

func (bnc *binance) withdrawLotSize(assetID uint32) (uint64, error) {
minsI := bnc.minWithdraw.Load()
if minsI == nil {
return 0, fmt.Errorf("no withdraw info")
}
mins := minsI.(map[uint32]*withdrawInfo)
if info, found := mins[assetID]; found {
return info.lotSize, nil
}
return 0, fmt.Errorf("no withdraw info for asset ID %d", assetID)
}

func (bnc *binance) Markets(ctx context.Context) (map[string]*Market, error) {
Expand Down Expand Up @@ -1161,6 +1236,7 @@ func (bnc *binance) Markets(ctx context.Context) (map[string]*Market, error) {
}
bnc.marketSnapshot.m = m
bnc.marketSnapshot.stamp = time.Now()

return m, nil
}

Expand Down Expand Up @@ -1248,13 +1324,12 @@ func (bnc *binance) request(ctx context.Context, method, endpoint string, query,

req.Header = header

// bnc.log.Tracef("Sending request: %+v", req)
var errPayload struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
if err := dexnet.Do(req, thing, dexnet.WithSizeLimit(1<<24), dexnet.WithErrorParsing(&errPayload)); err != nil {
bnc.log.Errorf("request error from endpoint %q with query = %q, body = %q", endpoint, queryString, bodyString)
bnc.log.Errorf("request error from endpoint %s %q with query = %q, body = %q", method, endpoint, queryString, bodyString)
return fmt.Errorf("%w, bn code = %d, msg = %q", err, errPayload.Code, errPayload.Msg)
}
return nil
Expand Down
43 changes: 33 additions & 10 deletions client/mm/libxc/bntypes/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,37 @@ package bntypes

import "encoding/json"

type Filter struct {
Type string `json:"filterType"`

// Price filter
MinPrice float64 `json:"minPrice,string"`
MaxPrice float64 `json:"maxPrice,string"`
TickSize float64 `json:"tickSize,string"`

// Lot size filter
MinQty float64 `json:"minQty,string"`
MaxQty float64 `json:"maxQty,string"`
StepSize float64 `json:"stepSize,string"`
}

type Market struct {
Symbol string `json:"symbol"`
Status string `json:"status"`
BaseAsset string `json:"baseAsset"`
BaseAssetPrecision int `json:"baseAssetPrecision"`
QuoteAsset string `json:"quoteAsset"`
QuoteAssetPrecision int `json:"quoteAssetPrecision"`
OrderTypes []string `json:"orderTypes"`
Symbol string `json:"symbol"`
Status string `json:"status"`
BaseAsset string `json:"baseAsset"`
BaseAssetPrecision int `json:"baseAssetPrecision"`
QuoteAsset string `json:"quoteAsset"`
QuoteAssetPrecision int `json:"quoteAssetPrecision"`
OrderTypes []string `json:"orderTypes"`
Filters []*Filter `json:"filters"`

// Below fields are parsed from Filters.
LotSize uint64
MinQty uint64
MaxQty uint64
RateStep uint64
MinPrice uint64
MaxPrice uint64
}

type Balance struct {
Expand All @@ -34,9 +57,9 @@ type NetworkInfo struct {
// ResetAddressStatus bool `json:"resetAddressStatus"`
// SpecialTips string `json:"specialTips"`
// UnLockConfirm int `json:"unLockConfirm"`
WithdrawEnable bool `json:"withdrawEnable"`
WithdrawFee float64 `json:"withdrawFee,string"`
// WithdrawIntegerMultiple float64 `json:"withdrawIntegerMultiple,string"`
WithdrawEnable bool `json:"withdrawEnable"`
WithdrawFee float64 `json:"withdrawFee,string"`
WithdrawIntegerMultiple float64 `json:"withdrawIntegerMultiple,string"`
// WithdrawMax float64 `json:"withdrawMax,string"`
WithdrawMin float64 `json:"withdrawMin,string"`
// SameAddress bool `json:"sameAddress"`
Comment on lines 63 to 65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe remove commented.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok.. good to know what other fields there are without having to check the API.

Expand Down
Loading