Binance: Fixes expiring futures contract pair formatting (#836)

* Fixes expiring contract formatting

* minor simplification

* shazniterinos

* Removes secret scientific test

* continue to address nits

* Shazniterinos

* Adds `bypassConfigFormatUpgrades` feature. Fixes Binance pair parsing.

* lint fix

* Adds test for binance func

* Consistent messaging and oopsie fix
This commit is contained in:
Scott
2021-11-16 15:02:31 +11:00
committed by GitHub
parent 25e9d284d0
commit 7c7aebe22f
17 changed files with 661 additions and 373 deletions

View File

@@ -172,7 +172,7 @@ func TestUExchangeInfo(t *testing.T) {
func TestUFuturesOrderbook(t *testing.T) {
t.Parallel()
_, err := b.UFuturesOrderbook(context.Background(), currency.Pair{Delimiter: "_", Base: currency.BTC, Quote: currency.USDT}, 1000)
_, err := b.UFuturesOrderbook(context.Background(), currency.NewPair(currency.BTC, currency.USDT), 1000)
if err != nil {
t.Error(err)
}
@@ -2647,3 +2647,139 @@ func TestWsOutboundAccountPosition(t *testing.T) {
t.Fatal(err)
}
}
func TestFormatExchangeCurrency(t *testing.T) {
t.Parallel()
type testos struct {
name string
pair currency.Pair
asset asset.Item
expectedDelimiter string
}
testerinos := []testos{
{
name: "spot-btcusdt",
pair: currency.NewPairWithDelimiter("BTC", "USDT", currency.UnderscoreDelimiter),
asset: asset.Spot,
expectedDelimiter: "",
},
{
name: "coinmarginedfutures-btcusd_perp",
pair: currency.NewPairWithDelimiter("BTCUSD", "PERP", currency.DashDelimiter),
asset: asset.CoinMarginedFutures,
expectedDelimiter: currency.UnderscoreDelimiter,
},
{
name: "coinmarginedfutures-btcusd_211231",
pair: currency.NewPairWithDelimiter("BTCUSD", "211231", currency.DashDelimiter),
asset: asset.CoinMarginedFutures,
expectedDelimiter: currency.UnderscoreDelimiter,
},
{
name: "margin-ltousdt",
pair: currency.NewPairWithDelimiter("LTO", "USDT", currency.UnderscoreDelimiter),
asset: asset.Margin,
expectedDelimiter: "",
},
{
name: "usdtmarginedfutures-btcusdt",
pair: currency.NewPairWithDelimiter("btc", "usdt", currency.DashDelimiter),
asset: asset.USDTMarginedFutures,
expectedDelimiter: "",
},
{
name: "usdtmarginedfutures-btcusdt_211231",
pair: currency.NewPairWithDelimiter("btcusdt", "211231", currency.UnderscoreDelimiter),
asset: asset.USDTMarginedFutures,
expectedDelimiter: currency.UnderscoreDelimiter,
},
}
for i := range testerinos {
tt := testerinos[i]
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result, err := b.FormatExchangeCurrency(tt.pair, tt.asset)
if err != nil {
t.Error(err)
}
if result.Delimiter != tt.expectedDelimiter {
t.Errorf("received '%v' expected '%v'", result.Delimiter, tt.expectedDelimiter)
}
})
}
}
func TestFormatSymbol(t *testing.T) {
t.Parallel()
type testos struct {
name string
pair currency.Pair
asset asset.Item
expectedString string
}
testerinos := []testos{
{
name: "spot-BTCUSDT",
pair: currency.NewPairWithDelimiter("BTC", "USDT", currency.UnderscoreDelimiter),
asset: asset.Spot,
expectedString: "BTCUSDT",
},
{
name: "coinmarginedfutures-btcusdperp",
pair: currency.NewPairWithDelimiter("BTCUSD", "PERP", currency.DashDelimiter),
asset: asset.CoinMarginedFutures,
expectedString: "BTCUSD_PERP",
},
{
name: "coinmarginedfutures-BTCUSD_211231",
pair: currency.NewPairWithDelimiter("BTCUSD", "211231", currency.DashDelimiter),
asset: asset.CoinMarginedFutures,
expectedString: "BTCUSD_211231",
},
{
name: "margin-LTOUSDT",
pair: currency.NewPairWithDelimiter("LTO", "USDT", currency.UnderscoreDelimiter),
asset: asset.Margin,
expectedString: "LTOUSDT",
},
{
name: "usdtmarginedfutures-BTCUSDT",
pair: currency.NewPairWithDelimiter("btc", "usdt", currency.DashDelimiter),
asset: asset.USDTMarginedFutures,
expectedString: "BTCUSDT",
},
{
name: "usdtmarginedfutures-BTCUSDT_211231",
pair: currency.NewPairWithDelimiter("btcusdt", "211231", currency.UnderscoreDelimiter),
asset: asset.USDTMarginedFutures,
expectedString: "BTCUSDT_211231",
},
}
for i := range testerinos {
tt := testerinos[i]
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result, err := b.FormatSymbol(tt.pair, tt.asset)
if err != nil {
t.Error(err)
}
if result != tt.expectedString {
t.Errorf("received '%v' expected '%v'", result, tt.expectedString)
}
})
}
}
func TestFormatUSDTMarginedFuturesPair(t *testing.T) {
t.Parallel()
pairFormat := currency.PairFormat{Uppercase: true}
resp := b.formatUSDTMarginedFuturesPair(currency.NewPair(currency.DOGE, currency.USDT), pairFormat)
if resp.String() != "DOGEUSDT" {
t.Errorf("received '%v' expected '%v'", resp.String(), "DOGEUSDT")
}
resp = b.formatUSDTMarginedFuturesPair(currency.NewPair(currency.DOGE, currency.NewCode("1234567890")), pairFormat)
if resp.String() != "DOGE_1234567890" {
t.Errorf("received '%v' expected '%v'", resp.String(), "DOGE_1234567890")
}
}

