diff --git a/currency/code_types.go b/currency/code_types.go index db3d167996b..0f8f12165fe 100644 --- a/currency/code_types.go +++ b/currency/code_types.go @@ -3021,6 +3021,10 @@ var ( FI = NewCode("FI") USDM = NewCode("USDM") USDTM = NewCode("USDTM") + CBETH = NewCode("CBETH") + PYUSD = NewCode("PYUSD") + EUROC = NewCode("EUROC") + LSETH = NewCode("LSETH") stables = Currencies{ USDT, diff --git a/exchanges/coinbasepro/coinbasepro.go b/exchanges/coinbasepro/coinbasepro.go index 73fd5d3df1f..f8443bd1f05 100644 --- a/exchanges/coinbasepro/coinbasepro.go +++ b/exchanges/coinbasepro/coinbasepro.go @@ -15,6 +15,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/common/key" + "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" @@ -80,6 +82,8 @@ const ( granOneDay = "ONE_DAY" startDateString = "start_date" endDateString = "end_date" + + errPayMethodNotFound = "payment method '%v' not found" ) var ( @@ -108,6 +112,7 @@ var ( errPointerNil = errors.New("relevant pointer is nil") errNameEmpty = errors.New("name cannot be empty") errPortfolioIDEmpty = errors.New("portfolio id cannot be empty") + errFeeTypeNotSupported = errors.New("fee type not supported") ) // GetAllAccounts returns information on all trading accounts associated with the API key @@ -2072,56 +2077,63 @@ func (c *CoinbasePro) SendAuthenticatedHTTPRequest(ctx context.Context, ep excha return json.Unmarshal(interim, result) } -// // GetFee returns an estimate of fee based on type of transaction -// func (c *CoinbasePro) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { -// var fee float64 -// switch feeBuilder.FeeType { -// case exchange.CryptocurrencyTradeFee: -// fees, err := c.GetFees(ctx) -// if err != nil { -// fee = fees.TakerFeeRate -// } else { -// fee = 0.006 -// } -// case exchange.InternationalBankWithdrawalFee: -// fee = getInternationalBankWithdrawalFee(feeBuilder.FiatCurrency) -// case exchange.InternationalBankDepositFee: -// fee = getInternationalBankDepositFee(feeBuilder.FiatCurrency) -// case exchange.OfflineTradeFee: -// fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount) -// } - -// if fee < 0 { -// fee = 0 -// } - -// return fee, nil -// } - -// // getOfflineTradeFee calculates the worst case-scenario trading fee -// func getOfflineTradeFee(price, amount float64) float64 { -// return 0.0025 * price * amount -// } - -// func (c *CoinbasePro) calculateTradingFee(trailingVolume []Volume, base, quote currency.Code, delimiter string, purchasePrice, amount float64, isMaker bool) float64 { -// var fee float64 -// for _, i := range trailingVolume { -// if strings.EqualFold(i.ProductID, base.String()+delimiter+quote.String()) { -// switch { -// case isMaker: -// fee = 0 -// case i.Volume <= 10000000: -// fee = 0.003 -// case i.Volume > 10000000 && i.Volume <= 100000000: -// fee = 0.002 -// case i.Volume > 100000000: -// fee = 0.001 -// } -// break -// } -// } -// return fee * amount * purchasePrice -// } +// GetFee returns an estimate of fee based on type of transaction +func (c *CoinbasePro) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { + if feeBuilder == nil { + return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) + } + var fee float64 + switch { + case !c.IsStablePair(feeBuilder.Pair) && feeBuilder.FeeType == exchange.CryptocurrencyTradeFee: + fees, err := c.GetTransactionSummary(ctx, time.Now().Add(-time.Hour*24*30), time.Now(), "", "", "") + fmt.Printf("Fees struct: %v\n", fees) + if err != nil { + return 0, err + } + if feeBuilder.IsMaker { + fee = fees.FeeTier.MakerFeeRate + } else { + fee = fees.FeeTier.TakerFeeRate + } + case feeBuilder.IsMaker && c.IsStablePair(feeBuilder.Pair) && + (feeBuilder.FeeType == exchange.CryptocurrencyTradeFee || feeBuilder.FeeType == exchange.OfflineTradeFee): + fee = 0 + case !feeBuilder.IsMaker && c.IsStablePair(feeBuilder.Pair) && + (feeBuilder.FeeType == exchange.CryptocurrencyTradeFee || feeBuilder.FeeType == exchange.OfflineTradeFee): + fee = 0.00001 + case feeBuilder.IsMaker && !c.IsStablePair(feeBuilder.Pair) && feeBuilder.FeeType == exchange.OfflineTradeFee: + fee = 0.006 + case !feeBuilder.IsMaker && !c.IsStablePair(feeBuilder.Pair) && feeBuilder.FeeType == exchange.OfflineTradeFee: + fee = 0.008 + default: + return 0, errFeeTypeNotSupported + } + return fee * feeBuilder.Amount * feeBuilder.PurchasePrice, nil +} + +var stableMap = map[key.PairAsset]bool{ + {Base: currency.USDT.Item, Quote: currency.USD.Item}: true, + {Base: currency.USDT.Item, Quote: currency.EUR.Item}: true, + {Base: currency.USDC.Item, Quote: currency.EUR.Item}: true, + {Base: currency.USDC.Item, Quote: currency.GBP.Item}: true, + {Base: currency.USDT.Item, Quote: currency.GBP.Item}: true, + {Base: currency.USDT.Item, Quote: currency.USDC.Item}: true, + {Base: currency.DAI.Item, Quote: currency.USD.Item}: true, + {Base: currency.CBETH.Item, Quote: currency.ETH.Item}: true, + {Base: currency.PYUSD.Item, Quote: currency.USD.Item}: true, + {Base: currency.EUROC.Item, Quote: currency.USD.Item}: true, + {Base: currency.GUSD.Item, Quote: currency.USD.Item}: true, + {Base: currency.EUROC.Item, Quote: currency.EUR.Item}: true, + {Base: currency.WBTC.Item, Quote: currency.BTC.Item}: true, + {Base: currency.LSETH.Item, Quote: currency.ETH.Item}: true, + {Base: currency.GYEN.Item, Quote: currency.USD.Item}: true, + {Base: currency.PAX.Item, Quote: currency.USD.Item}: true, +} + +// IsStablePair returns true if the currency pair is considered a "stable pair" by Coinbase +func (c *CoinbasePro) IsStablePair(pair currency.Pair) bool { + return stableMap[key.PairAsset{Base: pair.Base.Item, Quote: pair.Quote.Item}] +} // func getInternationalBankWithdrawalFee(c currency.Code) float64 { // var fee float64 diff --git a/exchanges/coinbasepro/coinbasepro_test.go b/exchanges/coinbasepro/coinbasepro_test.go index 1d7d733020b..65f4acc5a68 100644 --- a/exchanges/coinbasepro/coinbasepro_test.go +++ b/exchanges/coinbasepro/coinbasepro_test.go @@ -51,9 +51,14 @@ const ( errIDNotSet = "ID not set" skipPayMethodNotFound = "no payment methods found, skipping" skipInsufSuitableAccs = "insufficient suitable accounts found, skipping" + skipInsufficientFunds = "insufficient funds for test, skipping" errx7f = "setting proxy address error parse \"\\x7f\": net/url: invalid control character in URL" errPortfolioNameDuplicate = `CoinbasePro unsuccessful HTTP status code: 409 raw response: {"error":"CONFLICT","error_details":"[PORTFOLIO_ERROR_CODE_ALREADY_EXISTS] the requested portfolio name already exists","message":"[PORTFOLIO_ERROR_CODE_ALREADY_EXISTS] the requested portfolio name already exists"}, authenticated request failed` errPortTransferInsufFunds = `CoinbasePro unsuccessful HTTP status code: 429 raw response: {"error":"unknown","error_details":"[PORTFOLIO_ERROR_CODE_INSUFFICIENT_FUNDS] insufficient funds in source account","message":"[PORTFOLIO_ERROR_CODE_INSUFFICIENT_FUNDS] insufficient funds in source account"}, authenticated request failed` + errFeeBuilderNil = "*exchange.FeeBuilder nil pointer" + errUnsupportedAssetType = " unsupported asset type" + + testAmount = 0.00000001 ) func TestMain(m *testing.M) { @@ -402,7 +407,7 @@ func TestMovePortfolioFunds(t *testing.T) { t.Skip("fewer than 2 portfolios found, skipping") } _, err = c.MovePortfolioFunds(context.Background(), portID.Portfolios[0].UUID, portID.Portfolios[1].UUID, "BTC", - 0.00000001) + testAmount) if err != nil && err.Error() != errPortTransferInsufFunds { t.Error(err) } @@ -890,8 +895,27 @@ func TestSendMoney(t *testing.T) { if len(wID.Data) < 2 { t.Skip("fewer than 2 wallets found, skipping test") } + var ( + fromID string + toID string + ) + for i := range wID.Data { + if wID.Data[i].Currency.Name == "BTC" { + if wID.Data[i].Balance.Amount > 0 { + fromID = wID.Data[i].ID + } else { + toID = wID.Data[i].ID + } + } + if fromID != "" && toID != "" { + break + } + } + if fromID == "" || toID == "" { + t.Skip("insufficient funds or BTC wallets, skipping") + } _, err = c.SendMoney(context.Background(), "transfer", wID.Data[0].ID, wID.Data[1].ID, - "BTC", "GCT Test", "123", "", "", 0.00000001, false, false) + "BTC", "GCT Test", "123", "", "", testAmount, false, false) if err != nil { t.Error(err) } @@ -2486,7 +2510,7 @@ func TestWsStatus(t *testing.T) { "quote_currency": "USD", "base_min_size": "0.001", "base_max_size": "70", - "base_increment": "0.00000001", + "base_increment": "testAmount", "quote_increment": "0.01", "display_name": "BTC/USD", "status": "online", @@ -2520,10 +2544,10 @@ func TestWsStatus(t *testing.T) { { "id": "BTC", "name": "Bitcoin", - "min_size": "0.00000001", + "min_size": "testAmount", "status": "online", "status_message": null, - "max_precision": "0.00000001", + "max_precision": "testAmount", "convertible_to": [] } ] @@ -2770,6 +2794,72 @@ func TestStatusToStandardStatus(t *testing.T) { */ +func TestGetFee(t *testing.T) { + _, err := c.GetFee(context.Background(), nil) + if err.Error() != errFeeBuilderNil { + t.Errorf(errExpectMismatch, errFeeBuilderNil, err) + } + feeBuilder := exchange.FeeBuilder{ + FeeType: exchange.OfflineTradeFee, + Amount: 1, + PurchasePrice: 1, + } + resp, err := c.GetFee(context.Background(), &feeBuilder) + if err != nil { + t.Error(err) + } + if resp != 0.008 { + t.Errorf(errExpectMismatch, resp, 0.008) + } + feeBuilder.IsMaker = true + resp, err = c.GetFee(context.Background(), &feeBuilder) + if err != nil { + t.Error(err) + } + if resp != 0.006 { + t.Errorf(errExpectMismatch, resp, 0.006) + } + feeBuilder.Pair = currency.NewPair(currency.USDT, currency.USD) + resp, err = c.GetFee(context.Background(), &feeBuilder) + if err != nil { + t.Error(err) + } + if resp != 0 { + t.Errorf(errExpectMismatch, resp, 0) + } + feeBuilder.IsMaker = false + resp, err = c.GetFee(context.Background(), &feeBuilder) + if err != nil { + t.Error(err) + } + if resp != 0.00001 { + t.Errorf(errExpectMismatch, resp, 0.00001) + } + feeBuilder.FeeType = exchange.CryptocurrencyDepositFee + _, err = c.GetFee(context.Background(), &feeBuilder) + if err != errFeeTypeNotSupported { + t.Errorf(errExpectMismatch, errFeeTypeNotSupported, err) + } + feeBuilder.Pair = currency.Pair{} + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + feeBuilder.FeeType = exchange.CryptocurrencyTradeFee + resp, err = c.GetFee(context.Background(), &feeBuilder) + if err != nil { + t.Error(err) + } + if !(resp <= 0.008 && resp >= 0.0005) { + t.Errorf("expected fee range of 0.0005 and 0.008, received %v", resp) + } + feeBuilder.IsMaker = true + resp, err = c.GetFee(context.Background(), &feeBuilder) + if err != nil { + t.Error(err) + } + if !(resp <= 0.006 && resp >= 0) { + t.Errorf("expected fee range of 0 and 0.006, received %v", resp) + } +} + func TestPrepareDateString(t *testing.T) { t.Parallel() var expectedResult Params @@ -3070,22 +3160,6 @@ func TestGetWithdrawalsHistory(t *testing.T) { } } -func TestGetRecentTrades(t *testing.T) { - sharedtestvalues.SkipTestIfCredentialsUnset(t, c) - _, err := c.GetRecentTrades(context.Background(), testPair, asset.Spot) - if err != nil { - t.Error(err) - } -} - -func TestGetHistoricTrades(t *testing.T) { - sharedtestvalues.SkipTestIfCredentialsUnset(t, c) - _, err := c.GetHistoricTrades(context.Background(), testPair, asset.Spot, time.Time{}, time.Now()) - if err != nil { - t.Error(err) - } -} - func TestSubmitOrder(t *testing.T) { _, err := c.SubmitOrder(context.Background(), nil) if !errors.Is(err, common.ErrNilPointer) { @@ -3257,18 +3331,145 @@ func TestWithdrawCryptocurrencyFunds(t *testing.T) { if !errors.Is(err, common.ErrExchangeNameUnset) { t.Errorf(errExpectMismatch, err, common.ErrExchangeNameUnset) } - sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) req.Exchange = c.Name req.Currency = currency.BTC - req.Amount = 0.00000001 + req.Amount = testAmount req.Type = withdraw.Crypto req.Crypto.Address = testAddress _, err = c.WithdrawCryptocurrencyFunds(context.Background(), &req) + if !errors.Is(err, errWalletIDEmpty) { + t.Errorf(errExpectMismatch, err, errWalletIDEmpty) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + wallets, err := c.GetAllWallets(context.Background(), PaginationInp{}) + if err != nil { + t.Error(err) + } + if len(wallets.Data) == 0 { + t.Fatal(errExpectedNonEmpty) + } + for i := range wallets.Data { + if wallets.Data[i].Currency.Name == currency.BTC.String() && wallets.Data[i].Balance.Amount > testAmount { + req.WalletID = wallets.Data[i].ID + break + } + } + if req.WalletID == "" { + t.Skip(skipInsufficientFunds) + } + _, err = c.WithdrawCryptocurrencyFunds(context.Background(), &req) + if err != nil { + t.Error(err) + } +} + +func TestWithdrawFiatFunds(t *testing.T) { + req := withdraw.Request{} + _, err := c.WithdrawFiatFunds(context.Background(), &req) + if !errors.Is(err, common.ErrExchangeNameUnset) { + t.Errorf(errExpectMismatch, err, common.ErrExchangeNameUnset) + } + req.Exchange = c.Name + req.Currency = currency.AUD + req.Amount = 1 + req.Type = withdraw.Fiat + req.Fiat.Bank.Enabled = true + req.Fiat.Bank.SupportedExchanges = "CoinbasePro" + req.Fiat.Bank.SupportedCurrencies = "AUD" + req.Fiat.Bank.AccountNumber = "123" + req.Fiat.Bank.SWIFTCode = "456" + req.Fiat.Bank.BSBNumber = "789" + _, err = c.WithdrawFiatFunds(context.Background(), &req) + if !errors.Is(err, errWalletIDEmpty) { + t.Errorf(errExpectMismatch, err, errWalletIDEmpty) + } + req.WalletID = "meow" + req.Fiat.Bank.BankName = "GCT's Fake and Not Real Test Bank Meow Meow" + expectedError := fmt.Sprintf(errPayMethodNotFound, req.Fiat.Bank.BankName) + _, err = c.WithdrawFiatFunds(context.Background(), &req) + if err.Error() != expectedError { + t.Errorf(errExpectMismatch, err, expectedError) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, c, canManipulateRealOrders) + wallets, err := c.GetAllWallets(context.Background(), PaginationInp{}) + if err != nil { + t.Error(err) + } + if len(wallets.Data) == 0 { + t.Fatal(errExpectedNonEmpty) + } + for i := range wallets.Data { + if wallets.Data[i].Currency.Name == currency.AUD.String() && wallets.Data[i].Balance.Amount > testAmount { + req.WalletID = wallets.Data[i].ID + break + } + } + if req.WalletID == "" { + t.Skip(skipInsufficientFunds) + } + req.Fiat.Bank.BankName = "AUD Wallet" + _, err = c.WithdrawFiatFunds(context.Background(), &req) if err != nil { t.Error(err) } } +func TestWithdrawFiatFundsToInternationalBank(t *testing.T) { + req := withdraw.Request{} + _, err := c.WithdrawFiatFundsToInternationalBank(context.Background(), &req) + if !errors.Is(err, common.ErrExchangeNameUnset) { + t.Errorf(errExpectMismatch, err, common.ErrExchangeNameUnset) + } +} + +func TestGetFeeByType(t *testing.T) { + _, err := c.GetFeeByType(context.Background(), nil) + if err.Error() != errFeeBuilderNil { + t.Errorf(errExpectMismatch, err, errFeeBuilderNil) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + var feeBuilder exchange.FeeBuilder + feeBuilder.FeeType = exchange.OfflineTradeFee + feeBuilder.Amount = 1 + feeBuilder.PurchasePrice = 1 + resp, err := c.GetFeeByType(context.Background(), &feeBuilder) + if err != nil { + t.Error(err) + } + if resp != 0.008 { + t.Errorf(errExpectMismatch, resp, 0.008) + } +} + +func TestGetActiveOrders(t *testing.T) { + _, err := c.GetActiveOrders(context.Background(), nil) + if !errors.Is(err, common.ErrNilPointer) { + t.Errorf(errExpectMismatch, err, common.ErrNilPointer) + } + var req order.MultiOrderRequest + _, err = c.GetActiveOrders(context.Background(), &req) + if err.Error() != errUnsupportedAssetType { + t.Errorf(errExpectMismatch, err, errUnsupportedAssetType) + } + sharedtestvalues.SkipTestIfCredentialsUnset(t, c) + req.AssetType = asset.Spot + req.Side = order.AnySide + req.Type = order.AnyType + _, err = c.GetActiveOrders(context.Background(), &req) + if err != nil { + t.Error(err) + } + req.Pairs = req.Pairs.Add(currency.NewPair(currency.BTC, currency.USD)) + _, err = c.GetActiveOrders(context.Background(), &req) + if err != nil { + t.Error(err) + } +} + +func TestGetOrderHistory(t *testing.T) { + +} + // 8837708 143.0 ns/op 24 B/op 5 allocs/op // func BenchmarkXxx(b *testing.B) { // for x := 0; x < b.N; x++ { @@ -3389,3 +3590,20 @@ func TestWithdrawCryptocurrencyFunds(t *testing.T) { // _ = str2 // } // } + +// func AssignStructData(a AmCur) { +// req := map[string]interface{}{"amount": a.Amount, "currency": a.Currency} +// _ = req +// } + +// BenchmarkAssignStructData-8 18488131 65.81 ns/op 0 B/op 0 allocs/op +// BenchmarkAssignStructData-8 18336296 67.94 ns/op 0 B/op 0 allocs/op +// BenchmarkAssignStructData-8 5245032 230.0 ns/op 0 B/op 0 allocs/op +// BenchmarkAssignStructData-8 6194977 193.4 ns/op 0 B/op 0 allocs/op +// func BenchmarkAssignStructData(b *testing.B) { +// for x := 0; x < b.N; x++ { +// AssignStructData(AmCur{}) +// AssignStructData(AmCur{}) +// AssignStructData(AmCur{}) +// } +// } diff --git a/exchanges/coinbasepro/coinbasepro_types.go b/exchanges/coinbasepro/coinbasepro_types.go index b79874369d7..84f187cd20d 100644 --- a/exchanges/coinbasepro/coinbasepro_types.go +++ b/exchanges/coinbasepro/coinbasepro_types.go @@ -440,23 +440,27 @@ type TransactionSummary struct { TotalVolume float64 `json:"total_volume"` TotalFees float64 `json:"total_fees"` FeeTier struct { - PricingTier float64 `json:"pricing_tier,string"` - USDFrom float64 `json:"usd_from,string"` - USDTo float64 `json:"usd_to,string"` - TakerFeeRate float64 `json:"taker_fee_rate,string"` - MakerFeeRate float64 `json:"maker_fee_rate,string"` - } + PricingTier string `json:"pricing_tier"` + USDFrom float64 `json:"usd_from,string"` + USDTo float64 `json:"usd_to,string"` + TakerFeeRate float64 `json:"taker_fee_rate,string"` + MakerFeeRate float64 `json:"maker_fee_rate,string"` + AOPFrom convert.StringToFloat64 `json:"aop_from"` + AOPTo convert.StringToFloat64 `json:"aop_to"` + } `json:"fee_tier"` MarginRate struct { Value float64 `json:"value,string"` - } + } `json:"margin_rate"` GoodsAndServicesTax struct { Rate float64 `json:"rate,string"` Type string `json:"type"` - } - AdvancedTradeOnlyVolume float64 `json:"advanced_trade_only_volume"` - AdvancedTradeOnlyFees float64 `json:"advanced_trade_only_fees"` - CoinbaseProVolume float64 `json:"coinbase_pro_volume"` - CoinbaseProFees float64 `json:"coinbase_pro_fees"` + } `json:"goods_and_services_tax"` + AdvancedTradeOnlyVolume float64 `json:"advanced_trade_only_volume"` + AdvancedTradeOnlyFees float64 `json:"advanced_trade_only_fees"` + CoinbaseProVolume float64 `json:"coinbase_pro_volume"` + CoinbaseProFees float64 `json:"coinbase_pro_fees"` + TotalBalance convert.StringToFloat64 `json:"total_balance"` + HasPromoFee bool `json:"has_promo_fee"` } // GetAllOrdersResp contains information on a lot of orders, returned by GetAllOrders diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index fd0ff4613e0..79ca74a2347 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -5,12 +5,10 @@ import ( "encoding/hex" "errors" "fmt" - "sort" "strconv" "sync" "time" - "github.com/gofrs/uuid" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/config" @@ -651,114 +649,12 @@ func (c *CoinbasePro) GetWithdrawalsHistory(ctx context.Context, cur currency.Co // GetRecentTrades returns the most recent trades for a currency and asset func (c *CoinbasePro) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) { - return c.GetHistoricTrades(ctx, p, assetType, time.Time{}, time.Now()) + return nil, common.ErrFunctionNotSupported } // GetHistoricTrades returns historic trade data within the timeframe provided func (c *CoinbasePro) GetHistoricTrades(ctx context.Context, p currency.Pair, assetType asset.Item, startDate, endDate time.Time) ([]trade.Data, error) { - p, err := c.FormatExchangeCurrency(p, assetType) - if err != nil { - return nil, err - } - - statuses := []string{"FILLED", "CANCELLED", "EXPIRED", "FAILED"} - - ord, err := c.GetAllOrders(ctx, p.String(), "", "", "", "", assetType.Upper(), "", "", statuses, 2<<30-1, - startDate, endDate) - - if err != nil { - return nil, err - } - - resp := make([]trade.Data, len(ord.Orders)) - - for i := range ord.Orders { - var side order.Side - side, err = order.StringToOrderSide(ord.Orders[i].Side) - if err != nil { - return nil, err - } - id, err := uuid.FromString(ord.Orders[i].OrderID) - if err != nil { - return nil, err - } - var price float64 - var amount float64 - if ord.Orders[i].OrderConfiguration.MarketMarketIOC != nil { - err = stringToFloatPtr(&amount, ord.Orders[i].OrderConfiguration.MarketMarketIOC.QuoteSize) - if err != nil { - return nil, err - } - err = stringToFloatPtr(&amount, ord.Orders[i].OrderConfiguration.MarketMarketIOC.BaseSize) - if err != nil { - return nil, err - } - } - - if ord.Orders[i].OrderConfiguration.LimitLimitGTC != nil { - err = stringToFloatPtr(&price, ord.Orders[i].OrderConfiguration.LimitLimitGTC.LimitPrice) - if err != nil { - return nil, err - } - err = stringToFloatPtr(&amount, ord.Orders[i].OrderConfiguration.LimitLimitGTC.BaseSize) - if err != nil { - return nil, err - } - - } - if ord.Orders[i].OrderConfiguration.LimitLimitGTD != nil { - err = stringToFloatPtr(&price, ord.Orders[i].OrderConfiguration.LimitLimitGTD.LimitPrice) - if err != nil { - return nil, err - } - err = stringToFloatPtr(&amount, ord.Orders[i].OrderConfiguration.LimitLimitGTD.BaseSize) - if err != nil { - return nil, err - } - - } - if ord.Orders[i].OrderConfiguration.StopLimitStopLimitGTC != nil { - err = stringToFloatPtr(&price, ord.Orders[i].OrderConfiguration.StopLimitStopLimitGTC.LimitPrice) - if err != nil { - return nil, err - } - err = stringToFloatPtr(&amount, ord.Orders[i].OrderConfiguration.StopLimitStopLimitGTC.BaseSize) - if err != nil { - return nil, err - } - - } - if ord.Orders[i].OrderConfiguration.StopLimitStopLimitGTD != nil { - err = stringToFloatPtr(&price, ord.Orders[i].OrderConfiguration.StopLimitStopLimitGTD.LimitPrice) - if err != nil { - return nil, err - } - err = stringToFloatPtr(&amount, ord.Orders[i].OrderConfiguration.StopLimitStopLimitGTD.BaseSize) - if err != nil { - return nil, err - } - - } - - resp[i] = trade.Data{ - ID: id, - Exchange: c.Name, - CurrencyPair: p, - AssetType: assetType, - Side: side, - Price: price, - Amount: amount, - Timestamp: ord.Orders[i].CreatedTime, - } - } - - err = c.AddTradesToBuffer(resp...) - if err != nil { - return nil, err - } - - sort.Sort(trade.ByDate(resp)) - return resp, nil + return nil, common.ErrFunctionNotSupported } // SubmitOrder submits a new order @@ -868,6 +764,22 @@ func (c *CoinbasePro) CancelBatchOrders(ctx context.Context, o []order.Cancel) ( return &status, nil } +func (c *CoinbasePro) iterativeGetAllOrders(ctx context.Context, productID, userNativeCurrency, orderType, orderSide, cursor, productType, orderPlacementSource, contractExpiryType string, orderStatus []string, limit int32, startDate, endDate time.Time) ([]GetOrderResponse, error) { + var hasNext bool + var resp []GetOrderResponse + for hasNext { + interResp, err := c.GetAllOrders(ctx, productID, userNativeCurrency, orderType, orderSide, cursor, + productType, orderPlacementSource, contractExpiryType, orderStatus, limit, startDate, endDate) + if err != nil { + return nil, err + } + resp = append(resp, interResp.Orders...) + hasNext = interResp.HasNext + cursor = interResp.Cursor + } + return resp, nil +} + // CancelAllOrders cancels all orders associated with a currency pair func (c *CoinbasePro) CancelAllOrders(ctx context.Context, can *order.Cancel) (order.CancelAllResponse, error) { var resp order.CancelAllResponse @@ -878,20 +790,15 @@ func (c *CoinbasePro) CancelAllOrders(ctx context.Context, can *order.Cancel) (o if err != nil { return resp, err } - var ordIDs []GetOrderResponse var cursor string ordStatus := []string{"OPEN"} - hasNext := true - for hasNext { - interResp, err := c.GetAllOrders(ctx, can.Pair.String(), "", "", "", cursor, "", "", "", ordStatus, 1000, - time.Time{}, time.Time{}) - if err != nil { - return resp, err - } - ordIDs = append(ordIDs, interResp.Orders...) - hasNext = interResp.HasNext - cursor = interResp.Cursor + + ordIDs, err := c.iterativeGetAllOrders(ctx, can.Pair.String(), "", "", "", cursor, "", "", "", ordStatus, 1000, + time.Time{}, time.Time{}) + if err != nil { + return resp, err } + if len(ordStatus) == 0 { return resp, errNoMatchingOrders } @@ -911,16 +818,11 @@ func (c *CoinbasePro) CancelAllOrders(ctx context.Context, can *order.Cancel) (o return resp, nil } -// GetOrderInfo returns order information based on order ID -func (c *CoinbasePro) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetItem asset.Item) (*order.Detail, error) { - genOrderDetail, err := c.GetOrderByID(ctx, orderID, "", "") - if err != nil { - return nil, err - } - +func (c *CoinbasePro) getOrderRespToOrderDetail(genOrderDetail *GetOrderResponse, pair currency.Pair, assetItem asset.Item) (*order.Detail, error) { var amount float64 var quoteAmount float64 var orderType order.Type + var err error if genOrderDetail.OrderConfiguration.MarketMarketIOC != nil { err = stringToFloatPtr("eAmount, genOrderDetail.OrderConfiguration.MarketMarketIOC.QuoteSize) if err != nil { @@ -1049,8 +951,20 @@ func (c *CoinbasePro) GetOrderInfo(ctx context.Context, orderID string, pair cur LastUpdated: lastUpdateTime, Pair: pair, } + return &response, nil +} - fmt.Printf("%+v\n", response) +// GetOrderInfo returns order information based on order ID +func (c *CoinbasePro) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetItem asset.Item) (*order.Detail, error) { + genOrderDetail, err := c.GetOrderByID(ctx, orderID, "", "") + if err != nil { + return nil, err + } + + response, err := c.getOrderRespToOrderDetail(genOrderDetail, pair, assetItem) + if err != nil { + return nil, err + } fillData, err := c.GetFills(ctx, orderID, "", "", 2<<15-1, time.Time{}, time.Now()) if err != nil { @@ -1066,7 +980,8 @@ func (c *CoinbasePro) GetOrderInfo(ctx context.Context, orderID string, pair cur cursor = tempFillData.Cursor } response.Trades = make([]order.TradeHistory, len(fillData.Fills)) - switch orderSide { + var orderSide order.Side + switch response.Side { case order.Buy: orderSide = order.Sell case order.Sell: @@ -1085,7 +1000,7 @@ func (c *CoinbasePro) GetOrderInfo(ctx context.Context, orderID string, pair cur } } - return &response, nil + return response, nil } // GetDepositAddress returns a deposit address for a specified currency @@ -1127,28 +1042,17 @@ func (c *CoinbasePro) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawR if err := withdrawRequest.Validate(); err != nil { return nil, err } - gawResp, err := c.GetAllWallets(ctx, PaginationInp{}) - if err != nil { - return nil, err - } - if len(gawResp.Data) == 0 { - return nil, errNoMatchingWallets + if withdrawRequest.WalletID == "" { + return nil, errWalletIDEmpty } - var wID string - for i := range gawResp.Data { - if gawResp.Data[i].Currency.Code == withdrawRequest.Currency.String() { - wID = gawResp.Data[i].ID - break - } - } - - message := fmt.Sprintf("%+v", withdrawRequest) creds, err := c.GetCredentials(ctx) if err != nil { return nil, err } + message := fmt.Sprintf("%+v", withdrawRequest) + hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte(message), []byte(creds.Secret)) @@ -1167,9 +1071,9 @@ func (c *CoinbasePro) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawR message = message[:tocut] - resp, err := c.SendMoney(ctx, "send", wID, withdrawRequest.Crypto.Address, withdrawRequest.Currency.String(), - withdrawRequest.Description, message, "", withdrawRequest.Crypto.AddressTag, withdrawRequest.Amount, - false, false) + resp, err := c.SendMoney(ctx, "send", withdrawRequest.WalletID, withdrawRequest.Crypto.Address, + withdrawRequest.Currency.String(), withdrawRequest.Description, message, "", + withdrawRequest.Crypto.AddressTag, withdrawRequest.Amount, false, false) if err != nil { return nil, err @@ -1184,122 +1088,95 @@ func (c *CoinbasePro) WithdrawFiatFunds(ctx context.Context, withdrawRequest *wi if err := withdrawRequest.Validate(); err != nil { return nil, err } - // paymentMethods, err := c.GetPayMethods(ctx) - // if err != nil { - // return nil, err - // } + if withdrawRequest.WalletID == "" { + return nil, errWalletIDEmpty + } - selectedWithdrawalMethod := PaymentMethod{} - // for i := range paymentMethods { - // if withdrawRequest.Fiat.Bank.BankName == paymentMethods[i].Name { - // selectedWithdrawalMethod = paymentMethods[i] - // break - // } - // } - if selectedWithdrawalMethod.ID == "" { - return nil, fmt.Errorf("could not find payment method '%v'. Check the name via the website and try again", withdrawRequest.Fiat.Bank.BankName) + paymentMethods, err := c.GetAllPaymentMethods(ctx, PaginationInp{}) + if err != nil { + return nil, err } - // resp, err := c.WithdrawViaPaymentMethod(ctx, - // withdrawRequest.Amount, - // withdrawRequest.Currency.String(), - // selectedWithdrawalMethod.ID) - // if err != nil { - // return nil, err - // } + selectedWithdrawalMethod := PaymentMethodData{} + for i := range paymentMethods.Data { + if withdrawRequest.Fiat.Bank.BankName == paymentMethods.Data[i].Name { + selectedWithdrawalMethod = paymentMethods.Data[i] + break + } + } + if selectedWithdrawalMethod.ID == "" { + return nil, fmt.Errorf(errPayMethodNotFound, withdrawRequest.Fiat.Bank.BankName) + } - // return &withdraw.ExchangeResponse{ - // Status: resp.ID, - // }, nil - return nil, common.ErrFunctionNotSupported -} + resp, err := c.FiatTransfer(ctx, withdrawRequest.WalletID, withdrawRequest.Currency.String(), + selectedWithdrawalMethod.ID, withdrawRequest.Amount, true, FiatWithdrawal) -// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a -// withdrawal is submitted -func (c *CoinbasePro) WithdrawFiatFundsToInternationalBank(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { - if err := withdrawRequest.Validate(); err != nil { - return nil, err - } - v, err := c.WithdrawFiatFunds(ctx, withdrawRequest) if err != nil { return nil, err } + return &withdraw.ExchangeResponse{ - ID: v.ID, - Status: v.Status, + Name: selectedWithdrawalMethod.Name, + ID: resp.Data.ID, + Status: resp.Data.Status, }, nil } +// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a +// withdrawal is submitted +func (c *CoinbasePro) WithdrawFiatFundsToInternationalBank(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { + return c.WithdrawFiatFunds(ctx, withdrawRequest) +} + // GetFeeByType returns an estimate of fee based on type of transaction func (c *CoinbasePro) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { - // if feeBuilder == nil { - // return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) - // } - // if !c.AreCredentialsValid(ctx) && // Todo check connection status - // feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { - // feeBuilder.FeeType = exchange.OfflineTradeFee - // } - // return c.GetFee(ctx, feeBuilder) - return 99999, errors.New(common.ErrFunctionNotSupported.Error()) + if feeBuilder == nil { + return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) + } + if !c.AreCredentialsValid(ctx) && // Todo check connection status + feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { + feeBuilder.FeeType = exchange.OfflineTradeFee + } + return c.GetFee(ctx, feeBuilder) } // GetActiveOrders retrieves any orders that are active/open func (c *CoinbasePro) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) { + if req == nil { + return nil, common.ErrNilPointer + } err := req.Validate() if err != nil { return nil, err } var respOrders []GetOrderResponse - // var fPair currency.Pair - // for i := range req.Pairs { - // // fPair, err = c.FormatExchangeCurrency(req.Pairs[i], asset.Spot) - // if err != nil { - // return nil, err - // } - - // var resp []GetOrderResponse - // // resp, err = c.GetOrders(ctx, - // // []string{"open", "pending", "active"}, - // // fPair.String()) - // if err != nil { - // return nil, err - // } - // respOrders = append(respOrders, resp...) - // } - - format, err := c.GetPairFormat(asset.Spot, false) - if err != nil { - return nil, err + var cursor string + ordStatus := []string{"OPEN"} + pairIDs := req.Pairs.Strings() + if len(pairIDs) == 0 { + respOrders, err = c.iterativeGetAllOrders(ctx, "", "", req.Type.String(), req.Side.String(), cursor, + req.AssetType.Upper(), "", "", ordStatus, 1000, req.StartTime, req.EndTime) + if err != nil { + return nil, err + } + } else { + for i := range pairIDs { + interResp, err := c.iterativeGetAllOrders(ctx, pairIDs[i], "", req.Type.String(), req.Side.String(), + cursor, req.AssetType.Upper(), "", "", ordStatus, 1000, req.StartTime, req.EndTime) + if err != nil { + return nil, err + } + respOrders = append(respOrders, interResp...) + } } orders := make([]order.Detail, len(respOrders)) for i := range respOrders { - var curr currency.Pair - curr, err = currency.NewPairDelimiter(respOrders[i].ProductID, - format.Delimiter) - if err != nil { - return nil, err - } - var side order.Side - side, err = order.StringToOrderSide(respOrders[i].Side) + orderRec, err := c.getOrderRespToOrderDetail(&respOrders[i], req.Pairs[i], asset.Spot) if err != nil { return nil, err } - var orderType order.Type - // orderType, err = order.StringToOrderType(respOrders[i].Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", c.Name, err) - } - orders[i] = order.Detail{ - // OrderID: respOrders[i].ID, - // Amount: respOrders[i].Size, - ExecutedAmount: respOrders[i].FilledSize, - Type: orderType, - Date: respOrders[i].CreatedTime, - Side: side, - Pair: curr, - Exchange: c.Name, - } + orders[i] = *orderRec } return req.Filter(c.Name, orders), nil } @@ -1311,7 +1188,134 @@ func (c *CoinbasePro) GetOrderHistory(ctx context.Context, req *order.MultiOrder if err != nil { return nil, err } - var respOrders []GetOrderResponse + + var p []string + + if len(req.Pairs) == 0 { + p = make([]string, 1) + } else { + p = make([]string, len(req.Pairs)) + for i := range req.Pairs { + req.Pairs[i], err = c.FormatExchangeCurrency(req.Pairs[i], req.AssetType) + if err != nil { + return nil, err + } + p[i] = req.Pairs[i].String() + } + } + + closedStatuses := []string{"FILLED", "CANCELLED", "EXPIRED", "FAILED"} + openStatus := []string{"OPEN"} + var ord []GetOrderResponse + + for i := range p { + interOrd, err := c.iterativeGetAllOrders(ctx, p[i], "", req.Type.String(), req.Side.String(), "", + req.AssetType.Upper(), "", "", closedStatuses, 2<<30-1, req.StartTime, req.EndTime) + if err != nil { + return nil, err + } + ord = append(ord, interOrd...) + interOrd, err = c.iterativeGetAllOrders(ctx, p[i], "", req.Type.String(), req.Side.String(), "", + req.AssetType.Upper(), "", "", openStatus, 2<<30-1, req.StartTime, req.EndTime) + if err != nil { + return nil, err + } + ord = append(ord, interOrd...) + } + + orders := make([]order.Detail, len(ord)) + + for i := range ord { + singleOrder, err := c.getOrderRespToOrderDetail(&ord[i], req.Pairs[0], req.AssetType) + if err != nil { + return nil, err + } + orders[i] = *singleOrder + } + + // for i := range ord { + // var side order.Side + // side, err = order.StringToOrderSide(ord[i].Side) + // if err != nil { + // return nil, err + // } + // id, err := uuid.FromString(ord[i].OrderID) + // if err != nil { + // return nil, err + // } + // var price float64 + // var amount float64 + // if ord[i].OrderConfiguration.MarketMarketIOC != nil { + // err = stringToFloatPtr(&amount, ord[i].OrderConfiguration.MarketMarketIOC.QuoteSize) + // if err != nil { + // return nil, err + // } + // err = stringToFloatPtr(&amount, ord[i].OrderConfiguration.MarketMarketIOC.BaseSize) + // if err != nil { + // return nil, err + // } + // } + + // if ord[i].OrderConfiguration.LimitLimitGTC != nil { + // err = stringToFloatPtr(&price, ord[i].OrderConfiguration.LimitLimitGTC.LimitPrice) + // if err != nil { + // return nil, err + // } + // err = stringToFloatPtr(&amount, ord[i].OrderConfiguration.LimitLimitGTC.BaseSize) + // if err != nil { + // return nil, err + // } + + // } + // if ord[i].OrderConfiguration.LimitLimitGTD != nil { + // err = stringToFloatPtr(&price, ord[i].OrderConfiguration.LimitLimitGTD.LimitPrice) + // if err != nil { + // return nil, err + // } + // err = stringToFloatPtr(&amount, ord[i].OrderConfiguration.LimitLimitGTD.BaseSize) + // if err != nil { + // return nil, err + // } + + // } + // if ord[i].OrderConfiguration.StopLimitStopLimitGTC != nil { + // err = stringToFloatPtr(&price, ord[i].OrderConfiguration.StopLimitStopLimitGTC.LimitPrice) + // if err != nil { + // return nil, err + // } + // err = stringToFloatPtr(&amount, ord[i].OrderConfiguration.StopLimitStopLimitGTC.BaseSize) + // if err != nil { + // return nil, err + // } + + // } + // if ord[i].OrderConfiguration.StopLimitStopLimitGTD != nil { + // err = stringToFloatPtr(&price, ord[i].OrderConfiguration.StopLimitStopLimitGTD.LimitPrice) + // if err != nil { + // return nil, err + // } + // err = stringToFloatPtr(&amount, ord[i].OrderConfiguration.StopLimitStopLimitGTD.BaseSize) + // if err != nil { + // return nil, err + // } + + // } + + // orders[i] = order.Detail{ + // ID: id, + // Exchange: c.Name, + // CurrencyPair: p, + // AssetType: assetType, + // Side: side, + // Price: price, + // Amount: amount, + // Timestamp: ord[i].CreatedTime, + // } + // } + + return req.Filter(c.Name, orders), nil + + // var respOrders []GetOrderResponse // if len(req.Pairs) > 0 { // var fPair currency.Pair // var resp []GetOrderResponse @@ -1333,56 +1337,56 @@ func (c *CoinbasePro) GetOrderHistory(ctx context.Context, req *order.MultiOrder // } // } - format, err := c.GetPairFormat(asset.Spot, false) - if err != nil { - return nil, err - } + // format, err := c.GetPairFormat(asset.Spot, false) + // if err != nil { + // return nil, err + // } - orders := make([]order.Detail, len(respOrders)) - for i := range respOrders { - var curr currency.Pair - curr, err = currency.NewPairDelimiter(respOrders[i].ProductID, - format.Delimiter) - if err != nil { - return nil, err - } - var side order.Side - side, err = order.StringToOrderSide(respOrders[i].Side) - if err != nil { - return nil, err - } - var orderStatus order.Status - orderStatus, err = order.StringToOrderStatus(respOrders[i].Status) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", c.Name, err) - } - var orderType order.Type - // orderType, err = order.StringToOrderType(respOrders[i].Type) - if err != nil { - log.Errorf(log.ExchangeSys, "%s %v", c.Name, err) - } - detail := order.Detail{ - OrderID: respOrders[i].OrderID, - // Amount: respOrders[i].Size, - ExecutedAmount: respOrders[i].FilledSize, - // RemainingAmount: respOrders[i].Size - respOrders[i].FilledSize, - // Cost: respOrders[i].ExecutedValue, - CostAsset: curr.Quote, - Type: orderType, - Date: respOrders[i].CreatedTime, - // CloseTime: respOrders[i].DoneAt, - // Fee: respOrders[i].FillFees, - FeeAsset: curr.Quote, - Side: side, - Status: orderStatus, - Pair: curr, - // Price: respOrders[i].Price, - Exchange: c.Name, - } - detail.InferCostsAndTimes() - orders[i] = detail - } - return req.Filter(c.Name, orders), nil + // orders := make([]order.Detail, len(respOrders)) + // for i := range respOrders { + // var curr currency.Pair + // curr, err = currency.NewPairDelimiter(respOrders[i].ProductID, + // format.Delimiter) + // if err != nil { + // return nil, err + // } + // var side order.Side + // side, err = order.StringToOrderSide(respOrders[i].Side) + // if err != nil { + // return nil, err + // } + // var orderStatus order.Status + // orderStatus, err = order.StringToOrderStatus(respOrders[i].Status) + // if err != nil { + // log.Errorf(log.ExchangeSys, "%s %v", c.Name, err) + // } + // var orderType order.Type + // // orderType, err = order.StringToOrderType(respOrders[i].Type) + // if err != nil { + // log.Errorf(log.ExchangeSys, "%s %v", c.Name, err) + // } + // detail := order.Detail{ + // OrderID: respOrders[i].OrderID, + // // Amount: respOrders[i].Size, + // ExecutedAmount: respOrders[i].FilledSize, + // // RemainingAmount: respOrders[i].Size - respOrders[i].FilledSize, + // // Cost: respOrders[i].ExecutedValue, + // CostAsset: curr.Quote, + // Type: orderType, + // Date: respOrders[i].CreatedTime, + // // CloseTime: respOrders[i].DoneAt, + // // Fee: respOrders[i].FillFees, + // FeeAsset: curr.Quote, + // Side: side, + // Status: orderStatus, + // Pair: curr, + // // Price: respOrders[i].Price, + // Exchange: c.Name, + // } + // detail.InferCostsAndTimes() + // orders[i] = detail + // } + // return req.Filter(c.Name, orders), nil } // GetHistoricCandles returns a set of candle between two time periods for a diff --git a/portfolio/withdraw/withdraw_types.go b/portfolio/withdraw/withdraw_types.go index 70551a7a484..1ff8cce099a 100644 --- a/portfolio/withdraw/withdraw_types.go +++ b/portfolio/withdraw/withdraw_types.go @@ -92,6 +92,9 @@ type Request struct { // Used exclusively in Binance.US ClientOrderID string `json:"clientID"` + // Currently used exclusively in Coinbase + WalletID string `json:"walletID"` + // Used exclusively in Okcoin to classify internal represented by '3' or on chain represented by '4' InternalTransfer bool