orders: adds method to retrieve snapshot of order execution limit values (#946)

* orders: add method to Limit to retrieve order execution limit snapshots

* currency/btcmarkets: add error and update field name to standard

* linter: fix

* limts: don't return pointer

* limit: Add notes

* glorious: nits

* linter: fix

* limit: reinstate nil check

* exchanges: change field names to be more consistent (@thrasher-) suggestion

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
This commit is contained in:
Ryan O'Hara-Reid
2022-05-24 16:20:41 +10:00
committed by GitHub
parent 3a7837062c
commit 293d6104ed
14 changed files with 203 additions and 231 deletions

View File

@@ -485,7 +485,7 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange
return resp, err
}
if limits != nil {
if limits != (gctorder.MinMaxLevel{}) {
if !cfg.CurrencySettings[i].CanUseExchangeLimits {
log.Warnf(log.BackTester, "exchange %s order execution limits supported but disabled for %s %s, live results may differ",
cfg.CurrencySettings[i].ExchangeName,

View File

@@ -54,7 +54,7 @@ type Settings struct {
MinimumSlippageRate decimal.Decimal
MaximumSlippageRate decimal.Decimal
Limits *gctorder.Limits
Limits gctorder.MinMaxLevel
CanUseExchangeLimits bool
SkipCandleVolumeFitting bool
}

View File

@@ -11,14 +11,16 @@ import (
var (
// ErrCurrencyCodeEmpty defines an error if the currency code is empty
ErrCurrencyCodeEmpty = errors.New("currency code is empty")
errItemIsNil = errors.New("item is nil")
errItemIsEmpty = errors.New("item is empty")
errRoleUnset = errors.New("role unset")
// ErrCurrencyPairEmpty defines an error if the currency pair is empty
ErrCurrencyPairEmpty = errors.New("currency pair is empty")
// EMPTYCODE is an empty currency code
EMPTYCODE = Code{}
// EMPTYPAIR is an empty currency pair
EMPTYPAIR = Pair{}
errItemIsNil = errors.New("item is nil")
errItemIsEmpty = errors.New("item is empty")
errRoleUnset = errors.New("role unset")
)
func (r Role) String() string {

View File

@@ -1209,24 +1209,24 @@ func (b *Binance) FetchSpotExchangeLimits(ctx context.Context) ([]order.MinMaxLe
}
limits = append(limits, order.MinMaxLevel{
Pair: cp,
Asset: assets[z],
MinPrice: spot.Symbols[x].Filters[0].MinPrice,
MaxPrice: spot.Symbols[x].Filters[0].MaxPrice,
StepPrice: spot.Symbols[x].Filters[0].TickSize,
MultiplierUp: spot.Symbols[x].Filters[1].MultiplierUp,
MultiplierDown: spot.Symbols[x].Filters[1].MultiplierDown,
AveragePriceMinutes: spot.Symbols[x].Filters[1].AvgPriceMinutes,
MaxAmount: spot.Symbols[x].Filters[2].MaxQty,
MinAmount: spot.Symbols[x].Filters[2].MinQty,
StepAmount: spot.Symbols[x].Filters[2].StepSize,
MinNotional: spot.Symbols[x].Filters[3].MinNotional,
MaxIcebergParts: spot.Symbols[x].Filters[4].Limit,
MarketMinQty: spot.Symbols[x].Filters[5].MinQty,
MarketMaxQty: spot.Symbols[x].Filters[5].MaxQty,
MarketStepSize: spot.Symbols[x].Filters[5].StepSize,
MaxTotalOrders: spot.Symbols[x].Filters[6].MaxNumOrders,
MaxAlgoOrders: spot.Symbols[x].Filters[7].MaxNumAlgoOrders,
Pair: cp,
Asset: assets[z],
MinPrice: spot.Symbols[x].Filters[0].MinPrice,
MaxPrice: spot.Symbols[x].Filters[0].MaxPrice,
PriceStepIncrementSize: spot.Symbols[x].Filters[0].TickSize,
MultiplierUp: spot.Symbols[x].Filters[1].MultiplierUp,
MultiplierDown: spot.Symbols[x].Filters[1].MultiplierDown,
AveragePriceMinutes: spot.Symbols[x].Filters[1].AvgPriceMinutes,
MaxAmount: spot.Symbols[x].Filters[2].MaxQty,
MinAmount: spot.Symbols[x].Filters[2].MinQty,
AmountStepIncrementSize: spot.Symbols[x].Filters[2].StepSize,
MinNotional: spot.Symbols[x].Filters[3].MinNotional,
MaxIcebergParts: spot.Symbols[x].Filters[4].Limit,
MarketMinQty: spot.Symbols[x].Filters[5].MinQty,
MarketMaxQty: spot.Symbols[x].Filters[5].MaxQty,
MarketStepIncrementSize: spot.Symbols[x].Filters[5].StepSize,
MaxTotalOrders: spot.Symbols[x].Filters[6].MaxNumOrders,
MaxAlgoOrders: spot.Symbols[x].Filters[7].MaxNumAlgoOrders,
})
}
}

View File

@@ -1499,22 +1499,22 @@ func (b *Binance) FetchCoinMarginExchangeLimits(ctx context.Context) ([]order.Mi
}
limits = append(limits, order.MinMaxLevel{
Pair: cp,
Asset: asset.CoinMarginedFutures,
MinPrice: coinFutures.Symbols[x].Filters[0].MinPrice,
MaxPrice: coinFutures.Symbols[x].Filters[0].MaxPrice,
StepPrice: coinFutures.Symbols[x].Filters[0].TickSize,
MaxAmount: coinFutures.Symbols[x].Filters[1].MaxQty,
MinAmount: coinFutures.Symbols[x].Filters[1].MinQty,
StepAmount: coinFutures.Symbols[x].Filters[1].StepSize,
MarketMinQty: coinFutures.Symbols[x].Filters[2].MinQty,
MarketMaxQty: coinFutures.Symbols[x].Filters[2].MaxQty,
MarketStepSize: coinFutures.Symbols[x].Filters[2].StepSize,
MaxTotalOrders: coinFutures.Symbols[x].Filters[3].Limit,
MaxAlgoOrders: coinFutures.Symbols[x].Filters[4].Limit,
MultiplierUp: coinFutures.Symbols[x].Filters[5].MultiplierUp,
MultiplierDown: coinFutures.Symbols[x].Filters[5].MultiplierDown,
MultiplierDecimal: coinFutures.Symbols[x].Filters[5].MultiplierDecimal,
Pair: cp,
Asset: asset.CoinMarginedFutures,
MinPrice: coinFutures.Symbols[x].Filters[0].MinPrice,
MaxPrice: coinFutures.Symbols[x].Filters[0].MaxPrice,
PriceStepIncrementSize: coinFutures.Symbols[x].Filters[0].TickSize,
MaxAmount: coinFutures.Symbols[x].Filters[1].MaxQty,
MinAmount: coinFutures.Symbols[x].Filters[1].MinQty,
AmountStepIncrementSize: coinFutures.Symbols[x].Filters[1].StepSize,
MarketMinQty: coinFutures.Symbols[x].Filters[2].MinQty,
MarketMaxQty: coinFutures.Symbols[x].Filters[2].MaxQty,
MarketStepIncrementSize: coinFutures.Symbols[x].Filters[2].StepSize,
MaxTotalOrders: coinFutures.Symbols[x].Filters[3].Limit,
MaxAlgoOrders: coinFutures.Symbols[x].Filters[4].Limit,
MultiplierUp: coinFutures.Symbols[x].Filters[5].MultiplierUp,
MultiplierDown: coinFutures.Symbols[x].Filters[5].MultiplierDown,
MultiplierDecimal: coinFutures.Symbols[x].Filters[5].MultiplierDecimal,
})
}
return limits, nil

View File

@@ -2622,7 +2622,7 @@ func TestSetExchangeOrderExecutionLimits(t *testing.T) {
t.Fatal(err)
}
if limit == nil {
if limit == (order.MinMaxLevel{}) {
t.Fatal("exchange limit should be loaded")
}

View File

@@ -1169,23 +1169,23 @@ func (b *Binance) FetchUSDTMarginExchangeLimits(ctx context.Context) ([]order.Mi
}
limits = append(limits, order.MinMaxLevel{
Pair: cp,
Asset: asset.USDTMarginedFutures,
MinPrice: usdtFutures.Symbols[x].Filters[0].MinPrice,
MaxPrice: usdtFutures.Symbols[x].Filters[0].MaxPrice,
StepPrice: usdtFutures.Symbols[x].Filters[0].TickSize,
MaxAmount: usdtFutures.Symbols[x].Filters[1].MaxQty,
MinAmount: usdtFutures.Symbols[x].Filters[1].MinQty,
StepAmount: usdtFutures.Symbols[x].Filters[1].StepSize,
MarketMinQty: usdtFutures.Symbols[x].Filters[2].MinQty,
MarketMaxQty: usdtFutures.Symbols[x].Filters[2].MaxQty,
MarketStepSize: usdtFutures.Symbols[x].Filters[2].StepSize,
MaxTotalOrders: usdtFutures.Symbols[x].Filters[3].Limit,
MaxAlgoOrders: usdtFutures.Symbols[x].Filters[4].Limit,
MinNotional: usdtFutures.Symbols[x].Filters[5].Notional,
MultiplierUp: usdtFutures.Symbols[x].Filters[6].MultiplierUp,
MultiplierDown: usdtFutures.Symbols[x].Filters[6].MultiplierDown,
MultiplierDecimal: usdtFutures.Symbols[x].Filters[6].MultiplierDecimal,
Pair: cp,
Asset: asset.USDTMarginedFutures,
MinPrice: usdtFutures.Symbols[x].Filters[0].MinPrice,
MaxPrice: usdtFutures.Symbols[x].Filters[0].MaxPrice,
PriceStepIncrementSize: usdtFutures.Symbols[x].Filters[0].TickSize,
MaxAmount: usdtFutures.Symbols[x].Filters[1].MaxQty,
MinAmount: usdtFutures.Symbols[x].Filters[1].MinQty,
AmountStepIncrementSize: usdtFutures.Symbols[x].Filters[1].StepSize,
MarketMinQty: usdtFutures.Symbols[x].Filters[2].MinQty,
MarketMaxQty: usdtFutures.Symbols[x].Filters[2].MaxQty,
MarketStepIncrementSize: usdtFutures.Symbols[x].Filters[2].StepSize,
MaxTotalOrders: usdtFutures.Symbols[x].Filters[3].Limit,
MaxAlgoOrders: usdtFutures.Symbols[x].Filters[4].Limit,
MinNotional: usdtFutures.Symbols[x].Filters[5].Notional,
MultiplierUp: usdtFutures.Symbols[x].Filters[6].MultiplierUp,
MultiplierDown: usdtFutures.Symbols[x].Filters[6].MultiplierDown,
MultiplierDecimal: usdtFutures.Symbols[x].Filters[6].MultiplierDecimal,
})
}
return limits, nil

View File

@@ -1112,7 +1112,7 @@ func TestUpdateOrderExecutionLimits(t *testing.T) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if lim == nil {
if lim == (order.MinMaxLevel{}) {
t.Fatal("expected value return")
}
}

View File

@@ -1157,12 +1157,12 @@ func (b *BTCMarkets) UpdateOrderExecutionLimits(ctx context.Context, a asset.Ite
}
limits[x] = order.MinMaxLevel{
Pair: pair,
Asset: asset.Spot,
MinAmount: markets[x].MinOrderAmount,
MaxAmount: markets[x].MaxOrderAmount,
StepAmount: math.Pow(10, -markets[x].AmountDecimals),
StepPrice: math.Pow(10, -markets[x].PriceDecimals),
Pair: pair,
Asset: asset.Spot,
MinAmount: markets[x].MinOrderAmount,
MaxAmount: markets[x].MaxOrderAmount,
AmountStepIncrementSize: math.Pow(10, -markets[x].AmountDecimals),
PriceStepIncrementSize: math.Pow(10, -markets[x].PriceDecimals),
}
}
return b.LoadLimits(limits)

View File

@@ -1534,11 +1534,11 @@ func (f *FTX) FetchExchangeLimits(ctx context.Context) ([]order.MinMaxLevel, err
}
limits = append(limits, order.MinMaxLevel{
Pair: cp,
Asset: a,
StepPrice: data[x].PriceIncrement,
StepAmount: data[x].SizeIncrement,
MinAmount: data[x].MinProvideSize,
Pair: cp,
Asset: a,
PriceStepIncrementSize: data[x].PriceIncrement,
AmountStepIncrementSize: data[x].SizeIncrement,
MinAmount: data[x].MinProvideSize,
})
}
return limits, nil

View File

@@ -79,7 +79,7 @@ type IBotExchange interface {
FlushWebsocketChannels() error
AuthenticateWebsocket(ctx context.Context) error
GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (*order.Limits, error)
GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (order.MinMaxLevel, error)
CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType order.Type) error
UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error

View File

@@ -58,32 +58,32 @@ var (
// order size, order pricing, total notional values, total maximum orders etc
// for execution on an exchange.
type ExecutionLimits struct {
m map[asset.Item]map[*currency.Item]map[*currency.Item]*Limits
m map[asset.Item]map[*currency.Item]map[*currency.Item]MinMaxLevel
mtx sync.RWMutex
}
// MinMaxLevel defines the minimum and maximum parameters for a currency pair
// for outbound exchange execution
type MinMaxLevel struct {
Pair currency.Pair
Asset asset.Item
MinPrice float64
MaxPrice float64
StepPrice float64
MultiplierUp float64
MultiplierDown float64
MultiplierDecimal float64
AveragePriceMinutes int64
MinAmount float64
MaxAmount float64
StepAmount float64
MinNotional float64
MaxIcebergParts int64
MarketMinQty float64
MarketMaxQty float64
MarketStepSize float64
MaxTotalOrders int64
MaxAlgoOrders int64
Pair currency.Pair
Asset asset.Item
MinPrice float64
MaxPrice float64
PriceStepIncrementSize float64
MultiplierUp float64
MultiplierDown float64
MultiplierDecimal float64
AveragePriceMinutes int64
MinAmount float64
MaxAmount float64
AmountStepIncrementSize float64
MinNotional float64
MaxIcebergParts int64
MarketMinQty float64
MarketMaxQty float64
MarketStepIncrementSize float64
MaxTotalOrders int64
MaxAlgoOrders int64
}
// LoadLimits loads all limits levels into memory
@@ -94,7 +94,7 @@ func (e *ExecutionLimits) LoadLimits(levels []MinMaxLevel) error {
e.mtx.Lock()
defer e.mtx.Unlock()
if e.m == nil {
e.m = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Limits)
e.m = make(map[asset.Item]map[*currency.Item]map[*currency.Item]MinMaxLevel)
}
for x := range levels {
@@ -105,22 +105,20 @@ func (e *ExecutionLimits) LoadLimits(levels []MinMaxLevel) error {
}
m1, ok := e.m[levels[x].Asset]
if !ok {
m1 = make(map[*currency.Item]map[*currency.Item]*Limits)
m1 = make(map[*currency.Item]map[*currency.Item]MinMaxLevel)
e.m[levels[x].Asset] = m1
}
if levels[x].Pair.IsEmpty() {
return currency.ErrCurrencyPairEmpty
}
m2, ok := m1[levels[x].Pair.Base.Item]
if !ok {
m2 = make(map[*currency.Item]*Limits)
m2 = make(map[*currency.Item]MinMaxLevel)
m1[levels[x].Pair.Base.Item] = m2
}
limit, ok := m2[levels[x].Pair.Quote.Item]
if !ok {
limit = new(Limits)
m2[levels[x].Pair.Quote.Item] = limit
}
if levels[x].MinPrice > 0 &&
levels[x].MaxPrice > 0 &&
levels[x].MinPrice > levels[x].MaxPrice {
@@ -142,50 +140,34 @@ func (e *ExecutionLimits) LoadLimits(levels []MinMaxLevel) error {
levels[x].MinAmount,
levels[x].MaxAmount)
}
limit.m.Lock()
limit.minPrice = levels[x].MinPrice
limit.maxPrice = levels[x].MaxPrice
limit.stepIncrementSizePrice = levels[x].StepPrice
limit.minAmount = levels[x].MinAmount
limit.maxAmount = levels[x].MaxAmount
limit.stepIncrementSizeAmount = levels[x].StepAmount
limit.minNotional = levels[x].MinNotional
limit.multiplierUp = levels[x].MultiplierUp
limit.multiplierDown = levels[x].MultiplierDown
limit.averagePriceMinutes = levels[x].AveragePriceMinutes
limit.maxIcebergParts = levels[x].MaxIcebergParts
limit.marketMinQty = levels[x].MarketMinQty
limit.marketMaxQty = levels[x].MarketMaxQty
limit.marketStepIncrementSize = levels[x].MarketStepSize
limit.maxTotalOrders = levels[x].MaxTotalOrders
limit.maxAlgoOrders = levels[x].MaxAlgoOrders
limit.m.Unlock()
m2[levels[x].Pair.Quote.Item] = levels[x]
}
return nil
}
// GetOrderExecutionLimits returns the exchange limit parameters for a currency
func (e *ExecutionLimits) GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (*Limits, error) {
func (e *ExecutionLimits) GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (MinMaxLevel, error) {
e.mtx.RLock()
defer e.mtx.RUnlock()
if e.m == nil {
return nil, ErrExchangeLimitNotLoaded
return MinMaxLevel{}, ErrExchangeLimitNotLoaded
}
m1, ok := e.m[a]
if !ok {
return nil, errExchangeLimitAsset
return MinMaxLevel{}, errExchangeLimitAsset
}
m2, ok := m1[cp.Base.Item]
if !ok {
return nil, errExchangeLimitBase
return MinMaxLevel{}, errExchangeLimitBase
}
limit, ok := m2[cp.Quote.Item]
if !ok {
return nil, errExchangeLimitQuote
return MinMaxLevel{}, errExchangeLimitQuote
}
return limit, nil
@@ -225,136 +207,111 @@ func (e *ExecutionLimits) CheckOrderExecutionLimits(a asset.Item, cp currency.Pa
return nil
}
// Limits defines total limit values for an associated currency to be checked
// before execution on an exchange
type Limits struct {
minPrice float64
maxPrice float64
stepIncrementSizePrice float64
minAmount float64
maxAmount float64
stepIncrementSizeAmount float64
minNotional float64
multiplierUp float64
multiplierDown float64
averagePriceMinutes int64
maxIcebergParts int64
marketMinQty float64
marketMaxQty float64
marketStepIncrementSize float64
maxTotalOrders int64
maxAlgoOrders int64
m sync.RWMutex
}
// Conforms checks outbound parameters
func (l *Limits) Conforms(price, amount float64, orderType Type) error {
if l == nil {
// For when we return a nil pointer we can assume there's nothing to
// check
func (m *MinMaxLevel) Conforms(price, amount float64, orderType Type) error {
if m == nil {
return nil
}
l.m.RLock()
defer l.m.RUnlock()
if l.minAmount != 0 && amount < l.minAmount {
if m.MinAmount != 0 && amount < m.MinAmount {
return fmt.Errorf("%w min: %.8f supplied %.8f",
ErrAmountBelowMin,
l.minAmount,
m.MinAmount,
amount)
}
if l.maxAmount != 0 && amount > l.maxAmount {
if m.MaxAmount != 0 && amount > m.MaxAmount {
return fmt.Errorf("%w min: %.8f supplied %.8f",
ErrAmountExceedsMax,
l.maxAmount,
m.MaxAmount,
amount)
}
if l.stepIncrementSizeAmount != 0 {
if m.AmountStepIncrementSize != 0 {
dAmount := decimal.NewFromFloat(amount)
dMinAmount := decimal.NewFromFloat(l.minAmount)
dStep := decimal.NewFromFloat(l.stepIncrementSizeAmount)
dMinAmount := decimal.NewFromFloat(m.MinAmount)
dStep := decimal.NewFromFloat(m.AmountStepIncrementSize)
if !dAmount.Sub(dMinAmount).Mod(dStep).IsZero() {
return fmt.Errorf("%w stepSize: %.8f supplied %.8f",
ErrAmountExceedsStep,
l.stepIncrementSizeAmount,
m.AmountStepIncrementSize,
amount)
}
}
// Multiplier checking not done due to the fact we need coherence with the
// last average price (TODO)
// l.multiplierUp will be used to determine how far our price can go up
// l.multiplierDown will be used to determine how far our price can go down
// l.averagePriceMinutes will be used to determine mean over this period
// m.multiplierUp will be used to determine how far our price can go up
// m.multiplierDown will be used to determine how far our price can go down
// m.averagePriceMinutes will be used to determine mean over this period
// Max iceberg parts checking not done as we do not have that
// functionality yet (TODO)
// l.maxIcebergParts // How many components in an iceberg order
// m.maxIcebergParts // How many components in an iceberg order
// Max total orders not done due to order manager limitations (TODO)
// l.maxTotalOrders
// m.maxTotalOrders
// Max algo orders not done due to order manager limitations (TODO)
// l.maxAlgoOrders
// m.maxAlgoOrders
// If order type is Market we do not need to do price checks
if orderType != Market {
if l.minPrice != 0 && price < l.minPrice {
if m.MinPrice != 0 && price < m.MinPrice {
return fmt.Errorf("%w min: %.8f supplied %.8f",
ErrPriceBelowMin,
l.minPrice,
m.MinPrice,
price)
}
if l.maxPrice != 0 && price > l.maxPrice {
if m.MaxPrice != 0 && price > m.MaxPrice {
return fmt.Errorf("%w max: %.8f supplied %.8f",
ErrPriceExceedsMax,
l.maxPrice,
m.MaxPrice,
price)
}
if l.minNotional != 0 && (amount*price) < l.minNotional {
if m.MinNotional != 0 && (amount*price) < m.MinNotional {
return fmt.Errorf("%w minimum notional: %.8f value of order %.8f",
ErrNotionalValue,
l.minNotional,
m.MinNotional,
amount*price)
}
if l.stepIncrementSizePrice != 0 {
if m.PriceStepIncrementSize != 0 {
dPrice := decimal.NewFromFloat(price)
dMinPrice := decimal.NewFromFloat(l.minPrice)
dStep := decimal.NewFromFloat(l.stepIncrementSizePrice)
dMinPrice := decimal.NewFromFloat(m.MinPrice)
dStep := decimal.NewFromFloat(m.PriceStepIncrementSize)
if !dPrice.Sub(dMinPrice).Mod(dStep).IsZero() {
return fmt.Errorf("%w stepSize: %.8f supplied %.8f",
ErrPriceExceedsStep,
l.stepIncrementSizePrice,
m.PriceStepIncrementSize,
price)
}
}
return nil
}
if l.marketMinQty != 0 &&
l.minAmount < l.marketMinQty &&
amount < l.marketMinQty {
if m.MarketMinQty != 0 &&
m.MinAmount < m.MarketMinQty &&
amount < m.MarketMinQty {
return fmt.Errorf("%w min: %.8f supplied %.8f",
ErrMarketAmountBelowMin,
l.marketMinQty,
m.MarketMinQty,
amount)
}
if l.marketMaxQty != 0 &&
l.maxAmount > l.marketMaxQty &&
amount > l.marketMaxQty {
if m.MarketMaxQty != 0 &&
m.MaxAmount > m.MarketMaxQty &&
amount > m.MarketMaxQty {
return fmt.Errorf("%w max: %.8f supplied %.8f",
ErrMarketAmountExceedsMax,
l.marketMaxQty,
m.MarketMaxQty,
amount)
}
if l.marketStepIncrementSize != 0 && l.stepIncrementSizeAmount != l.marketStepIncrementSize {
if m.MarketStepIncrementSize != 0 &&
m.AmountStepIncrementSize != m.MarketStepIncrementSize {
dAmount := decimal.NewFromFloat(amount)
dMinMAmount := decimal.NewFromFloat(l.marketMinQty)
dStep := decimal.NewFromFloat(l.marketStepIncrementSize)
dMinMAmount := decimal.NewFromFloat(m.MarketMinQty)
dStep := decimal.NewFromFloat(m.MarketStepIncrementSize)
if !dAmount.Sub(dMinMAmount).Mod(dStep).IsZero() {
return fmt.Errorf("%w stepSize: %.8f supplied %.8f",
ErrMarketAmountExceedsStep,
l.marketStepIncrementSize,
m.MarketStepIncrementSize,
amount)
}
}
@@ -362,13 +319,12 @@ func (l *Limits) Conforms(price, amount float64, orderType Type) error {
}
// ConformToDecimalAmount (POC) conforms amount to its amount interval
func (l *Limits) ConformToDecimalAmount(amount decimal.Decimal) decimal.Decimal {
if l == nil {
func (m *MinMaxLevel) ConformToDecimalAmount(amount decimal.Decimal) decimal.Decimal {
if m == nil {
return amount
}
l.m.Lock()
defer l.m.Unlock()
dStep := decimal.NewFromFloat(l.stepIncrementSizeAmount)
dStep := decimal.NewFromFloat(m.AmountStepIncrementSize)
if dStep.IsZero() || amount.Equal(dStep) {
return amount
}
@@ -382,25 +338,22 @@ func (l *Limits) ConformToDecimalAmount(amount decimal.Decimal) decimal.Decimal
}
// ConformToAmount (POC) conforms amount to its amount interval
func (l *Limits) ConformToAmount(amount float64) float64 {
if l == nil {
// For when we return a nil pointer we can assume there's nothing to
// check
return amount
}
l.m.Lock()
defer l.m.Unlock()
if l.stepIncrementSizeAmount == 0 || amount == l.stepIncrementSizeAmount {
func (m *MinMaxLevel) ConformToAmount(amount float64) float64 {
if m == nil {
return amount
}
if amount < l.stepIncrementSizeAmount {
if m.AmountStepIncrementSize == 0 || amount == m.AmountStepIncrementSize {
return amount
}
if amount < m.AmountStepIncrementSize {
return 0
}
// Convert floats to decimal types
dAmount := decimal.NewFromFloat(amount)
dStep := decimal.NewFromFloat(l.stepIncrementSizeAmount)
dStep := decimal.NewFromFloat(m.AmountStepIncrementSize)
// derive modulus
mod := dAmount.Mod(dStep)
// subtract modulus to get the floor

View File

@@ -9,9 +9,11 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
var btcusd = currency.NewPair(currency.BTC, currency.USD)
var ltcusd = currency.NewPair(currency.LTC, currency.USD)
var btcltc = currency.NewPair(currency.BTC, currency.LTC)
var (
btcusd = currency.NewPair(currency.BTC, currency.USD)
ltcusd = currency.NewPair(currency.LTC, currency.USD)
btcltc = currency.NewPair(currency.BTC, currency.LTC)
)
func TestLoadLimits(t *testing.T) {
t.Parallel()
@@ -37,6 +39,21 @@ func TestLoadLimits(t *testing.T) {
err)
}
invalidPairLoading := []MinMaxLevel{
{
Asset: asset.Spot,
MinPrice: 100000,
MaxPrice: 1000000,
MinAmount: 1,
MaxAmount: 10,
},
}
err = e.LoadLimits(invalidPairLoading)
if !errors.Is(err, currency.ErrCurrencyPairEmpty) {
t.Fatalf("expected error %v but received %v", currency.ErrCurrencyPairEmpty, err)
}
newLimits := []MinMaxLevel{
{
Pair: btcusd,
@@ -168,10 +185,10 @@ func TestGetOrderExecutionLimits(t *testing.T) {
t.Fatalf("expected error %v but received %v", nil, err)
}
if tt.maxAmount != newLimits[0].MaxAmount ||
tt.minAmount != newLimits[0].MinAmount ||
tt.maxPrice != newLimits[0].MaxPrice ||
tt.minPrice != newLimits[0].MinPrice {
if tt.MaxAmount != newLimits[0].MaxAmount ||
tt.MinAmount != newLimits[0].MinAmount ||
tt.MaxPrice != newLimits[0].MaxPrice ||
tt.MinPrice != newLimits[0].MinPrice {
t.Fatal("unexpected values")
}
}
@@ -248,14 +265,14 @@ func TestCheckLimit(t *testing.T) {
func TestConforms(t *testing.T) {
t.Parallel()
var tt *Limits
var tt MinMaxLevel
err := tt.Conforms(0, 0, Limit)
if err != nil {
t.Fatal(err)
}
tt = &Limits{
minNotional: 100,
tt = MinMaxLevel{
MinNotional: 100,
}
err = tt.Conforms(1, 1, Limit)
@@ -268,7 +285,7 @@ func TestConforms(t *testing.T) {
t.Fatalf("expected error %v but received %v", nil, err)
}
tt.stepIncrementSizePrice = 0.001
tt.PriceStepIncrementSize = 0.001
err = tt.Conforms(200.0001, .5, Limit)
if !errors.Is(err, ErrPriceExceedsStep) {
t.Fatalf("expected error %v but received %v", ErrPriceExceedsStep, err)
@@ -278,7 +295,7 @@ func TestConforms(t *testing.T) {
t.Fatalf("expected error %v but received %v", nil, err)
}
tt.stepIncrementSizeAmount = 0.001
tt.AmountStepIncrementSize = 0.001
err = tt.Conforms(200, .0002, Limit)
if !errors.Is(err, ErrAmountExceedsStep) {
t.Fatalf("expected error %v but received %v", ErrAmountExceedsStep, err)
@@ -288,10 +305,10 @@ func TestConforms(t *testing.T) {
t.Fatalf("expected error %v but received %v", nil, err)
}
tt.minAmount = 1
tt.maxAmount = 10
tt.marketMinQty = 1.1
tt.marketMaxQty = 9.9
tt.MinAmount = 1
tt.MaxAmount = 10
tt.MarketMinQty = 1.1
tt.MarketMaxQty = 9.9
err = tt.Conforms(200000, 1, Market)
if !errors.Is(err, ErrMarketAmountBelowMin) {
@@ -303,12 +320,12 @@ func TestConforms(t *testing.T) {
t.Fatalf("expected error %v but received: %v", ErrMarketAmountExceedsMax, err)
}
tt.marketStepIncrementSize = 10
tt.MarketStepIncrementSize = 10
err = tt.Conforms(200000, 9.1, Market)
if !errors.Is(err, ErrMarketAmountExceedsStep) {
t.Fatalf("expected error %v but received: %v", ErrMarketAmountExceedsStep, err)
}
tt.marketStepIncrementSize = 1
tt.MarketStepIncrementSize = 1
err = tt.Conforms(200000, 9.1, Market)
if !errors.Is(err, nil) {
t.Fatalf("expected error %v but received: %v", nil, err)
@@ -317,19 +334,19 @@ func TestConforms(t *testing.T) {
func TestConformToDecimalAmount(t *testing.T) {
t.Parallel()
var tt *Limits
var tt MinMaxLevel
if !tt.ConformToDecimalAmount(decimal.NewFromFloat(1.001)).Equal(decimal.NewFromFloat(1.001)) {
t.Fatal("value should not be changed")
}
tt = &Limits{}
tt = MinMaxLevel{}
val := tt.ConformToDecimalAmount(decimal.NewFromInt(1))
if !val.Equal(decimal.NewFromInt(1)) { // If there is no step amount set this should not change
// the inputted amount
t.Fatal("unexpected amount")
}
tt.stepIncrementSizeAmount = 0.001
tt.AmountStepIncrementSize = 0.001
val = tt.ConformToDecimalAmount(decimal.NewFromFloat(1.001))
if !val.Equal(decimal.NewFromFloat(1.001)) {
t.Error("unexpected amount", val)
@@ -345,7 +362,7 @@ func TestConformToDecimalAmount(t *testing.T) {
t.Error("unexpected amount", val)
}
tt.stepIncrementSizeAmount = 100
tt.AmountStepIncrementSize = 100
val = tt.ConformToDecimalAmount(decimal.NewFromInt(100))
if !val.Equal(decimal.NewFromInt(100)) {
t.Fatal("unexpected amount", val)
@@ -363,19 +380,19 @@ func TestConformToDecimalAmount(t *testing.T) {
func TestConformToAmount(t *testing.T) {
t.Parallel()
var tt *Limits
var tt MinMaxLevel
if tt.ConformToAmount(1.001) != 1.001 {
t.Fatal("value should not be changed")
}
tt = &Limits{}
tt = MinMaxLevel{}
val := tt.ConformToAmount(1)
if val != 1 { // If there is no step amount set this should not change
// the inputted amount
t.Fatal("unexpected amount")
}
tt.stepIncrementSizeAmount = 0.001
tt.AmountStepIncrementSize = 0.001
val = tt.ConformToAmount(1.001)
if val != 1.001 {
t.Error("unexpected amount", val)
@@ -391,7 +408,7 @@ func TestConformToAmount(t *testing.T) {
t.Error("unexpected amount", val)
}
tt.stepIncrementSizeAmount = 100
tt.AmountStepIncrementSize = 100
val = tt.ConformToAmount(100)
if val != 100 {
t.Fatal("unexpected amount", val)

View File

@@ -274,8 +274,8 @@ func (c *CustomEx) AuthenticateWebsocket(ctx context.Context) error {
return nil
}
func (c *CustomEx) GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (*order.Limits, error) {
return nil, nil
func (c *CustomEx) GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (order.MinMaxLevel, error) {
return order.MinMaxLevel{}, nil
}
func (c *CustomEx) CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType order.Type) error {