View File

@@ -84,6 +84,7 @@ func (b *Binance) SetDefaults() {
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: currency.UnderscoreDelimiter,
},
}
err := b.StoreAssetPairFormat(asset.Spot, fmt1)
@@ -264,6 +265,7 @@ func (b *Binance) Run() {
b.PrintEnabledPairs()
}
forceUpdate := false
a := b.GetAssetTypes(true)
for x := range a {
if err := b.UpdateOrderExecutionLimits(context.TODO(), a[x]); err != nil {
@@ -272,13 +274,60 @@ func (b *Binance) Run() {
b.Name,
err)
}
if a[x] == asset.USDTMarginedFutures && !b.BypassConfigFormatUpgrades {
format, err := b.GetPairFormat(asset.USDTMarginedFutures, false)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to get enabled currencies. Err %s\n",
b.Name,
err)
return
}
var enabled, avail currency.Pairs
enabled, err = b.CurrencyPairs.GetPairs(asset.USDTMarginedFutures, true)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to get enabled currencies. Err %s\n",
b.Name,
err)
return
}
avail, err = b.CurrencyPairs.GetPairs(asset.USDTMarginedFutures, false)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to get available currencies. Err %s\n",
b.Name,
err)
return
}
if !common.StringDataContains(enabled.Strings(), format.Delimiter) ||
!common.StringDataContains(avail.Strings(), format.Delimiter) {
var enabledPairs currency.Pairs
enabledPairs, err = currency.NewPairsFromStrings([]string{
currency.BTC.String() + format.Delimiter + currency.USDT.String(),
})
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to update currencies. Err %s\n",
b.Name,
err)
} else {
log.Warnf(log.ExchangeSys, exchange.ResetConfigPairsWarningMessage, b.Name, a[x], enabledPairs)
forceUpdate = true
err = b.UpdatePairs(enabledPairs, a[x], true, true)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update currencies. Err: %s\n",
b.Name,
err)
}
}
}
}
}
if !b.GetEnabledFeatures().AutoPairUpdates {
if !b.GetEnabledFeatures().AutoPairUpdates && !forceUpdate {
return
}
if err := b.UpdateTradablePairs(context.TODO(), false); err != nil {
if err := b.UpdateTradablePairs(context.TODO(), forceUpdate); err != nil {
log.Errorf(log.ExchangeSys,
"%s failed to update tradable pairs. Err: %s",
b.Name,
@@ -295,53 +344,70 @@ func (b *Binance) FetchTradablePairs(ctx context.Context, a asset.Item) ([]strin
if err != nil {
return nil, err
}
tradingStatus := "TRADING"
var pairs []string
switch a {
case asset.Spot, asset.Margin:
info, err := b.GetExchangeInfo(ctx)
var info ExchangeInfo
info, err = b.GetExchangeInfo(ctx)
if err != nil {
return nil, err
}
for x := range info.Symbols {
if info.Symbols[x].Status == "TRADING" {
pair := info.Symbols[x].BaseAsset +
format.Delimiter +
info.Symbols[x].QuoteAsset
if a == asset.Spot && info.Symbols[x].IsSpotTradingAllowed {
pairs = append(pairs, pair)
}
if a == asset.Margin && info.Symbols[x].IsMarginTradingAllowed {
pairs = append(pairs, pair)
}
if info.Symbols[x].Status != tradingStatus {
continue
}
pair := info.Symbols[x].BaseAsset +
format.Delimiter +
info.Symbols[x].QuoteAsset
if a == asset.Spot && info.Symbols[x].IsSpotTradingAllowed {
pairs = append(pairs, pair)
}
if a == asset.Margin && info.Symbols[x].IsMarginTradingAllowed {
pairs = append(pairs, pair)
}
}
case asset.CoinMarginedFutures:
cInfo, err := b.FuturesExchangeInfo(ctx)
var cInfo CExchangeInfo
cInfo, err = b.FuturesExchangeInfo(ctx)
if err != nil {
return pairs, err
}
for z := range cInfo.Symbols {
if cInfo.Symbols[z].ContractStatus == "TRADING" {
curr, err := currency.NewPairFromString(cInfo.Symbols[z].Symbol)
if err != nil {
return nil, err
}
pairs = append(pairs, format.Format(curr))
if cInfo.Symbols[z].ContractStatus != tradingStatus {
continue
}
var curr currency.Pair
curr, err = currency.NewPairFromString(cInfo.Symbols[z].Symbol)
if err != nil {
return nil, err
}
pairs = append(pairs, format.Format(curr))
}
case asset.USDTMarginedFutures:
uInfo, err := b.UExchangeInfo(ctx)
var uInfo UFuturesExchangeInfo
uInfo, err = b.UExchangeInfo(ctx)
if err != nil {
return pairs, err
}
for u := range uInfo.Symbols {
if uInfo.Symbols[u].Status == "TRADING" {
curr, err := currency.NewPairFromString(uInfo.Symbols[u].Symbol)
if uInfo.Symbols[u].Status != tradingStatus {
continue
}
var curr currency.Pair
if uInfo.Symbols[u].ContractType == "PERPETUAL" {
curr, err = currency.NewPairFromStrings(uInfo.Symbols[u].BaseAsset, uInfo.Symbols[u].QuoteAsset)
if err != nil {
return nil, err
}
} else {
curr, err = currency.NewPairFromString(uInfo.Symbols[u].Symbol)
if err != nil {
return nil, err
}
pairs = append(pairs, format.Format(curr))
}
pairs = append(pairs, format.Format(curr))
}
}
return pairs, nil
@@ -1742,3 +1808,44 @@ func (b *Binance) GetAvailableTransferChains(ctx context.Context, cryptocurrency
}
return availableChains, nil
}
// FormatExchangeCurrency is a method that formats and returns a currency pair
// based on the user currency display preferences
// overrides default implementation to use optional delimiter
func (b *Binance) FormatExchangeCurrency(p currency.Pair, a asset.Item) (currency.Pair, error) {
pairFmt, err := b.GetPairFormat(a, true)
if err != nil {
return currency.Pair{}, err
}
if a == asset.USDTMarginedFutures {
return b.formatUSDTMarginedFuturesPair(p, pairFmt), nil
}
return p.Format(pairFmt.Delimiter, pairFmt.Uppercase), nil
}
// FormatSymbol formats the given pair to a string suitable for exchange API requests
// overrides default implementation to use optional delimiter
func (b *Binance) FormatSymbol(p currency.Pair, a asset.Item) (string, error) {
pairFmt, err := b.GetPairFormat(a, true)
if err != nil {
return p.String(), err
}
if a == asset.USDTMarginedFutures {
p = b.formatUSDTMarginedFuturesPair(p, pairFmt)
return p.String(), nil
}
return pairFmt.Format(p), nil
}
// formatUSDTMarginedFuturesPair Binance USDTMarginedFutures pairs have a delimiter
// only if the contract has an expiry date
func (b *Binance) formatUSDTMarginedFuturesPair(p currency.Pair, pairFmt currency.PairFormat) currency.Pair {
quote := p.Quote.String()
for _, c := range quote {
if c < '0' || c > '9' {
// character rune is alphabetic, cannot be expiring contract
return p.Format(pairFmt.Delimiter, pairFmt.Uppercase)
}
}
return p.Format(currency.UnderscoreDelimiter, pairFmt.Uppercase)
}

View File

@@ -554,36 +554,51 @@ type UFuturesExchangeInfo struct {
Limit int64 `json:"limit"`
RateLimitType string `json:"rateLimitType"`
} `json:"rateLimits"`
ServerTime int64 `json:"serverTime"`
Symbols []struct {
Symbol string `json:"symbol"`
Status string `json:"status"`
MaintenanceMarginPercent float64 `json:"maintMarginPercent,string"`
RequiredMarginPercent float64 `json:"requiredMarginPercent,string"`
BaseAsset string `json:"baseAsset"`
QuoteAsset string `json:"quoteAsset"`
PricePrecision int64 `json:"pricePrecision"`
QuantityPrecision int64 `json:"quantityPrecision"`
BaseAssetPrecision int64 `json:"baseAssetPrecision"`
QuotePrecision int64 `json:"quotePrecision"`
Filters []struct {
MinPrice float64 `json:"minPrice,string"`
MaxPrice float64 `json:"maxPrice,string"`
FilterType string `json:"filterType"`
TickSize float64 `json:"tickSize,string"`
StepSize float64 `json:"stepSize,string"`
MaxQty float64 `json:"maxQty,string"`
MinQty float64 `json:"minQty,string"`
Limit int64 `json:"limit"`
MultiplierDown float64 `json:"multiplierDown,string"`
MultiplierUp float64 `json:"multiplierUp,string"`
MultiplierDecimal float64 `json:"multiplierDecimal,string"`
Notional float64 `json:"notional,string"`
} `json:"filters"`
OrderTypes []string `json:"orderTypes"`
TimeInForce []string `json:"timeInForce"`
} `json:"symbols"`
Timezone string `json:"timezone"`
ServerTime int64 `json:"serverTime"`
Symbols []UFuturesSymbolInfo `json:"symbols"`
Timezone string `json:"timezone"`
}
// UFuturesSymbolInfo contains details of a currency symbol
// for a usdt margined future contract
type UFuturesSymbolInfo struct {
Symbol string `json:"symbol"`
Pair string `json:"pair"`
ContractType string `json:"contractType"`
DeliveryDate time.Time `json:"deliveryDate"`
OnboardDate time.Time `json:"onboardDate"`
Status string `json:"status"`
MaintenanceMarginPercent float64 `json:"maintMarginPercent,string"`
RequiredMarginPercent float64 `json:"requiredMarginPercent,string"`
BaseAsset string `json:"baseAsset"`
QuoteAsset string `json:"quoteAsset"`
MarginAsset string `json:"marginAsset"`
PricePrecision int64 `json:"pricePrecision"`
QuantityPrecision int64 `json:"quantityPrecision"`
BaseAssetPrecision int64 `json:"baseAssetPrecision"`
QuotePrecision int64 `json:"quotePrecision"`
UnderlyingType string `json:"underlyingType"`
UnderlyingSubType []string `json:"underlyingSubType"`
SettlePlan float64 `json:"settlePlan"`
TriggerProtect float64 `json:"triggerProtect,string"`
Filters []struct {
FilterType string `json:"filterType"`
MinPrice float64 `json:"minPrice,string"`
MaxPrice float64 `json:"maxPrice,string"`
TickSize float64 `json:"tickSize,string"`
StepSize float64 `json:"stepSize,string"`
MaxQty float64 `json:"maxQty,string"`
MinQty float64 `json:"minQty,string"`
Limit int64 `json:"limit"`
MultiplierDown float64 `json:"multiplierDown,string"`
MultiplierUp float64 `json:"multiplierUp,string"`
MultiplierDecimal float64 `json:"multiplierDecimal,string"`
Notional float64 `json:"notional,string"`
} `json:"filters"`
OrderTypes []string `json:"OrderType"`
TimeInForce []string `json:"timeInForce"`
LiquidationFee float64 `json:"liquidationFee,string"`
MarketTakeBound float64 `json:"marketTakeBound,string"`
}
// CExchangeInfo stores exchange info for cfutures

View File

@@ -423,3 +423,21 @@ func (a *wsListStatus) UnmarshalJSON(data []byte) error {
a.Data.TransactionTime = aux.Data.TransactionTime.Time()
return nil
}
// UnmarshalJSON deserialises the JSON info, including the timestamp
func (u *UFuturesSymbolInfo) UnmarshalJSON(data []byte) error {
type Alias UFuturesSymbolInfo
aux := &struct {
DeliveryDate binanceTime `json:"deliveryDate"`
OnboardDate binanceTime `json:"onboardDate"`
*Alias
}{
Alias: (*Alias)(u),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
u.DeliveryDate = aux.DeliveryDate.Time()
u.OnboardDate = aux.OnboardDate.Time()
return nil
}