diff --git a/exchanges/bithumb/bithumb.go b/exchanges/bithumb/bithumb.go index 1c1a0def..88ebbd14 100644 --- a/exchanges/bithumb/bithumb.go +++ b/exchanges/bithumb/bithumb.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "net/http" "net/url" "reflect" @@ -16,6 +17,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" ) @@ -619,3 +622,41 @@ func (b *Bithumb) GetCandleStick(symbol, interval string) (resp OHLCVResponse, e err = b.SendHTTPRequest(exchange.RestSpot, path, &resp) return } + +// FetchExchangeLimits fetches spot order execution limits +func (b *Bithumb) FetchExchangeLimits() ([]order.MinMaxLevel, error) { + ticks, err := b.GetAllTickers() + if err != nil { + return nil, err + } + + var limits []order.MinMaxLevel + for code, data := range ticks { + c := currency.NewCode(code) + cp := currency.NewPair(c, currency.KRW) + if err != nil { + return nil, err + } + + limits = append(limits, order.MinMaxLevel{ + Pair: cp, + Asset: asset.Spot, + MinAmount: getAmountMinimum(data.ClosingPrice), + }) + } + return limits, nil +} + +// getAmountMinimum derives the minimum amount based on current price. This +// keeps amount in line with front end, rounded to 4 decimal places. As +// transaction policy: +// https://en.bithumb.com/customer_support/info_guide?seq=537&categorySeq=302 +// Seems to not be inline with front end limits. +func getAmountMinimum(unitPrice float64) float64 { + if unitPrice <= 0 { + return 0 + } + ratio := 500 / unitPrice + pow := math.Pow(10, float64(4)) + return math.Ceil(ratio*pow) / pow // Round up our units +} diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index 181ff49c..b5bf4d5f 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -1,6 +1,7 @@ package bithumb import ( + "errors" "log" "os" "testing" @@ -616,3 +617,82 @@ func TestGetHistoricTrades(t *testing.T) { t.Error(err) } } + +func TestUpdateOrderExecutionLimits(t *testing.T) { + err := b.UpdateOrderExecutionLimits("") + if err != nil { + t.Fatal(err) + } + cp := currency.NewPair(currency.BTC, currency.KRW) + limit, err := b.GetOrderExecutionLimits(asset.Spot, cp) + if err != nil { + t.Fatal(err) + } + + err = limit.Conforms(46241000, 0.00001, order.Limit) + if !errors.Is(err, order.ErrAmountBelowMin) { + t.Fatalf("expected error %v but received %v", + order.ErrAmountBelowMin, + err) + } + + err = limit.Conforms(46241000, 0.0001, order.Limit) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", + nil, + err) + } +} + +func TestGetAmountMinimum(t *testing.T) { + testCases := []struct { + name string + unitprice float64 + expected float64 + }{ + { + name: "ETH-KRW", + unitprice: 2638000.0, + expected: 0.0002, + }, + { + name: "DOGE-KRW", + unitprice: 236.5, + expected: 2.1142, + }, + { + name: "XRP-KRW", + unitprice: 818.8, + expected: 0.6107, + }, + { + name: "LTC-KRW", + unitprice: 160100, + expected: 0.0032, + }, + { + name: "BTC-KRW", + unitprice: 46079000, + expected: 0.0001, + }, + { + name: "nonsense", + unitprice: 0, + expected: 0, + }, + } + + for i := range testCases { + tt := &testCases[i] + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + minAmount := getAmountMinimum(tt.unitprice) + if minAmount != tt.expected { + t.Fatalf("expected: %f but received: %f for unit price: %f", + tt.expected, + minAmount, + tt.unitprice) + } + }) + } +} diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index a98ec08b..27b2cf59 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -151,11 +151,19 @@ func (b *Bithumb) Run() { b.PrintEnabledPairs() } + err := b.UpdateOrderExecutionLimits("") + if err != nil { + log.Errorf(log.ExchangeSys, + "%s failed to set exchange order execution limits. Err: %v", + b.Name, + err) + } + if !b.GetEnabledFeatures().AutoPairUpdates { return } - err := b.UpdateTradablePairs(false) + err = b.UpdateTradablePairs(false) if err != nil { log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", b.Name, err) } @@ -797,3 +805,12 @@ func (b *Bithumb) GetHistoricCandles(pair currency.Pair, a asset.Item, start, en func (b *Bithumb) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) { return b.GetHistoricCandles(pair, a, start, end, interval) } + +// UpdateOrderExecutionLimits sets exchange executions for a required asset type +func (b *Bithumb) UpdateOrderExecutionLimits(_ asset.Item) error { + limits, err := b.FetchExchangeLimits() + if err != nil { + return fmt.Errorf("cannot update exchange execution limits: %w", err) + } + return b.LoadLimits(limits) +}