mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
BTSE: Various fixes (#1550)
* Common: DriveBy sanitisation of vars * BTSE: Unify to exchange OrderLimits * BTSE: Remove SeedAssets and test GetOrderExcutionLimit * BTSE: Fix handling for K_* pairs In addition to the M_ pairs we previously handled, BTSE has now introduced K_* pairs (K_SATS-USD*). This change moves the handling over to look for an exponent, and moves the filtering to happen on all market data. The original MarketSummary is still availiable, but I can't see any of our current use-cases wanting to get the unfiltered list * BTSE: Fix marketSummary futures field BTSE returns no futures field for futures api marketInfo, and the documentation for the futures api shows returning false. When we know we asked for futures, it makes the data flow much saner if we can trust this field and not have to track what asset we asked for * BTSE: Abstract marketPair.Pair() * BTSE: Fix UpdateTicker symbol format
This commit is contained in:
@@ -7,12 +7,10 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Public errors related to assets
|
||||
var (
|
||||
// ErrNotSupported is an error for an unsupported asset type
|
||||
ErrNotSupported = errors.New("unsupported asset type")
|
||||
// ErrNotEnabled is an error for an asset not enabled
|
||||
ErrNotEnabled = errors.New("asset type not enabled")
|
||||
// ErrInvalidAsset is returned when the assist isn't valid
|
||||
ErrNotEnabled = errors.New("asset type not enabled")
|
||||
ErrInvalidAsset = errors.New("asset is invalid")
|
||||
)
|
||||
|
||||
|
||||
@@ -68,8 +68,9 @@ func (b *BTSE) FetchFundingHistory(ctx context.Context, symbol string) (map[stri
|
||||
return resp, b.SendHTTPRequest(ctx, exchange.RestFutures, http.MethodGet, btseFuturesFunding+params.Encode(), &resp, false, queryFunc)
|
||||
}
|
||||
|
||||
// GetMarketSummary stores market summary data
|
||||
func (b *BTSE) GetMarketSummary(ctx context.Context, symbol string, spot bool) (MarketSummary, error) {
|
||||
// GetRawMarketSummary returns an unfiltered list of market pairs
|
||||
// Consider using the wrapper function GetMarketSummary instead
|
||||
func (b *BTSE) GetRawMarketSummary(ctx context.Context, symbol string, spot bool) (MarketSummary, error) {
|
||||
var m MarketSummary
|
||||
path := btseMarketOverview
|
||||
if symbol != "" {
|
||||
@@ -619,23 +620,7 @@ func parseOrderTime(timeStr string) (time.Time, error) {
|
||||
return time.Parse(time.DateTime, timeStr)
|
||||
}
|
||||
|
||||
// MillionPairs returns a map of symbol names which have a IsMillion equivalent
|
||||
func (m *MarketSummary) MillionPairs() map[string]bool {
|
||||
pairs := map[string]bool{}
|
||||
for _, s := range *m {
|
||||
if s.Active && s.HasLiquidity() && s.IsMillions() {
|
||||
pairs[strings.TrimPrefix(s.Symbol, "M_")] = true
|
||||
}
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
// HasLiquidity returns if a market pair has a bid or ask != 0
|
||||
func (m *MarketPair) HasLiquidity() bool {
|
||||
return m.LowestAsk != 0 || m.HighestBid != 0
|
||||
}
|
||||
|
||||
// IsMillions returns if a market pair represents a million of Base / Quote
|
||||
func (m *MarketPair) IsMillions() bool {
|
||||
return strings.HasPrefix(m.Symbol, "M_")
|
||||
}
|
||||
|
||||
@@ -144,6 +144,10 @@ func TestFormatExchangeKlineInterval(t *testing.T) {
|
||||
|
||||
func TestGetHistoricCandles(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := b.Requester
|
||||
b := new(BTSE) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
|
||||
require.NoError(t, testexch.Setup(b), "Test exchange Setup must not error")
|
||||
b.Requester = r
|
||||
start := time.Now().AddDate(0, 0, -3)
|
||||
_, err := b.GetHistoricCandles(context.Background(), spotPair, asset.Spot, kline.OneHour, start, time.Now())
|
||||
assert.NoError(t, err, "GetHistoricCandles should not error")
|
||||
@@ -154,6 +158,10 @@ func TestGetHistoricCandles(t *testing.T) {
|
||||
|
||||
func TestGetHistoricCandlesExtended(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := b.Requester
|
||||
b := new(BTSE) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
|
||||
require.NoError(t, testexch.Setup(b), "Test exchange Setup must not error")
|
||||
b.Requester = r
|
||||
err := b.CurrencyPairs.StorePairs(asset.Futures, currency.Pairs{futuresPair}, true)
|
||||
assert.NoError(t, err, "StorePairs should not error")
|
||||
|
||||
@@ -542,80 +550,28 @@ func TestMatchType(t *testing.T) {
|
||||
assert.True(t, ret, "matchType should match")
|
||||
}
|
||||
|
||||
func TestSeedOrderSizeLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := b.seedOrderSizeLimits(context.Background())
|
||||
assert.NoError(t, err, "seedOrderSizeLimits should not error")
|
||||
}
|
||||
|
||||
func TestOrderSizeLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
seedOrderSizeLimitMap()
|
||||
_, ok := OrderSizeLimits(spotPair.String())
|
||||
assert.True(t, ok, "OrderSizeLimits should find BTC-USD")
|
||||
|
||||
_, ok = OrderSizeLimits("XRP-GARBAGE")
|
||||
assert.False(t, ok, "OrderSizeLimits should not find XRP-GARBAGE until the next bull market")
|
||||
}
|
||||
|
||||
func seedOrderSizeLimitMap() {
|
||||
testOrderSizeLimits := []struct {
|
||||
name string
|
||||
o OrderSizeLimit
|
||||
}{
|
||||
{
|
||||
name: "XRP-USD",
|
||||
o: OrderSizeLimit{
|
||||
MinSizeIncrement: 1,
|
||||
MinOrderSize: 1,
|
||||
MaxOrderSize: 1000000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LTC-USD",
|
||||
o: OrderSizeLimit{
|
||||
MinSizeIncrement: 0.01,
|
||||
MinOrderSize: 0.01,
|
||||
MaxOrderSize: 5000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BTC-USD",
|
||||
o: OrderSizeLimit{
|
||||
MinSizeIncrement: 0.0001,
|
||||
MinOrderSize: 1,
|
||||
MaxOrderSize: 1000000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
orderSizeLimitMap.Range(func(key interface{}, _ interface{}) bool {
|
||||
orderSizeLimitMap.Delete(key)
|
||||
return true
|
||||
})
|
||||
|
||||
for x := range testOrderSizeLimits {
|
||||
orderSizeLimitMap.Store(testOrderSizeLimits[x].name, testOrderSizeLimits[x].o)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithinLimits(t *testing.T) {
|
||||
// TestUpdateOrderExecutionLimits exercises UpdateOrderExecutionLimits
|
||||
func TestUpdateOrderExecutionLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
testexch.UpdatePairsOnce(t, b)
|
||||
seedOrderSizeLimitMap()
|
||||
p, _ := currency.NewPairDelimiter("XRP-USD", "-")
|
||||
assert.NoError(t, b.withinLimits(p, 1.0), "withinLimits should not error")
|
||||
assert.NoError(t, b.withinLimits(p, 5.0000001), "withinLimits should not error")
|
||||
assert.NoError(t, b.withinLimits(p, 100), "withinLimits should not error")
|
||||
assert.NoError(t, b.withinLimits(p, 10.1), "withinLimits should not error")
|
||||
for _, a := range b.GetAssetTypes(false) {
|
||||
err := b.UpdateOrderExecutionLimits(context.Background(), a)
|
||||
require.NoErrorf(t, err, "UpdateOrderExecutionLimits must not error for %s", a)
|
||||
|
||||
p.Base = currency.LTC
|
||||
assert.NoError(t, b.withinLimits(p, 10), "withinLimits should not error")
|
||||
assert.ErrorIs(t, b.withinLimits(p, 0.009), order.ErrAmountBelowMin, "withinLimits should error correctly")
|
||||
pairs, err := b.GetAvailablePairs(a)
|
||||
require.NoErrorf(t, err, "GetAvailablePairs must not error for %s", a)
|
||||
require.NotEmpty(t, pairs, "GetAvailablePairs must return some pairs")
|
||||
|
||||
p.Base = currency.BTC
|
||||
assert.NoError(t, b.withinLimits(p, 10), "withinLimits should not error")
|
||||
assert.ErrorIs(t, b.withinLimits(p, 0.001), order.ErrAmountBelowMin, "withinLimits should error correctly")
|
||||
for _, p := range pairs {
|
||||
limits, err := b.GetOrderExecutionLimits(a, p)
|
||||
require.NoErrorf(t, err, "GetOrderExecutionLimits must not error for %s %s", a, p)
|
||||
assert.Positivef(t, limits.MinimumBaseAmount, "MinimumBaseAmount must be positive for %s %s", a, p)
|
||||
assert.Positivef(t, limits.MaximumBaseAmount, "MaximumBaseAmount must be positive for %s %s", a, p)
|
||||
assert.Positivef(t, limits.AmountStepIncrementSize, "AmountStepIncrementSize must be positive for %s %s", a, p)
|
||||
assert.Positivef(t, limits.MinPrice, "MinPrice must be positive for %s %s", a, p)
|
||||
assert.Positivef(t, limits.PriceStepIncrementSize, "PriceStepIncrementSize must be positive for %s %s", a, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRecentTrades(t *testing.T) {
|
||||
@@ -718,7 +674,11 @@ func TestIsPerpetualFutureCurrency(t *testing.T) {
|
||||
|
||||
func TestGetOpenInterest(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := b.Requester
|
||||
b := new(BTSE) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
|
||||
require.NoError(t, testexch.Setup(b), "Test exchange Setup must not error")
|
||||
testexch.UpdatePairsOnce(t, b)
|
||||
b.Requester = r
|
||||
cp1 := currency.NewPair(currency.BTC, currency.PFC)
|
||||
cp2 := currency.NewPair(currency.ETH, currency.PFC)
|
||||
sharedtestvalues.SetupCurrencyPairsForExchangeAsset(t, b, asset.Futures, futuresPair, cp1, cp2)
|
||||
@@ -769,3 +729,23 @@ func TestGetCurrencyTradeURL(t *testing.T) {
|
||||
assert.NotEmpty(t, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStripExponent exercises StripExponent
|
||||
func TestStripExponent(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := (&MarketPair{Symbol: "BTC-ETH"}).StripExponent()
|
||||
assert.NoError(t, err, "Should not error on a symbol without exponent")
|
||||
assert.Empty(t, s, "Should return an empty symbol without exponent")
|
||||
|
||||
for _, p := range []string{"B", "M", "K"} {
|
||||
s, err = (&MarketPair{Symbol: p + "_BTC-ETH"}).StripExponent()
|
||||
assert.NoError(t, err, "Should not error on a symbol with exponent")
|
||||
assert.Equal(t, "BTC-ETH", s, "Should return the symbol without the exponent")
|
||||
}
|
||||
|
||||
_, err = (&MarketPair{Symbol: "Z_BTC-ETH"}).StripExponent()
|
||||
assert.ErrorIs(t, err, errInvalidPairSymbol, "Should error on a symbol with unknown exponent")
|
||||
|
||||
_, err = (&MarketPair{Symbol: "M_BTC_ETH"}).StripExponent()
|
||||
assert.ErrorIs(t, err, errInvalidPairSymbol, "Should error on a symbol with too many underscores")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package btse
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -354,16 +353,6 @@ type ErrorResponse struct {
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// OrderSizeLimit holds accepted minimum, maximum, and size increment when submitting new orders
|
||||
type OrderSizeLimit struct {
|
||||
MinOrderSize float64
|
||||
MaxOrderSize float64
|
||||
MinSizeIncrement float64
|
||||
}
|
||||
|
||||
// orderSizeLimitMap map of OrderSizeLimit per currency
|
||||
var orderSizeLimitMap sync.Map
|
||||
|
||||
// WsSubscriptionAcknowledgement contains successful subscription messages
|
||||
type WsSubscriptionAcknowledgement struct {
|
||||
Channel []string `json:"channel"`
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -33,6 +32,11 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
|
||||
)
|
||||
|
||||
// Private Errors
|
||||
var (
|
||||
errInvalidPairSymbol = errors.New("invalid currency pair symbol")
|
||||
)
|
||||
|
||||
// SetDefaults sets the basic defaults for BTSE
|
||||
func (b *BTSE) SetDefaults() {
|
||||
b.Name = "BTSE"
|
||||
@@ -193,11 +197,6 @@ func (b *BTSE) Setup(exch *config.Exchange) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.seedOrderSizeLimits(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.Websocket.SetupNewConnection(stream.ConnectionSetup{
|
||||
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
||||
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
|
||||
@@ -210,41 +209,16 @@ func (b *BTSE) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.P
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var errs error
|
||||
pairs := make(currency.Pairs, 0, len(m))
|
||||
mPairs := m.MillionPairs()
|
||||
for _, l := range m {
|
||||
if !l.Active || !l.HasLiquidity() ||
|
||||
(a == asset.Spot && !l.IsMarketOpenToSpot) { // Skip OTC assets only tradable on web UI
|
||||
continue
|
||||
}
|
||||
if mPairs[l.Symbol] {
|
||||
// BTSE lists M_ symbols for very small pairs, in millions. For those listings, we want to take the M_ listing in preference
|
||||
// to the native listing, since they're often going to appear as locked markets due to size (bid == ask, e.g. 0.0000000003)
|
||||
continue
|
||||
}
|
||||
baseCurr := l.Base
|
||||
var quoteCurr string
|
||||
if a == asset.Futures {
|
||||
s := strings.Split(l.Symbol, l.Base) // e.g. RUNEPFC for RUNE-USD futures pair
|
||||
if len(s) <= 1 {
|
||||
continue
|
||||
}
|
||||
quoteCurr = s[1]
|
||||
for _, marketInfo := range m {
|
||||
if pair, err := marketInfo.Pair(); err != nil {
|
||||
errs = common.AppendError(errs, fmt.Errorf("%s: %w", marketInfo.Symbol, err))
|
||||
} else {
|
||||
s := strings.Split(l.Symbol, currency.DashDelimiter)
|
||||
if len(s) != 2 {
|
||||
continue
|
||||
}
|
||||
baseCurr = s[0]
|
||||
quoteCurr = s[1]
|
||||
pairs = append(pairs, pair)
|
||||
}
|
||||
pair, err := currency.NewPairFromStrings(baseCurr, quoteCurr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pairs = append(pairs, pair)
|
||||
}
|
||||
return pairs, nil
|
||||
return pairs, errs
|
||||
}
|
||||
|
||||
// UpdateTradablePairs updates the exchanges available pairs and stores
|
||||
@@ -305,7 +279,11 @@ func (b *BTSE) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item)
|
||||
if !b.SupportsAsset(a) {
|
||||
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
|
||||
}
|
||||
ticks, err := b.GetMarketSummary(ctx, p.String(), a == asset.Spot)
|
||||
symbol, err := b.FormatSymbol(p, a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ticks, err := b.GetMarketSummary(ctx, symbol, a == asset.Spot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -458,23 +436,6 @@ func (b *BTSE) GetAccountFundingHistory(_ context.Context) ([]exchange.FundingHi
|
||||
return nil, common.ErrFunctionNotSupported
|
||||
}
|
||||
|
||||
func (b *BTSE) withinLimits(pair currency.Pair, amount float64) error {
|
||||
val, found := OrderSizeLimits(pair.String())
|
||||
if !found {
|
||||
return fmt.Errorf("%w for pair %v", order.ErrExchangeLimitNotLoaded, pair)
|
||||
}
|
||||
if math.Mod(amount, val.MinSizeIncrement) < 0 {
|
||||
return fmt.Errorf("%w %v %v %v", order.ErrAmountBelowMin, pair, amount, val.MinSizeIncrement)
|
||||
}
|
||||
if amount < val.MinOrderSize {
|
||||
return fmt.Errorf("%w %v %v %v", order.ErrAmountBelowMin, pair, amount, val.MinOrderSize)
|
||||
}
|
||||
if amount > val.MaxOrderSize {
|
||||
return fmt.Errorf("%w %v %v %v", order.ErrAmountExceedsMax, pair, amount, val.MinSizeIncrement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWithdrawalsHistory returns previous withdrawals data
|
||||
func (b *BTSE) GetWithdrawalsHistory(_ context.Context, _ currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
|
||||
return nil, common.ErrFunctionNotSupported
|
||||
@@ -543,10 +504,6 @@ func (b *BTSE) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = b.withinLimits(fPair, s.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := b.CreateOrder(ctx,
|
||||
s.ClientID, 0.0,
|
||||
@@ -1077,45 +1034,6 @@ func (b *BTSE) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pai
|
||||
return req.ProcessResponse(timeSeries)
|
||||
}
|
||||
|
||||
func (b *BTSE) seedOrderSizeLimits(ctx context.Context) error {
|
||||
pairs, err := b.GetMarketSummary(ctx, "", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for x := range pairs {
|
||||
tempValues := OrderSizeLimit{
|
||||
MinOrderSize: pairs[x].MinOrderSize,
|
||||
MaxOrderSize: pairs[x].MaxOrderSize,
|
||||
MinSizeIncrement: pairs[x].MinSizeIncrement,
|
||||
}
|
||||
orderSizeLimitMap.Store(pairs[x].Symbol, tempValues)
|
||||
}
|
||||
|
||||
pairs, err = b.GetMarketSummary(ctx, "", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for x := range pairs {
|
||||
tempValues := OrderSizeLimit{
|
||||
MinOrderSize: pairs[x].MinOrderSize,
|
||||
MaxOrderSize: pairs[x].MaxOrderSize,
|
||||
MinSizeIncrement: pairs[x].MinSizeIncrement,
|
||||
}
|
||||
orderSizeLimitMap.Store(pairs[x].Symbol, tempValues)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OrderSizeLimits looks up currency pair in orderSizeLimitMap and returns OrderSizeLimit
|
||||
func OrderSizeLimits(pair string) (limits OrderSizeLimit, found bool) {
|
||||
resp, ok := orderSizeLimitMap.Load(pair)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
val, ok := resp.(OrderSizeLimit)
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// GetServerTime returns the current exchange server time.
|
||||
func (b *BTSE) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) {
|
||||
st, err := b.GetCurrentServerTime(ctx)
|
||||
@@ -1125,6 +1043,95 @@ func (b *BTSE) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, erro
|
||||
return st.ISO, nil
|
||||
}
|
||||
|
||||
// ExponentPairs returns a map of symbol names which have a Exponent equivalent
|
||||
// e.g. PIT-USD will be returned if M_PIT-USD exists, and SATS-USD if K_SATS-USD exists
|
||||
func (m *MarketSummary) ExponentPairs() (map[string]bool, error) {
|
||||
pairs := map[string]bool{}
|
||||
var errs error
|
||||
for _, s := range *m {
|
||||
if s.Active && s.HasLiquidity() {
|
||||
if symbol, err := s.StripExponent(); err != nil {
|
||||
errs = common.AppendError(errs, err)
|
||||
} else if symbol != "" {
|
||||
pairs[symbol] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return pairs, errs
|
||||
}
|
||||
|
||||
// StripExponent returns the symbol without a exponent prefix; e.g. B_, M_, K_
|
||||
// Returns an empty string if no exponent prefix is found
|
||||
// Errors if there's too many underscores, or if the exponent is not recognised
|
||||
func (m *MarketPair) StripExponent() (string, error) {
|
||||
parts := strings.Split(m.Symbol, "_")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
return "", nil
|
||||
case 2:
|
||||
switch parts[0] {
|
||||
case "B", "M", "K":
|
||||
return parts[1], nil
|
||||
}
|
||||
}
|
||||
return "", errInvalidPairSymbol
|
||||
}
|
||||
|
||||
// Pair returns the currency Pair for a MarketPair
|
||||
func (m *MarketPair) Pair() (currency.Pair, error) {
|
||||
baseCurr := m.Base
|
||||
var quoteCurr string
|
||||
if m.Futures {
|
||||
s := strings.Split(m.Symbol, m.Base) // e.g. RUNEPFC for RUNE-USD futures pair
|
||||
if len(s) <= 1 {
|
||||
return currency.EMPTYPAIR, errInvalidPairSymbol
|
||||
}
|
||||
quoteCurr = s[1]
|
||||
} else {
|
||||
s := strings.Split(m.Symbol, currency.DashDelimiter)
|
||||
if len(s) != 2 {
|
||||
return currency.EMPTYPAIR, errInvalidPairSymbol
|
||||
}
|
||||
baseCurr = s[0]
|
||||
quoteCurr = s[1]
|
||||
}
|
||||
return currency.NewPairFromStrings(baseCurr, quoteCurr)
|
||||
}
|
||||
|
||||
// GetMarketSummary returns filtered market pair details; Specifically:
|
||||
// - Pairs which aren't active are removed
|
||||
// - Pairs which don't have liquidity are removed
|
||||
// - OTC pairs only traded on web UI are removed
|
||||
// - Pairs with an exponent counterpart pair are removed
|
||||
// BTSE lists M_ symbols for very small pairs, in millions. For those listings, we want to take the M_ listing in preference
|
||||
// to the native listing, since they're often going to appear as locked markets due to size (bid == ask, e.g. 0.0000000003)
|
||||
func (b *BTSE) GetMarketSummary(ctx context.Context, symbol string, spot bool) (MarketSummary, error) {
|
||||
m, err := b.GetRawMarketSummary(ctx, symbol, spot)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
ePairs, err := m.ExponentPairs()
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
filtered := make(MarketSummary, 0, len(m))
|
||||
for _, l := range m {
|
||||
if !l.Active || !l.HasLiquidity() || (spot && !l.IsMarketOpenToSpot) { // Skip OTC assets only tradable on web UI
|
||||
continue
|
||||
}
|
||||
if ePairs[l.Symbol] { // Skip pair with an exponent sibling
|
||||
continue
|
||||
}
|
||||
if !spot {
|
||||
// BTSE API for futures does not return futures field at all, and the docs show it coming back as false
|
||||
// Much easier for our data flow if we can trust this field
|
||||
l.Futures = true
|
||||
}
|
||||
filtered = append(filtered, l)
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// GetFuturesContractDetails returns details about futures contracts
|
||||
func (b *BTSE) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) {
|
||||
if !item.IsFutures() {
|
||||
@@ -1265,8 +1272,33 @@ func (b *BTSE) IsPerpetualFutureCurrency(a asset.Item, p currency.Pair) (bool, e
|
||||
}
|
||||
|
||||
// UpdateOrderExecutionLimits updates order execution limits
|
||||
func (b *BTSE) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error {
|
||||
return common.ErrNotYetImplemented
|
||||
func (b *BTSE) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error {
|
||||
summary, err := b.GetMarketSummary(ctx, "", a == asset.Spot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var errs error
|
||||
limits := make([]order.MinMaxLevel, 0, len(summary))
|
||||
for _, marketInfo := range summary {
|
||||
p, err := marketInfo.Pair() //nolint:govet // Deliberately shadow err
|
||||
if err != nil {
|
||||
errs = common.AppendError(err, fmt.Errorf("%s: %w", p, err))
|
||||
continue
|
||||
}
|
||||
limits = append(limits, order.MinMaxLevel{
|
||||
Pair: p,
|
||||
Asset: a,
|
||||
MinimumBaseAmount: marketInfo.MinOrderSize,
|
||||
MaximumBaseAmount: marketInfo.MaxOrderSize,
|
||||
AmountStepIncrementSize: marketInfo.MinSizeIncrement,
|
||||
MinPrice: marketInfo.MinValidPrice,
|
||||
PriceStepIncrementSize: marketInfo.MinPriceIncrement,
|
||||
})
|
||||
}
|
||||
if err = b.LoadLimits(limits); err != nil {
|
||||
errs = common.AppendError(errs, err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// GetOpenInterest returns the open interest rate for a given asset pair
|
||||
|
||||
@@ -10,45 +10,26 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
)
|
||||
|
||||
// Public errors for order limits
|
||||
var (
|
||||
// ErrExchangeLimitNotLoaded defines if an exchange does not have minmax
|
||||
// values
|
||||
ErrExchangeLimitNotLoaded = errors.New("exchange limits not loaded")
|
||||
// ErrPriceBelowMin is when the price is lower than the minimum price
|
||||
// limit accepted by the exchange
|
||||
ErrPriceBelowMin = errors.New("price below minimum limit")
|
||||
// ErrPriceExceedsMax is when the price is higher than the maximum price
|
||||
// limit accepted by the exchange
|
||||
ErrPriceExceedsMax = errors.New("price exceeds maximum limit")
|
||||
// ErrPriceExceedsStep is when the price is not divisible by its step
|
||||
ErrPriceExceedsStep = errors.New("price exceeds step limit")
|
||||
// ErrAmountBelowMin is when the amount is lower than the minimum amount
|
||||
// limit accepted by the exchange
|
||||
ErrAmountBelowMin = errors.New("amount below minimum limit")
|
||||
// ErrAmountExceedsMax is when the amount is higher than the maximum amount
|
||||
// limit accepted by the exchange
|
||||
ErrAmountExceedsMax = errors.New("amount exceeds maximum limit")
|
||||
// ErrAmountExceedsStep is when the amount is not divisible by its step
|
||||
ErrAmountExceedsStep = errors.New("amount exceeds step limit")
|
||||
// ErrNotionalValue is when the notional value does not exceed currency pair
|
||||
// requirements
|
||||
ErrNotionalValue = errors.New("total notional value is under minimum limit")
|
||||
// ErrMarketAmountBelowMin is when the amount is lower than the minimum
|
||||
// amount limit accepted by the exchange for a market order
|
||||
ErrMarketAmountBelowMin = errors.New("market order amount below minimum limit")
|
||||
// ErrMarketAmountExceedsMax is when the amount is higher than the maximum
|
||||
// amount limit accepted by the exchange for a market order
|
||||
ErrMarketAmountExceedsMax = errors.New("market order amount exceeds maximum limit")
|
||||
// ErrMarketAmountExceedsStep is when the amount is not divisible by its
|
||||
// step for a market order
|
||||
ErrMarketAmountExceedsStep = errors.New("market order amount exceeds step limit")
|
||||
// ErrCannotValidateAsset is thrown when the asset is not loaded
|
||||
ErrCannotValidateAsset = errors.New("cannot check limit, asset not loaded")
|
||||
// ErrCannotValidateBaseCurrency is thrown when the base currency is not loaded
|
||||
ErrCannotValidateBaseCurrency = errors.New("cannot check limit, base currency not loaded")
|
||||
// ErrCannotValidateQuoteCurrency is thrown when the quote currency is not loaded
|
||||
ErrLoadLimitsFailed = errors.New("failed to load exchange limits")
|
||||
ErrExchangeLimitNotLoaded = errors.New("exchange limits not loaded")
|
||||
ErrPriceBelowMin = errors.New("price below minimum limit")
|
||||
ErrPriceExceedsMax = errors.New("price exceeds maximum limit")
|
||||
ErrPriceExceedsStep = errors.New("price exceeds step limit") // price is not divisible by its step
|
||||
ErrAmountBelowMin = errors.New("amount below minimum limit")
|
||||
ErrAmountExceedsMax = errors.New("amount exceeds maximum limit")
|
||||
ErrAmountExceedsStep = errors.New("amount exceeds step limit") // amount is not divisible by its step
|
||||
ErrNotionalValue = errors.New("total notional value is under minimum limit")
|
||||
ErrMarketAmountBelowMin = errors.New("market order amount below minimum limit")
|
||||
ErrMarketAmountExceedsMax = errors.New("market order amount exceeds maximum limit")
|
||||
ErrMarketAmountExceedsStep = errors.New("market order amount exceeds step limit") // amount is not divisible by its step for a market order
|
||||
ErrCannotValidateAsset = errors.New("cannot check limit, asset not loaded")
|
||||
ErrCannotValidateBaseCurrency = errors.New("cannot check limit, base currency not loaded")
|
||||
ErrCannotValidateQuoteCurrency = errors.New("cannot check limit, quote currency not loaded")
|
||||
)
|
||||
|
||||
var (
|
||||
errExchangeLimitBase = errors.New("exchange limits not found for base currency")
|
||||
errExchangeLimitQuote = errors.New("exchange limits not found for quote currency")
|
||||
errCannotLoadLimit = errors.New("cannot load limit, levels not supplied")
|
||||
@@ -105,9 +86,7 @@ func (e *ExecutionLimits) LoadLimits(levels []MinMaxLevel) error {
|
||||
|
||||
for x := range levels {
|
||||
if !levels[x].Asset.IsValid() {
|
||||
return fmt.Errorf("cannot load levels for '%s': %w",
|
||||
levels[x].Asset,
|
||||
asset.ErrNotSupported)
|
||||
return fmt.Errorf("cannot load levels for '%s': %w", levels[x].Asset, asset.ErrNotSupported)
|
||||
}
|
||||
m1, ok := e.m[levels[x].Asset]
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user