package kucoin import ( "context" "errors" "fmt" "sort" "strings" "time" "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" "github.com/thrasher-corp/gocryptotrader/exchange/websocket" "github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/margin" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" "github.com/thrasher-corp/gocryptotrader/log" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) // SetDefaults sets the basic defaults for Kucoin func (e *Exchange) SetDefaults() { e.Name = "Kucoin" e.Enabled = true e.Verbose = false e.API.CredentialsValidator.RequiresKey = true e.API.CredentialsValidator.RequiresSecret = true e.API.CredentialsValidator.RequiresClientID = true for _, a := range []asset.Item{asset.Spot, asset.Margin, asset.Futures} { ps := currency.PairStore{ AssetEnabled: true, RequestFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}, ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}, } if a == asset.Futures { ps.RequestFormat.Delimiter = "" ps.ConfigFormat.Delimiter = currency.UnderscoreDelimiter } if err := e.SetAssetPairStore(a, ps); err != nil { log.Errorf(log.ExchangeSys, "%s error storing %q default asset formats: %s", e.Name, a, err) } } e.Features = exchange.Features{ CurrencyTranslations: currency.NewTranslations(map[currency.Code]currency.Code{ currency.XBT: currency.BTC, currency.USDTM: currency.USDT, currency.USDM: currency.USD, currency.USDCM: currency.USDC, }), TradingRequirements: protocol.TradingRequirements{ ClientOrderID: true, }, Supports: exchange.FeaturesSupported{ REST: true, Websocket: true, RESTCapabilities: protocol.Features{ TickerFetching: true, TickerBatching: true, OrderbookFetching: true, AutoPairUpdates: true, AccountInfo: true, CryptoWithdrawal: true, SubmitOrder: true, GetOrder: true, GetOrders: true, CancelOrder: true, CancelOrders: true, TradeFetching: true, UserTradeHistory: true, KlineFetching: true, DepositHistory: true, WithdrawalHistory: true, }, WebsocketCapabilities: protocol.Features{ TickerFetching: true, OrderbookFetching: true, Subscribe: true, Unsubscribe: true, AuthenticatedEndpoints: true, AccountInfo: true, GetOrders: true, TradeFetching: true, KlineFetching: true, GetOrder: true, }, FuturesCapabilities: exchange.FuturesCapabilities{ Positions: true, Leverage: true, CollateralMode: true, FundingRates: true, MaximumFundingRateHistory: kline.ThreeMonth.Duration(), SupportedFundingRateFrequencies: map[kline.Interval]bool{ kline.EightHour: true, }, FundingRateBatching: map[asset.Item]bool{ asset.Futures: true, }, OpenInterest: exchange.OpenInterestSupport{ Supported: true, SupportedViaTicker: true, SupportsRestBatch: true, }, }, MaximumOrderHistory: kline.OneDay.Duration() * 7, WithdrawPermissions: exchange.AutoWithdrawCrypto, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( kline.IntervalCapacity{Interval: kline.OneMin}, kline.IntervalCapacity{Interval: kline.ThreeMin}, kline.IntervalCapacity{Interval: kline.FiveMin}, kline.IntervalCapacity{Interval: kline.FifteenMin}, kline.IntervalCapacity{Interval: kline.ThirtyMin}, kline.IntervalCapacity{Interval: kline.OneHour}, kline.IntervalCapacity{Interval: kline.TwoHour}, kline.IntervalCapacity{Interval: kline.FourHour}, kline.IntervalCapacity{Interval: kline.SixHour}, kline.IntervalCapacity{Interval: kline.EightHour}, kline.IntervalCapacity{Interval: kline.TwelveHour}, kline.IntervalCapacity{Interval: kline.OneDay}, kline.IntervalCapacity{Interval: kline.OneWeek}, ), GlobalResultLimit: 500, }, }, Subscriptions: defaultSubscriptions.Clone(), } var err error e.Requester, err = request.New(e.Name, common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), request.WithLimiter(GetRateLimit())) if err != nil { log.Errorln(log.ExchangeSys, err) } e.API.Endpoints = e.NewEndpoints() err = e.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{ exchange.RestSpot: kucoinAPIURL, exchange.RestFutures: kucoinFuturesAPIURL, exchange.WebsocketSpot: kucoinWebsocketURL, }) if err != nil { log.Errorln(log.ExchangeSys, err) } e.Websocket = websocket.NewManager() e.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit e.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout e.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit } // Setup takes in the supplied exchange configuration details and sets params func (e *Exchange) Setup(exch *config.Exchange) error { err := exch.Validate() if err != nil { return err } if !exch.Enabled { e.SetEnabled(false) return nil } err = e.SetupDefaults(exch) if err != nil { return err } e.checkSubscriptions() wsRunningEndpoint, err := e.API.Endpoints.GetURL(exchange.WebsocketSpot) if err != nil { return err } err = e.Websocket.Setup( &websocket.ManagerSetup{ ExchangeConfig: exch, DefaultURL: kucoinWebsocketURL, RunningURL: wsRunningEndpoint, Connector: e.WsConnect, Subscriber: e.Subscribe, Unsubscriber: e.Unsubscribe, GenerateSubscriptions: e.generateSubscriptions, Features: &e.Features.Supports.WebsocketCapabilities, OrderbookBufferConfig: buffer.Config{ SortBuffer: true, SortBufferByUpdateIDs: true, }, TradeFeed: e.Features.Enabled.TradeFeed, }) if err != nil { return err } return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{ ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, ResponseMaxLimit: exch.WebsocketResponseMaxLimit, RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1), }) } // FetchTradablePairs returns a list of the exchanges tradable pairs func (e *Exchange) FetchTradablePairs(ctx context.Context, assetType asset.Item) (currency.Pairs, error) { var cp currency.Pair switch assetType { case asset.Futures: myPairs, err := e.GetFuturesOpenContracts(ctx) if err != nil { return nil, err } pairs := make(currency.Pairs, 0, len(myPairs)) for x := range myPairs { if strings.ToLower(myPairs[x].Status) != "open" { //nolint:gocritic // strings.ToLower is faster continue } cp, err = currency.NewPairFromStrings(myPairs[x].BaseCurrency, myPairs[x].Symbol[len(myPairs[x].BaseCurrency):]) if err != nil { return nil, err } pairs = pairs.Add(cp) } configFormat, err := e.GetPairFormat(asset.Futures, false) if err != nil { return nil, err } return pairs.Format(configFormat), nil case asset.Spot, asset.Margin: myPairs, err := e.GetSymbols(ctx, "") if err != nil { return nil, err } pairs := make(currency.Pairs, 0, len(myPairs)) for x := range myPairs { if !myPairs[x].EnableTrading || (assetType == asset.Margin && !myPairs[x].IsMarginEnabled) { continue } // Symbol field must be used to generate pair as this is the symbol // to fetch data from the API. e.g. BSV-USDT name is BCHSV-USDT as symbol. cp, err = currency.NewPairFromString(strings.ToUpper(myPairs[x].Symbol)) if err != nil { return nil, err } pairs = pairs.Add(cp) } return pairs, nil default: return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } } // UpdateTradablePairs updates the exchanges available pairs and stores // them in the exchanges config func (e *Exchange) UpdateTradablePairs(ctx context.Context) error { assets := e.GetAssetTypes(true) for a := range assets { pairs, err := e.FetchTradablePairs(ctx, assets[a]) if err != nil { return err } if err := e.UpdatePairs(pairs, assets[a], false); err != nil { return err } } return nil } // UpdateTicker updates and returns the ticker for a currency pair func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) { p, err := e.FormatExchangeCurrency(p, assetType) if err != nil { return nil, err } if err := e.UpdateTickers(ctx, assetType); err != nil { return nil, err } return ticker.GetTicker(e.Name, p, assetType) } // UpdateTickers updates all currency pairs of a given asset type func (e *Exchange) UpdateTickers(ctx context.Context, assetType asset.Item) error { var errs error switch assetType { case asset.Futures: ticks, err := e.GetFuturesOpenContracts(ctx) if err != nil { return err } pairs, err := e.GetEnabledPairs(asset.Futures) if err != nil { return err } for x := range ticks { var pair currency.Pair pair, err = currency.NewPairFromStrings(ticks[x].BaseCurrency, ticks[x].Symbol[len(ticks[x].BaseCurrency):]) if err != nil { return err } if !pairs.Contains(pair, true) { continue } err = ticker.ProcessTicker(&ticker.Price{ Last: ticks[x].LastTradePrice, High: ticks[x].HighPrice, Low: ticks[x].LowPrice, Volume: ticks[x].VolumeOf24h, OpenInterest: ticks[x].OpenInterest.Float64(), Pair: pair, ExchangeName: e.Name, AssetType: assetType, }) if err != nil { errs = common.AppendError(errs, err) } } case asset.Spot, asset.Margin: ticks, err := e.GetTickers(ctx) if err != nil { return err } for t := range ticks.Tickers { pair, enabled, err := e.MatchSymbolCheckEnabled(ticks.Tickers[t].Symbol, assetType, true) if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return err } if !enabled { continue } err = ticker.ProcessTicker(&ticker.Price{ Last: ticks.Tickers[t].Last, High: ticks.Tickers[t].High, Low: ticks.Tickers[t].Low, Volume: ticks.Tickers[t].Volume, Ask: ticks.Tickers[t].Sell, Bid: ticks.Tickers[t].Buy, Pair: pair, ExchangeName: e.Name, AssetType: assetType, LastUpdated: ticks.Time.Time(), }) if err != nil { errs = common.AppendError(errs, err) } } default: return fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } return errs } // UpdateOrderbook updates and returns the orderbook for a currency pair func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, a asset.Item) (*orderbook.Book, error) { err := e.CurrencyPairs.IsAssetEnabled(a) if err != nil { return nil, err } p, err = e.FormatExchangeCurrency(p, a) if err != nil { return nil, err } var ordBook *Orderbook switch a { case asset.Futures: ordBook, err = e.GetFuturesOrderbook(ctx, p.String()) case asset.Spot, asset.Margin: ordBook, err = e.GetPartOrderbook100(ctx, p.String()) default: return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) } if err != nil { return nil, err } ob := &orderbook.Book{ Exchange: e.Name, Pair: p, Asset: a, ValidateOrderbook: e.ValidateOrderbook, Asks: ordBook.Asks, Bids: ordBook.Bids, } if err := ob.Process(); err != nil { return nil, err } return orderbook.Get(e.Name, p, a) } // UpdateAccountBalances retrieves currency balances func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { if err := e.CurrencyPairs.IsAssetEnabled(assetType); err != nil { return nil, fmt.Errorf("%w: %q", asset.ErrNotSupported, assetType) } subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} switch assetType { case asset.Futures: for _, settlement := range []string{"XBT", "USDT"} { resp, err := e.GetFuturesAccountOverview(ctx, settlement) if err != nil { return nil, err } subAccts[0].Balances.Set(resp.Currency, accounts.Balance{ Total: resp.AvailableBalance + resp.FrozenFunds, Hold: resp.FrozenFunds, Free: resp.AvailableBalance, }) } case asset.Spot, asset.Margin: resp, err := e.GetAllAccounts(ctx, currency.EMPTYCODE, "") if err != nil { return nil, err } for i := range resp { if resp[i].AccountType == "margin" && assetType == asset.Spot { continue } else if resp[i].AccountType == "trade" && assetType == asset.Margin { continue } subAccts[0].Balances.Set(resp[i].Currency, accounts.Balance{ Total: resp[i].Balance.Float64(), Hold: resp[i].Holds.Float64(), Free: resp[i].Available.Float64(), }) } default: return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and // withdrawals func (e *Exchange) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) { withdrawalsData, err := e.GetWithdrawalList(ctx, currency.EMPTYCODE, "", time.Time{}, time.Time{}) if err != nil { return nil, err } depositsData, err := e.GetHistoricalDepositList(ctx, currency.EMPTYCODE, "", time.Time{}, time.Time{}) if err != nil { return nil, err } fundingData := make([]exchange.FundingHistory, len(withdrawalsData.Items)+len(depositsData.Items)) for x := range depositsData.Items { fundingData[x] = exchange.FundingHistory{ Timestamp: depositsData.Items[x].CreatedAt.Time(), ExchangeName: e.Name, TransferType: "deposit", CryptoTxID: depositsData.Items[x].WalletTxID, Status: depositsData.Items[x].Status, Amount: depositsData.Items[x].Amount, Currency: depositsData.Items[x].Currency, } } length := len(depositsData.Items) for x := range withdrawalsData.Items { fundingData[length+x] = exchange.FundingHistory{ Fee: withdrawalsData.Items[x].Fee, Timestamp: withdrawalsData.Items[x].UpdatedAt.Time(), ExchangeName: e.Name, TransferType: "withdrawal", CryptoToAddress: withdrawalsData.Items[x].Address, CryptoTxID: withdrawalsData.Items[x].WalletTxID, Status: withdrawalsData.Items[x].Status, Amount: withdrawalsData.Items[x].Amount, Currency: withdrawalsData.Items[x].Currency, TransferID: withdrawalsData.Items[x].ID, } } return fundingData, nil } // GetWithdrawalsHistory returns previous withdrawals data func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, c currency.Code, assetType asset.Item) ([]exchange.WithdrawalHistory, error) { if !e.SupportsAsset(assetType) { return nil, asset.ErrNotSupported } var withdrawals *HistoricalDepositWithdrawalResponse withdrawals, err := e.GetHistoricalWithdrawalList(ctx, c.Upper(), "", time.Time{}, time.Time{}) if err != nil { return nil, err } resp := make([]exchange.WithdrawalHistory, len(withdrawals.Items)) for x := range withdrawals.Items { resp[x] = exchange.WithdrawalHistory{ Status: withdrawals.Items[x].Status, CryptoTxID: withdrawals.Items[x].WalletTxID, Timestamp: withdrawals.Items[x].CreatedAt.Time(), Amount: withdrawals.Items[x].Amount, TransferType: "withdrawal", Currency: c.String(), } } return resp, nil } // GetRecentTrades returns the most recent trades for a currency and asset func (e *Exchange) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) { p, err := e.FormatExchangeCurrency(p, assetType) if err != nil { return nil, err } var resp []trade.Data switch assetType { case asset.Futures: tradeData, err := e.GetFuturesTradeHistory(ctx, p.String()) if err != nil { return nil, err } var side order.Side for i := range tradeData { side, err = order.StringToOrderSide(tradeData[0].Side) if err != nil { return nil, err } resp = append(resp, trade.Data{ TID: tradeData[i].TradeID, Exchange: e.Name, CurrencyPair: p, AssetType: assetType, Price: tradeData[i].Price, Amount: tradeData[i].Size, Timestamp: tradeData[i].FilledTime.Time(), Side: side, }) } case asset.Spot, asset.Margin: tradeData, err := e.GetTradeHistory(ctx, p.String()) if err != nil { return nil, err } var side order.Side for i := range tradeData { side, err = order.StringToOrderSide(tradeData[0].Side) if err != nil { return nil, err } resp = append(resp, trade.Data{ TID: tradeData[i].Sequence, Exchange: e.Name, CurrencyPair: p, Side: side, AssetType: assetType, Price: tradeData[i].Price, Amount: tradeData[i].Size, Timestamp: tradeData[i].Time.Time(), }) } default: return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } if e.IsSaveTradeDataEnabled() { err := trade.AddTradesToBuffer(resp...) if err != nil { return nil, err } } sort.Sort(trade.ByDate(resp)) return resp, nil } // GetHistoricTrades returns historic trade data within the timeframe provided func (e *Exchange) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) { return nil, common.ErrFunctionNotSupported } // SubmitOrder submits a new order // For OCO (One Cancels the Other) orders, the StopLoss parameters under the order submission argument field RiskManagementModes are treated as stop values, // and the TakeProfit parameters are treated as limit order. func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { sideString, err := e.OrderSideString(s.Side) if err != nil { return nil, err } s.Pair, err = e.FormatExchangeCurrency(s.Pair, s.AssetType) if err != nil { return nil, err } var o string switch s.AssetType { case asset.Futures: if s.Leverage == 0 { s.Leverage = 1 } var orderType, stopOrderType, stopOrderBoundary string switch s.Type { case order.Stop, order.StopLimit, order.TrailingStop: orderType = "limit" if s.TriggerPrice == 0 { break } switch s.TriggerPriceType { case order.IndexPrice: stopOrderType = "IP" case order.MarkPrice: stopOrderType = "MP" case order.LastPrice: stopOrderType = "TP" } switch s.Type { case order.StopLimit: switch s.Side { case order.Sell: stopOrderBoundary = "up" case order.Buy: stopOrderBoundary = "down" } case order.TrailingStop, order.Stop: switch s.Side { case order.Sell: // Stop-loss when order type is order.Stop stopOrderBoundary = "down" case order.Buy: // Take Profit when order type is order.Stop stopOrderBoundary = "up" } } case order.Market, order.Limit: orderType = s.Type.Lower() default: return nil, order.ErrUnsupportedOrderType } o, err = e.PostFuturesOrder(ctx, &FuturesOrderParam{ ClientOrderID: s.ClientOrderID, Side: sideString, Symbol: s.Pair, OrderType: orderType, Size: s.Amount, Price: s.Price, Leverage: s.Leverage, VisibleSize: 0, ReduceOnly: s.ReduceOnly, PostOnly: s.TimeInForce.Is(order.PostOnly), Hidden: s.Hidden, Stop: stopOrderBoundary, StopPrice: s.TriggerPrice, StopPriceType: stopOrderType, Iceberg: s.Iceberg, }) if err != nil { return nil, err } return s.DeriveSubmitResponse(o) case asset.Spot: switch s.Type { case order.Limit, order.Market, order.StopLimit, order.StopMarket: var oType order.Type switch s.Type { case order.Limit, order.StopLimit: oType = order.Limit case order.Market, order.StopMarket: oType = order.Market } var timeInForce string if oType == order.Limit { switch { case s.TimeInForce.Is(order.FillOrKill) || s.TimeInForce.Is(order.ImmediateOrCancel): timeInForce = s.TimeInForce.String() default: timeInForce = order.GoodTillCancel.String() } } var stopType string var stopPrice float64 switch { case s.RiskManagementModes.StopLoss.Enabled && s.RiskManagementModes.StopEntry.Enabled: return nil, errors.New("can not enable more than one risk management") case s.RiskManagementModes.StopEntry.Enabled: stopType = "entry" stopPrice = s.RiskManagementModes.StopEntry.Price case s.RiskManagementModes.StopLoss.Enabled: stopType = "loss" stopPrice = s.RiskManagementModes.StopLoss.Price } var o string if stopType != "" { o, err = e.PostStopOrder(ctx, s.ClientOrderID, sideString, s.Pair.String(), oType.Lower(), "", stopType, "", SpotTradeType, timeInForce, s.Amount, s.Price, stopPrice, 0, 0, 0, s.TimeInForce.Is(order.PostOnly), s.Hidden, s.Iceberg) if err != nil { return nil, err } return s.DeriveSubmitResponse(o) } o, err = e.PostOrder(ctx, &SpotOrderParam{ ClientOrderID: s.ClientOrderID, Side: sideString, Symbol: s.Pair, OrderType: s.Type.Lower(), Size: s.Amount, Price: s.Price, PostOnly: s.TimeInForce.Is(order.PostOnly), Hidden: s.Hidden, TimeInForce: timeInForce, Iceberg: s.Iceberg, TradeType: SpotTradeType, ReduceOnly: s.ReduceOnly, }) if err != nil { return nil, err } return s.DeriveSubmitResponse(o) case order.OCO: switch { case !s.RiskManagementModes.TakeProfit.Enabled || s.RiskManagementModes.TakeProfit.Price <= 0: return nil, errors.New("take profit price is required") case !s.RiskManagementModes.StopLoss.Enabled || s.RiskManagementModes.StopLoss.Price <= 0: return nil, errors.New("stop loss price is required") } switch s.Side { case order.Sell: if s.RiskManagementModes.TakeProfit.Price <= s.RiskManagementModes.StopLoss.Price { return nil, errors.New("stop loss price must be below take profit trigger price for sell orders") } case order.Buy: if s.RiskManagementModes.TakeProfit.Price >= s.RiskManagementModes.StopLoss.Price { return nil, errors.New("stop loss price must be greater than take profit trigger price for buy orders") } } limitPrice := s.RiskManagementModes.TakeProfit.Price stopPrice := s.RiskManagementModes.StopLoss.Price var o string o, err = e.PlaceOCOOrder(ctx, &OCOOrderParams{ Symbol: s.Pair, Side: sideString, Price: s.Price, Size: s.Amount, StopPrice: stopPrice, LimitPrice: limitPrice, ClientOrderID: s.ClientOrderID, }) if err != nil { return nil, err } return s.DeriveSubmitResponse(o) default: return nil, order.ErrUnsupportedOrderType } case asset.Margin: o, err := e.PostMarginOrder(ctx, &MarginOrderParam{ ClientOrderID: s.ClientOrderID, Side: sideString, Symbol: s.Pair, OrderType: s.Type.Lower(), MarginModel: MarginModeToString(s.MarginType), Price: s.Price, Size: s.Amount, VisibleSize: s.Amount, PostOnly: s.TimeInForce.Is(order.PostOnly), Hidden: s.Hidden, AutoBorrow: s.AutoBorrow, AutoRepay: s.AutoBorrow, Iceberg: s.Iceberg, }) if err != nil { return nil, err } ret, err := s.DeriveSubmitResponse(o.OrderID) if err != nil { return nil, err } ret.BorrowSize = o.BorrowSize ret.LoanApplyID = o.LoanApplyID return ret, nil default: return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, s.AssetType) } } // MarginModeToString returns a string representation of a MarginMode func MarginModeToString(mType margin.Type) string { switch mType { case margin.Isolated: return mType.String() case margin.Multi: return "cross" default: return "" } } // ModifyOrder modifies an existing order func (e *Exchange) ModifyOrder(context.Context, *order.Modify) (*order.ModifyResponse, error) { return nil, common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number func (e *Exchange) CancelOrder(ctx context.Context, ord *order.Cancel) error { if ord == nil { return common.ErrNilPointer } err := e.CurrencyPairs.IsAssetEnabled(ord.AssetType) if err != nil { return err } pairFormat, err := e.GetPairFormat(ord.AssetType, true) if err != nil { return err } ord.Pair = ord.Pair.Format(pairFormat) switch ord.AssetType { case asset.Spot, asset.Margin: if ord.OrderID == "" && ord.ClientOrderID == "" { return fmt.Errorf("%w, either clientOrderID or OrderID is required", order.ErrOrderIDNotSet) } switch ord.Type { case order.OCO: if ord.OrderID != "" { _, err = e.CancelOCOOrderByOrderID(ctx, ord.OrderID) } else if ord.ClientOrderID != "" { _, err = e.CancelOCOOrderByClientOrderID(ctx, ord.ClientOrderID) } case order.Stop, order.StopLimit: if ord.OrderID != "" { _, err = e.CancelStopOrder(ctx, ord.OrderID) } else { _, err = e.CancelStopOrderByClientOrderID(ctx, ord.ClientOrderID, ord.Pair.String()) } default: if ord.OrderID != "" { _, err = e.CancelSingleOrder(ctx, ord.OrderID) } else { _, err = e.CancelOrderByClientOID(ctx, ord.ClientOrderID) } } return err case asset.Futures: if ord.OrderID == "" && ord.ClientOrderID == "" { return fmt.Errorf("%w, either clientOrderID or OrderID is required", order.ErrOrderIDNotSet) } if ord.OrderID == "" { if ord.Pair.IsEmpty() { return fmt.Errorf("%w, symbol information is required", currency.ErrCurrencyPairEmpty) } _, err = e.CancelFuturesOrderByClientOrderID(ctx, ord.Pair.String(), ord.ClientOrderID) } else { _, err = e.CancelFuturesOrderByOrderID(ctx, ord.OrderID) } if err != nil { return err } default: return fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, ord.AssetType) } return nil } // CancelBatchOrders cancels orders by their corresponding ID numbers func (e *Exchange) CancelBatchOrders(_ context.Context, _ []order.Cancel) (*order.CancelBatchResponse, error) { return nil, common.ErrFunctionNotSupported } // CancelAllOrders cancels all orders associated with a currency pair func (e *Exchange) CancelAllOrders(ctx context.Context, orderCancellation *order.Cancel) (order.CancelAllResponse, error) { if orderCancellation == nil { return order.CancelAllResponse{}, common.ErrNilPointer } err := e.CurrencyPairs.IsAssetEnabled(orderCancellation.AssetType) if err != nil { return order.CancelAllResponse{}, err } result := order.CancelAllResponse{} err = orderCancellation.Validate() if err != nil { return result, err } var pairString string if !orderCancellation.Pair.IsEmpty() { orderCancellation.Pair, err = e.FormatExchangeCurrency(orderCancellation.Pair, orderCancellation.AssetType) if err != nil { return result, err } pairString = orderCancellation.Pair.String() } var values []string switch orderCancellation.AssetType { case asset.Margin, asset.Spot: var orderIDs []string if orderCancellation.OrderID != "" { orderIDs = append(orderIDs, orderCancellation.OrderID) } if orderCancellation.ClientOrderID != "" { orderIDs = append(orderIDs, orderCancellation.ClientOrderID) } switch orderCancellation.Type { case order.OCO: var response *OCOOrderCancellationResponse response, err = e.CancelOCOMultipleOrders(ctx, orderIDs, orderCancellation.Pair.String()) if err != nil { return order.CancelAllResponse{}, err } values = response.CancelledOrderIDs case order.Stop, order.StopLimit: values, err = e.CancelStopOrders(ctx, orderCancellation.Pair.String(), e.AccountToTradeTypeString(orderCancellation.AssetType, MarginModeToString(orderCancellation.MarginType)), orderIDs) if err != nil { return order.CancelAllResponse{}, err } default: tradeType := e.AccountToTradeTypeString(orderCancellation.AssetType, MarginModeToString(orderCancellation.MarginType)) values, err = e.CancelAllOpenOrders(ctx, pairString, tradeType) if err != nil { return order.CancelAllResponse{}, err } } case asset.Futures: values, err = e.CancelMultipleFuturesLimitOrders(ctx, orderCancellation.Pair.String()) if err != nil { return result, err } stopOrders, err := e.CancelAllFuturesStopOrders(ctx, orderCancellation.Pair.String()) if err != nil { return result, err } values = append(values, stopOrders...) default: return order.CancelAllResponse{}, fmt.Errorf("%w %v", asset.ErrNotSupported, orderCancellation.AssetType) } result.Status = map[string]string{} for x := range values { result.Status[values[x]] = order.Cancelled.String() } return result, nil } // GetOrderInfo returns order information based on order ID func (e *Exchange) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (*order.Detail, error) { err := e.CurrencyPairs.IsAssetEnabled(assetType) if err != nil { return nil, err } pair, err = e.FormatExchangeCurrency(pair, assetType) if err != nil { return nil, err } switch assetType { case asset.Futures: var orderDetail *FuturesOrder orderDetail, err = e.GetFuturesOrderDetails(ctx, orderID, "") if err != nil { return nil, err } var nPair currency.Pair nPair, err = currency.NewPairFromString(orderDetail.Symbol) if err != nil { return nil, err } var oType order.Type oType, err = order.StringToOrderType(orderDetail.OrderType) if err != nil { return nil, err } var side order.Side side, err = order.StringToOrderSide(orderDetail.Side) if err != nil { return nil, err } switch side { case order.Sell: side = order.Short case order.Buy: side = order.Long } if !pair.IsEmpty() && !nPair.Equal(pair) { return nil, fmt.Errorf("order with id %s and symbol %v does not exist", orderID, pair) } var oStatus order.Status if orderDetail.IsActive { oStatus = order.Active } else { oStatus = order.Closed } return &order.Detail{ Exchange: e.Name, OrderID: orderDetail.ID, Pair: pair, Type: oType, Side: side, AssetType: assetType, ExecutedAmount: orderDetail.DealSize, RemainingAmount: orderDetail.Size - orderDetail.DealSize, Amount: orderDetail.Size, Price: orderDetail.Price, Date: orderDetail.CreatedAt.Time(), HiddenOrder: orderDetail.Hidden, TimeInForce: StringToTimeInForce(orderDetail.TimeInForce, orderDetail.PostOnly), ReduceOnly: orderDetail.ReduceOnly, Leverage: orderDetail.Leverage, AverageExecutedPrice: orderDetail.Price, QuoteAmount: orderDetail.Size, ClientOrderID: orderDetail.ClientOid, Status: oStatus, CloseTime: orderDetail.EndAt.Time(), LastUpdated: orderDetail.UpdatedAt.Time(), }, nil case asset.Spot, asset.Margin: orderDetail, err := e.GetOrderByID(ctx, orderID) if err != nil { return nil, err } nPair, err := currency.NewPairFromString(orderDetail.Symbol) if err != nil { return nil, err } oType, err := order.StringToOrderType(orderDetail.Type) if err != nil { return nil, err } side, err := order.StringToOrderSide(orderDetail.Side) if err != nil { return nil, err } if !pair.IsEmpty() && !nPair.Equal(pair) { return nil, fmt.Errorf("order with id %s and currency Pair %v does not exist", orderID, pair) } var oStatus order.Status if orderDetail.IsActive { oStatus = order.Active } else { oStatus = order.Closed } var orderAssetType asset.Item var mType margin.Type switch orderDetail.TradeType { case SpotTradeType: orderAssetType = asset.Spot case CrossMarginTradeType: mType = margin.Multi orderAssetType = asset.Margin case IsolatedMarginTradeType: mType = margin.Isolated orderAssetType = asset.Margin } if orderAssetType != assetType { return nil, fmt.Errorf("%w, expected order asset type %v, got %v", asset.ErrInvalidAsset, assetType, orderAssetType) } var remainingSize float64 if orderDetail.RemainSize.Float64() != 0 { remainingSize = orderDetail.RemainSize.Float64() } else { remainingSize = orderDetail.Size.Float64() - orderDetail.DealSize.Float64() } return &order.Detail{ Exchange: e.Name, OrderID: orderDetail.ID, Pair: pair, Type: oType, Side: side, Fee: orderDetail.Fee.Float64(), AssetType: assetType, ExecutedAmount: orderDetail.DealSize.Float64(), RemainingAmount: remainingSize, Amount: orderDetail.Size.Float64(), Price: orderDetail.Price.Float64(), Date: orderDetail.CreatedAt.Time(), HiddenOrder: orderDetail.Hidden, TimeInForce: StringToTimeInForce(orderDetail.TimeInForce, orderDetail.PostOnly), AverageExecutedPrice: orderDetail.Price.Float64(), FeeAsset: currency.NewCode(orderDetail.FeeCurrency), ClientOrderID: orderDetail.ClientOID, Status: oStatus, CloseTime: orderDetail.CreatedAt.Time(), MarginType: mType, LastUpdated: orderDetail.LastUpdatedAt.Time(), }, nil default: return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } } // GetDepositAddress returns a deposit address for a specified currency func (e *Exchange) GetDepositAddress(ctx context.Context, c currency.Code, _, chain string) (*deposit.Address, error) { ad, err := e.GetDepositAddressesV2(ctx, c.Upper()) if err != nil { return nil, err } if chain != "" { // check if there is a matching chain address. for a := range ad { if strings.EqualFold(ad[a].Chain, chain) { return &deposit.Address{ Address: ad[a].Address, Chain: ad[a].Chain, Tag: ad[a].Memo, }, nil } } return nil, fmt.Errorf("%w matching the chain name %s", errNoDepositAddress, chain) } if len(ad) > 1 { return nil, errMultipleDepositAddress } else if len(ad) == 0 { return nil, errNoDepositAddress } return &deposit.Address{ Address: ad[0].Address, Chain: ad[0].Chain, Tag: ad[0].Memo, }, nil } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted // The endpoint was deprecated for futures, please transfer assets from the FUTURES account to the MAIN account first, // and then withdraw from the MAIN account func (e *Exchange) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { if err := withdrawRequest.Validate(); err != nil { return nil, err } withdrawalID, err := e.ApplyWithdrawal(ctx, withdrawRequest.Currency, withdrawRequest.Crypto.Address, withdrawRequest.Crypto.AddressTag, withdrawRequest.Description, withdrawRequest.Crypto.Chain, "INTERNAL", withdrawRequest.InternalTransfer, withdrawRequest.Amount) if err != nil { return nil, err } return &withdraw.ExchangeResponse{ ID: withdrawalID, }, nil } // WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted func (e *Exchange) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is // submitted func (e *Exchange) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // OrderTypeToString returns an order type instance from string. func OrderTypeToString(oType order.Type) (string, error) { switch oType { case order.Limit: return "limit", nil case order.Market: return "market", nil case order.StopLimit: return "limit_stop", nil case order.StopMarket: return "market_stop", nil case order.AnyType, order.UnknownType: return "", nil default: return "", order.ErrUnsupportedOrderType } } // GetActiveOrders retrieves any orders that are active/open func (e *Exchange) GetActiveOrders(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) { if getOrdersRequest == nil { return nil, common.ErrNilPointer } err := e.CurrencyPairs.IsAssetEnabled(getOrdersRequest.AssetType) if err != nil { return nil, err } if err := getOrdersRequest.Validate(); err != nil { return nil, err } format, err := e.GetPairFormat(getOrdersRequest.AssetType, true) if err != nil { return nil, err } getOrdersRequest.Pairs = getOrdersRequest.Pairs.Format(format) var pair string var orders []order.Detail switch getOrdersRequest.AssetType { case asset.Futures: if len(getOrdersRequest.Pairs) == 1 { pair = format.Format(getOrdersRequest.Pairs[0]) } sideString, err := e.OrderSideString(getOrdersRequest.Side) if err != nil { return nil, err } oType, err := OrderTypeToString(getOrdersRequest.Type) if err != nil { return nil, err } futuresOrders, err := e.GetFuturesOrders(ctx, "active", pair, sideString, oType, getOrdersRequest.StartTime, getOrdersRequest.EndTime) if err != nil { return nil, err } for x := range futuresOrders.Items { if !futuresOrders.Items[x].IsActive { continue } var dPair currency.Pair var enabled bool dPair, enabled, err = e.MatchSymbolCheckEnabled(futuresOrders.Items[x].Symbol, getOrdersRequest.AssetType, false) if err != nil { return nil, err } if !enabled { continue } side, err := order.StringToOrderSide(futuresOrders.Items[x].Side) if err != nil { return nil, err } switch side { case order.Sell: side = order.Short case order.Buy: side = order.Long } oType, err := order.StringToOrderType(futuresOrders.Items[x].OrderType) if err != nil { return nil, fmt.Errorf("asset type: %v order type: %v err: %w", getOrdersRequest.AssetType, getOrdersRequest.Type, err) } status, err := order.StringToOrderStatus(futuresOrders.Items[x].Status) if err != nil { return nil, err } orders = append(orders, order.Detail{ OrderID: futuresOrders.Items[x].ID, ClientOrderID: futuresOrders.Items[x].ClientOid, Amount: futuresOrders.Items[x].Size, ContractAmount: futuresOrders.Items[x].Size, RemainingAmount: futuresOrders.Items[x].Size - futuresOrders.Items[x].FilledSize, ExecutedAmount: futuresOrders.Items[x].FilledSize, Exchange: e.Name, Date: futuresOrders.Items[x].CreatedAt.Time(), LastUpdated: futuresOrders.Items[x].UpdatedAt.Time(), Price: futuresOrders.Items[x].Price, Side: side, Type: oType, Pair: dPair, TimeInForce: StringToTimeInForce(futuresOrders.Items[x].TimeInForce, futuresOrders.Items[x].PostOnly), ReduceOnly: futuresOrders.Items[x].ReduceOnly, Status: status, SettlementCurrency: currency.NewCode(futuresOrders.Items[x].SettleCurrency), Leverage: futuresOrders.Items[x].Leverage, AssetType: getOrdersRequest.AssetType, HiddenOrder: futuresOrders.Items[x].Hidden, }) } case asset.Spot, asset.Margin: var singlePair currency.Pair if len(getOrdersRequest.Pairs) == 1 { singlePair = getOrdersRequest.Pairs[0] } switch getOrdersRequest.Type { case order.OCO: response, err := e.GetOCOOrderList(ctx, 500, 1, singlePair.String(), getOrdersRequest.StartTime, getOrdersRequest.EndTime, []string{}) if err != nil { return nil, err } for a := range response.Items { if response.Items[a].Status != "NEW" { continue } cp, err := currency.NewPairFromString(response.Items[a].Symbol) if err != nil { return nil, err } if len(getOrdersRequest.Pairs) > 1 && !getOrdersRequest.Pairs.Contains(cp, true) { continue } status, err := order.StringToOrderStatus(response.Items[a].Status) if err != nil { return nil, err } orders = append(orders, order.Detail{ OrderID: response.Items[a].OrderID, ClientOrderID: response.Items[a].ClientOrderID, Exchange: e.Name, LastUpdated: response.Items[a].OrderTime.Time(), Type: order.OCO, Pair: cp, Status: status, }) } case order.Stop, order.StopLimit, order.StopMarket, order.ConditionalStop: // NOTE: The orderType values 'limit', 'market', 'limit_stop', and 'market_stop' trigger an "The order type is invalid" error. // As a result, these options are currently unavailable. tradeType := SpotTradeType if getOrdersRequest.AssetType == asset.Margin { if getOrdersRequest.MarginType == margin.Multi { tradeType = CrossMarginTradeType } else { tradeType = IsolatedMarginTradeType } } response, err := e.ListStopOrders(ctx, singlePair.String(), getOrdersRequest.Side.Lower(), "", tradeType, []string{getOrdersRequest.FromOrderID}, getOrdersRequest.StartTime, getOrdersRequest.EndTime, 0, 0) if err != nil { return nil, err } for a := range response.Items { if response.Items[a].Status != "New" { continue } var dPair currency.Pair var enabled bool dPair, enabled, err = e.MatchSymbolCheckEnabled(response.Items[a].Symbol, getOrdersRequest.AssetType, false) if err != nil { return nil, err } if !enabled { continue } if len(getOrdersRequest.Pairs) > 1 && !getOrdersRequest.Pairs.Contains(dPair, true) { continue } side, err := order.StringToOrderSide(response.Items[a].Side) if err != nil { return nil, err } status, err := order.StringToOrderStatus(response.Items[a].Status) if err != nil { return nil, err } orders = append(orders, order.Detail{ OrderID: response.Items[a].ID, ClientOrderID: response.Items[a].ClientOID, Amount: response.Items[a].Size, ContractAmount: response.Items[a].Size, Exchange: e.Name, Date: response.Items[a].CreatedAt.Time(), LastUpdated: response.Items[a].OrderTime.Time(), Price: response.Items[a].Price, Side: side, Type: order.Stop, Pair: dPair, TimeInForce: StringToTimeInForce(response.Items[a].TimeInForce, response.Items[a].PostOnly), Status: status, AssetType: getOrdersRequest.AssetType, HiddenOrder: response.Items[a].Hidden, }) } default: if len(getOrdersRequest.Pairs) == 1 { pair = format.Format(getOrdersRequest.Pairs[0]) } sideString, err := e.OrderSideString(getOrdersRequest.Side) if err != nil { return nil, err } oType, err := OrderTypeToString(getOrdersRequest.Type) if err != nil { return nil, fmt.Errorf("asset type: %v order type: %v err: %w", getOrdersRequest.AssetType, getOrdersRequest.Type, err) } spotOrders, err := e.ListOrders(ctx, "active", pair, sideString, oType, "", getOrdersRequest.StartTime, getOrdersRequest.EndTime) if err != nil { return nil, err } for x := range spotOrders.Items { if !spotOrders.Items[x].IsActive { continue } var dPair currency.Pair var isEnabled bool dPair, isEnabled, err = e.MatchSymbolCheckEnabled(spotOrders.Items[x].Symbol, getOrdersRequest.AssetType, true) if err != nil { return nil, err } if !isEnabled { continue } if len(getOrdersRequest.Pairs) > 0 && !getOrdersRequest.Pairs.Contains(dPair, true) { continue } side, err := order.StringToOrderSide(spotOrders.Items[x].Side) if err != nil { return nil, err } oType, err := order.StringToOrderType(spotOrders.Items[x].TradeType) if err != nil { return nil, err } orders = append(orders, order.Detail{ OrderID: spotOrders.Items[x].ID, Amount: spotOrders.Items[x].Size.Float64(), RemainingAmount: spotOrders.Items[x].Size.Float64() - spotOrders.Items[x].DealSize.Float64(), ExecutedAmount: spotOrders.Items[x].DealSize.Float64(), Exchange: e.Name, Date: spotOrders.Items[x].CreatedAt.Time(), Price: spotOrders.Items[x].Price.Float64(), Side: side, Type: oType, Pair: dPair, }) } } default: return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, getOrdersRequest.AssetType) } return orders, nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (e *Exchange) GetOrderHistory(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) { if getOrdersRequest == nil { return nil, common.ErrNilPointer } if err := e.CurrencyPairs.IsAssetEnabled(getOrdersRequest.AssetType); err != nil { return nil, err } if err := getOrdersRequest.Validate(); err != nil { return nil, err } sideString, err := e.OrderSideString(getOrdersRequest.Side) if err != nil { return nil, err } var orders []order.Detail var orderSide order.Side var orderStatus order.Status var oType order.Type var pair currency.Pair switch getOrdersRequest.AssetType { case asset.Futures: var futuresOrders *FutureOrdersResponse var newOrders *FutureOrdersResponse if len(getOrdersRequest.Pairs) == 0 { futuresOrders, err = e.GetFuturesOrders(ctx, "", "", sideString, getOrdersRequest.Type.Lower(), getOrdersRequest.StartTime, getOrdersRequest.EndTime) if err != nil { return nil, err } } else { for x := range getOrdersRequest.Pairs { getOrdersRequest.Pairs[x], err = e.FormatExchangeCurrency(getOrdersRequest.Pairs[x], getOrdersRequest.AssetType) if err != nil { return nil, err } newOrders, err = e.GetFuturesOrders(ctx, "", getOrdersRequest.Pairs[x].String(), sideString, getOrdersRequest.Type.Lower(), getOrdersRequest.StartTime, getOrdersRequest.EndTime) if err != nil { return nil, fmt.Errorf("%w while fetching for symbol %s", err, getOrdersRequest.Pairs[x].String()) } if futuresOrders == nil { futuresOrders = newOrders } else { futuresOrders.Items = append(futuresOrders.Items, newOrders.Items...) } } } orders = make(order.FilteredOrders, 0, len(futuresOrders.Items)) for i := range orders { orderSide, err = order.StringToOrderSide(futuresOrders.Items[i].Side) if err != nil { return nil, err } var isEnabled bool pair, isEnabled, err = e.MatchSymbolCheckEnabled(futuresOrders.Items[i].Symbol, getOrdersRequest.AssetType, true) if err != nil { return nil, err } if !isEnabled { continue } oType, err = order.StringToOrderType(futuresOrders.Items[i].OrderType) if err != nil { log.Errorf(log.ExchangeSys, "%s %v", e.Name, err) } orders = append(orders, order.Detail{ Price: futuresOrders.Items[i].Price, Amount: futuresOrders.Items[i].Size, ExecutedAmount: futuresOrders.Items[i].DealSize, RemainingAmount: futuresOrders.Items[i].Size - futuresOrders.Items[i].DealSize, Date: futuresOrders.Items[i].CreatedAt.Time(), Exchange: e.Name, OrderID: futuresOrders.Items[i].ID, Side: orderSide, Status: orderStatus, Type: oType, Pair: pair, }) orders[i].InferCostsAndTimes() } case asset.Spot, asset.Margin: var singlePair currency.Pair if len(getOrdersRequest.Pairs) == 1 { singlePair = getOrdersRequest.Pairs[0] } switch getOrdersRequest.Type { case order.OCO: var response *OCOOrders response, err = e.GetOCOOrderList(ctx, 500, 1, singlePair.String(), getOrdersRequest.StartTime, getOrdersRequest.EndTime, []string{}) if err != nil { return nil, err } var cp currency.Pair for a := range response.Items { cp, err = currency.NewPairFromString(response.Items[a].Symbol) if err != nil { return nil, err } if len(getOrdersRequest.Pairs) > 1 && !getOrdersRequest.Pairs.Contains(cp, true) { continue } var status order.Status status, err = order.StringToOrderStatus(response.Items[a].Status) if err != nil { return nil, err } orders = append(orders, order.Detail{ OrderID: response.Items[a].OrderID, ClientOrderID: response.Items[a].ClientOrderID, Exchange: e.Name, LastUpdated: response.Items[a].OrderTime.Time(), Type: order.OCO, Pair: cp, Status: status, }) } case order.Stop, order.StopLimit, order.StopMarket, order.ConditionalStop: // NOTE: The orderType values 'limit', 'market', 'limit_stop', and 'market_stop' trigger an "The order type is invalid" error. // As a result, these options are currently unavailable. tradeType := SpotTradeType if getOrdersRequest.AssetType == asset.Margin { if getOrdersRequest.MarginType == margin.Multi { tradeType = CrossMarginTradeType } else { tradeType = IsolatedMarginTradeType } } var response *StopOrderListResponse response, err = e.ListStopOrders(ctx, singlePair.String(), sideString, "", tradeType, []string{getOrdersRequest.FromOrderID}, getOrdersRequest.StartTime, getOrdersRequest.EndTime, 0, 0) if err != nil { return nil, err } for a := range response.Items { var dPair currency.Pair var enabled bool dPair, enabled, err = e.MatchSymbolCheckEnabled(response.Items[a].Symbol, getOrdersRequest.AssetType, false) if err != nil { return nil, err } if !enabled { continue } if len(getOrdersRequest.Pairs) > 1 && !getOrdersRequest.Pairs.Contains(dPair, true) { continue } var ( side order.Side status order.Status ) side, err = order.StringToOrderSide(response.Items[a].Side) if err != nil { return nil, err } status, err = order.StringToOrderStatus(response.Items[a].Status) if err != nil { return nil, err } orders = append(orders, order.Detail{ OrderID: response.Items[a].ID, ClientOrderID: response.Items[a].ClientOID, Amount: response.Items[a].Size, ContractAmount: response.Items[a].Size, TriggerPrice: response.Items[a].StopPrice, Exchange: e.Name, Date: response.Items[a].CreatedAt.Time(), LastUpdated: response.Items[a].OrderTime.Time(), Price: response.Items[a].Price, Side: side, Type: order.Stop, Pair: dPair, TimeInForce: StringToTimeInForce(response.Items[a].TimeInForce, response.Items[a].PostOnly), Status: status, AssetType: getOrdersRequest.AssetType, HiddenOrder: response.Items[a].Hidden, }) } default: var responseOrders *OrdersListResponse var newOrders *OrdersListResponse if len(getOrdersRequest.Pairs) == 0 { responseOrders, err = e.ListOrders(ctx, "", "", sideString, getOrdersRequest.Type.Lower(), "", getOrdersRequest.StartTime, getOrdersRequest.EndTime) if err != nil { return nil, err } } else { for x := range getOrdersRequest.Pairs { newOrders, err = e.ListOrders(ctx, "", getOrdersRequest.Pairs[x].String(), sideString, getOrdersRequest.Type.Lower(), "", getOrdersRequest.StartTime, getOrdersRequest.EndTime) if err != nil { return nil, fmt.Errorf("%w while fetching for symbol %s", err, getOrdersRequest.Pairs[x].String()) } if responseOrders == nil { responseOrders = newOrders } else { responseOrders.Items = append(responseOrders.Items, newOrders.Items...) } } } orders = make([]order.Detail, len(responseOrders.Items)) for i := range orders { orderSide, err = order.StringToOrderSide(responseOrders.Items[i].Side) if err != nil { return nil, err } var orderStatus order.Status pair, err = currency.NewPairFromString(responseOrders.Items[i].Symbol) if err != nil { return nil, err } var oType order.Type oType, err = order.StringToOrderType(responseOrders.Items[i].Type) if err != nil { log.Errorf(log.ExchangeSys, "%s %v", e.Name, err) } orders[i] = order.Detail{ Price: responseOrders.Items[i].Price.Float64(), Amount: responseOrders.Items[i].Size.Float64(), ExecutedAmount: responseOrders.Items[i].DealSize.Float64(), RemainingAmount: responseOrders.Items[i].Size.Float64() - responseOrders.Items[i].DealSize.Float64(), Date: responseOrders.Items[i].CreatedAt.Time(), Exchange: e.Name, OrderID: responseOrders.Items[i].ID, Side: orderSide, Status: orderStatus, Type: oType, Pair: pair, } orders[i].InferCostsAndTimes() } } } return getOrdersRequest.Filter(e.Name, orders), nil } // GetFeeByType returns an estimate of fee based on the type of transaction func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { if feeBuilder == nil { return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) } if !e.AreCredentialsValid(ctx) && feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } if feeBuilder.Pair.IsEmpty() { return 0, currency.ErrCurrencyPairEmpty } switch feeBuilder.FeeType { case exchange.CryptocurrencyWithdrawalFee, exchange.CryptocurrencyTradeFee: fee, err := e.GetTradingFee(ctx, currency.Pairs{feeBuilder.Pair}) if err != nil { return 0, err } if feeBuilder.IsMaker { return feeBuilder.Amount * fee[0].MakerFeeRate, nil } return feeBuilder.Amount * fee[0].TakerFeeRate, nil case exchange.OfflineTradeFee: return feeBuilder.Amount * 0.001, nil case exchange.CryptocurrencyDepositFee: return 0, nil default: if !feeBuilder.FiatCurrency.IsEmpty() { fee, err := e.GetBasicFee(ctx, "1") if err != nil { return 0, err } if feeBuilder.IsMaker { return feeBuilder.Amount * fee.MakerFeeRate, nil } return feeBuilder.Amount * fee.TakerFeeRate, nil } return 0, errors.New("can't construct fee") } } // ValidateCredentials validates current credentials used for wrapper func (e *Exchange) ValidateCredentials(ctx context.Context, assetType asset.Item) error { err := e.CurrencyPairs.IsAssetEnabled(assetType) if err != nil { return err } _, err = e.UpdateAccountBalances(ctx, assetType) return e.CheckTransientError(err) } // GetHistoricCandles returns candles between a time period for a set time interval func (e *Exchange) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { req, err := e.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } var timeseries []kline.Candle switch a { case asset.Futures: var candles []FuturesKline candles, err := e.GetFuturesKline(ctx, int64(interval.Duration().Minutes()), req.RequestFormatted.String(), req.Start, req.End) if err != nil { return nil, err } for x := range candles { timeseries = append( timeseries, kline.Candle{ Time: candles[x].StartTime.Time(), Open: candles[x].Open, High: candles[x].High, Low: candles[x].Low, Close: candles[x].Close, Volume: candles[x].Volume, }) } case asset.Spot, asset.Margin: intervalString, err := IntervalToString(interval) if err != nil { return nil, err } var candles []Kline candles, err = e.GetKlines(ctx, req.RequestFormatted.String(), intervalString, req.Start, req.End) if err != nil { return nil, err } for x := range candles { timeseries = append( timeseries, kline.Candle{ Time: candles[x].StartTime.Time(), Open: candles[x].Open.Float64(), High: candles[x].High.Float64(), Low: candles[x].Low.Float64(), Close: candles[x].Close.Float64(), Volume: candles[x].Volume.Float64(), }) } default: return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) } return req.ProcessResponse(timeseries) } // GetHistoricCandlesExtended returns candles between a time period for a set time interval func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { req, err := e.GetKlineExtendedRequest(pair, a, interval, start, end) if err != nil { return nil, err } var timeSeries []kline.Candle switch a { case asset.Futures: for x := range req.RangeHolder.Ranges { var candles []FuturesKline candles, err = e.GetFuturesKline(ctx, int64(interval.Duration().Minutes()), req.RequestFormatted.String(), req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time) if err != nil { return nil, err } for y := range candles { timeSeries = append( timeSeries, kline.Candle{ Time: candles[y].StartTime.Time(), Open: candles[y].Open, High: candles[y].High, Low: candles[y].Low, Close: candles[y].Close, Volume: candles[y].Volume, }) } } return req.ProcessResponse(timeSeries) case asset.Spot, asset.Margin: var intervalString string intervalString, err = IntervalToString(interval) if err != nil { return nil, err } var candles []Kline for x := range req.RangeHolder.Ranges { candles, err = e.GetKlines(ctx, req.RequestFormatted.String(), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time) if err != nil { return nil, err } for x := range candles { timeSeries = append( timeSeries, kline.Candle{ Time: candles[x].StartTime.Time(), Open: candles[x].Open.Float64(), High: candles[x].High.Float64(), Low: candles[x].Low.Float64(), Close: candles[x].Close.Float64(), Volume: candles[x].Volume.Float64(), }) } } return req.ProcessResponse(timeSeries) default: return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) } } // GetServerTime returns the current exchange server time. func (e *Exchange) GetServerTime(ctx context.Context, a asset.Item) (time.Time, error) { switch a { case asset.Spot, asset.Margin: return e.GetCurrentServerTime(ctx) case asset.Futures: return e.GetFuturesServerTime(ctx) default: return time.Time{}, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } // GetAvailableTransferChains returns the available transfer blockchains for the specific cryptocurrency func (e *Exchange) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) { if cryptocurrency.IsEmpty() { return nil, currency.ErrCurrencyCodeEmpty } currencyDetail, err := e.GetCurrencyDetailV3(ctx, cryptocurrency, "") if err != nil { return nil, err } chains := make([]string, len(currencyDetail.Chains)) for x := range currencyDetail.Chains { chains[x] = currencyDetail.Chains[x].ChainName } return chains, nil } // ValidateAPICredentials validates current credentials used for wrapper functionality func (e *Exchange) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error { _, err := e.UpdateAccountBalances(ctx, assetType) return e.CheckTransientError(err) } // GetFuturesContractDetails returns details about futures contracts func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) { if !item.IsFutures() { return nil, futures.ErrNotFuturesAsset } if !e.SupportsAsset(item) || item != asset.Futures { return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item) } contracts, err := e.GetFuturesOpenContracts(ctx) if err != nil { return nil, err } resp := make([]futures.Contract, len(contracts)) for i := range contracts { var cp, underlying currency.Pair underlying, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].QuoteCurrency) if err != nil { return nil, err } cp, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].Symbol[len(contracts[i].BaseCurrency):]) if err != nil { return nil, err } settleCurr := currency.NewCode(contracts[i].SettleCurrency) var ct futures.ContractType if contracts[i].ContractType == "FFWCSX" { ct = futures.Perpetual } else { ct = futures.Quarterly } contractSettlementType := futures.Linear if contracts[i].IsInverse { contractSettlementType = futures.Inverse } var fri time.Duration if len(e.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { // can infer funding rate interval from the only funding rate frequency defined for k := range e.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { fri = k.Duration() } } timeOfCurrentFundingRate := time.Now().Add((time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond) - fri).Truncate(time.Hour).UTC() resp[i] = futures.Contract{ Exchange: e.Name, Name: cp, Underlying: underlying, SettlementCurrency: settleCurr, MarginCurrency: settleCurr, Asset: item, StartDate: contracts[i].FirstOpenDate.Time(), EndDate: contracts[i].ExpireDate.Time(), IsActive: !strings.EqualFold(contracts[i].Status, "closed"), Status: contracts[i].Status, Multiplier: contracts[i].Multiplier, MaxLeverage: contracts[i].MaxLeverage, SettlementType: contractSettlementType, LatestRate: fundingrate.Rate{ Rate: decimal.NewFromFloat(contracts[i].FundingFeeRate), Time: timeOfCurrentFundingRate, // kucoin pays every 8 hours }, Type: ct, } } return resp, nil } // GetLatestFundingRates returns the latest funding rates data func (e *Exchange) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { if r == nil { return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) } var fri time.Duration if len(e.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { // can infer funding rate interval from the only funding rate frequency defined for k := range e.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { fri = k.Duration() } } if r.Pair.IsEmpty() { contracts, err := e.GetFuturesOpenContracts(ctx) if err != nil { return nil, err } if r.IncludePredictedRate { log.Warnf(log.ExchangeSys, "%s predicted rate for all currencies requires an additional %v requests", e.Name, len(contracts)) } timeChecked := time.Now() resp := make([]fundingrate.LatestRateResponse, 0, len(contracts)) for i := range contracts { timeOfNextFundingRate := time.Now().Add(time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond).Truncate(time.Hour).UTC() var cp currency.Pair cp, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].Symbol[len(contracts[i].BaseCurrency):]) if err != nil { return nil, err } var isPerp bool isPerp, err = e.IsPerpetualFutureCurrency(r.Asset, cp) if err != nil { return nil, err } if !isPerp { continue } rate := fundingrate.LatestRateResponse{ Exchange: e.Name, Asset: r.Asset, Pair: cp, LatestRate: fundingrate.Rate{ Time: timeOfNextFundingRate.Add(-fri), Rate: decimal.NewFromFloat(contracts[i].FundingFeeRate), }, TimeOfNextRate: timeOfNextFundingRate, TimeChecked: timeChecked, } if r.IncludePredictedRate { var fr *FuturesFundingRate fr, err = e.GetFuturesCurrentFundingRate(ctx, contracts[i].Symbol) if err != nil { return nil, err } rate.PredictedUpcomingRate = fundingrate.Rate{ Time: timeOfNextFundingRate, Rate: decimal.NewFromFloat(fr.PredictedValue), } } resp = append(resp, rate) } return resp, nil } resp := make([]fundingrate.LatestRateResponse, 1) is, err := e.IsPerpetualFutureCurrency(r.Asset, r.Pair) if err != nil { return nil, err } if !is { return nil, fmt.Errorf("%w %s %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) } fPair, err := e.FormatExchangeCurrency(r.Pair, r.Asset) if err != nil { return nil, err } var fr *FuturesFundingRate fr, err = e.GetFuturesCurrentFundingRate(ctx, fPair.String()) if err != nil { return nil, err } rate := fundingrate.LatestRateResponse{ Exchange: e.Name, Asset: r.Asset, Pair: r.Pair, LatestRate: fundingrate.Rate{ Time: fr.TimePoint.Time(), Rate: decimal.NewFromFloat(fr.Value), }, TimeOfNextRate: fr.TimePoint.Time().Add(fri).Truncate(time.Hour).UTC(), TimeChecked: time.Now(), } if r.IncludePredictedRate { rate.PredictedUpcomingRate = fundingrate.Rate{ Time: rate.TimeOfNextRate, Rate: decimal.NewFromFloat(fr.PredictedValue), } } resp[0] = rate return resp, nil } // IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future func (e *Exchange) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (bool, error) { return a == asset.Futures && (cp.Quote.Equal(currency.USDTM) || cp.Quote.Equal(currency.USDM)), nil } // GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period func (e *Exchange) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { if r == nil { return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) } if r.Asset != asset.Futures { return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) } if r.Pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } if !r.StartDate.IsZero() && !r.EndDate.IsZero() { err := common.StartEndTimeCheck(r.StartDate, r.EndDate) if err != nil { return nil, err } } var err error r.Pair, err = e.FormatExchangeCurrency(r.Pair, r.Asset) if err != nil { return nil, err } records, err := e.GetPublicFundingRate(ctx, r.Pair.String(), r.StartDate, r.EndDate) if err != nil { return nil, err } if len(records) == 0 { return nil, fundingrate.ErrNoFundingRatesFound } fundingRates := make([]fundingrate.Rate, 0, len(records)) for i := range records { if (!r.EndDate.IsZero() && r.EndDate.Before(records[i].Timepoint.Time())) || (!r.StartDate.IsZero() && r.StartDate.After(records[i].Timepoint.Time())) { continue } fundingRates = append(fundingRates, fundingrate.Rate{ Rate: decimal.NewFromFloat(records[i].FundingRate), Time: records[i].Timepoint.Time(), }) } if len(fundingRates) == 0 { return nil, fundingrate.ErrNoFundingRatesFound } return &fundingrate.HistoricalRates{ Exchange: e.Name, Asset: r.Asset, Pair: r.Pair, FundingRates: fundingRates, StartDate: fundingRates[len(fundingRates)-1].Time, EndDate: fundingRates[0].Time, LatestRate: fundingRates[0], PaymentCurrency: r.PaymentCurrency, }, nil } // GetLeverage gets the account's initial leverage for the asset type and pair func (e *Exchange) GetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ order.Side) (float64, error) { return -1, fmt.Errorf("%w leverage is set during order placement, view orders to view leverage", common.ErrFunctionNotSupported) } // SetLeverage sets the account's initial leverage for the asset type and pair func (e *Exchange) SetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ float64, _ order.Side) error { return fmt.Errorf("%w leverage is set during order placement", common.ErrFunctionNotSupported) } // SetMarginType sets the default margin type for when opening a new position func (e *Exchange) SetMarginType(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type) error { return fmt.Errorf("%w must be set via website", common.ErrFunctionNotSupported) } // SetCollateralMode sets the collateral type for your account func (e *Exchange) SetCollateralMode(_ context.Context, _ asset.Item, _ collateral.Mode) error { return fmt.Errorf("%w must be set via website", common.ErrFunctionNotSupported) } // GetCollateralMode returns the collateral type for your account func (e *Exchange) GetCollateralMode(_ context.Context, _ asset.Item) (collateral.Mode, error) { return collateral.UnknownMode, fmt.Errorf("%w only via website", common.ErrFunctionNotSupported) } // ChangePositionMargin will modify a position/currencies margin parameters func (e *Exchange) ChangePositionMargin(ctx context.Context, r *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error) { if r == nil { return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) } if r.Asset != asset.Futures { return nil, fmt.Errorf("%w %v", futures.ErrNotFuturesAsset, r.Asset) } if r.Pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } if r.MarginType != margin.Isolated { return nil, fmt.Errorf("%w %v", margin.ErrMarginTypeUnsupported, r.MarginType) } fPair, err := e.FormatExchangeCurrency(r.Pair, r.Asset) if err != nil { return nil, err } resp, err := e.AddMargin(ctx, fPair.String(), fmt.Sprintf("%s%v%v", r.Pair, r.NewAllocatedMargin, time.Now().Unix()), r.NewAllocatedMargin) if err != nil { return nil, err } if resp == nil { return nil, fmt.Errorf("%s - %s", e.Name, "no response received") } return &margin.PositionChangeResponse{ Exchange: e.Name, Pair: r.Pair, Asset: r.Asset, AllocatedMargin: resp.PosMargin, MarginType: r.MarginType, }, nil } // GetFuturesPositionSummary returns position summary details for an active position func (e *Exchange) GetFuturesPositionSummary(ctx context.Context, r *futures.PositionSummaryRequest) (*futures.PositionSummary, error) { if r == nil { return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) } if r.Asset != asset.Futures { return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset) } if r.Pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } fPair, err := e.FormatExchangeCurrency(r.Pair, r.Asset) if err != nil { return nil, err } pos, err := e.GetFuturesPosition(ctx, fPair.String()) if err != nil { return nil, err } marginType := margin.Isolated if pos.CrossMode { marginType = margin.Multi } contracts, err := e.GetFuturesContractDetails(ctx, r.Asset) if err != nil { return nil, err } var multiplier, contractSize float64 var settlementType futures.ContractSettlementType for i := range contracts { if !contracts[i].Name.Equal(fPair) { continue } multiplier = contracts[i].Multiplier contractSize = multiplier * pos.CurrentQty settlementType = contracts[i].SettlementType } ao, err := e.GetFuturesAccountOverview(ctx, fPair.Base.String()) if err != nil { return nil, err } return &futures.PositionSummary{ Pair: r.Pair, Asset: r.Asset, MarginType: marginType, CollateralMode: collateral.MultiMode, Currency: currency.NewCode(pos.SettleCurrency), StartDate: pos.OpeningTimestamp.Time(), AvailableEquity: decimal.NewFromFloat(ao.AccountEquity), MarginBalance: decimal.NewFromFloat(ao.MarginBalance), NotionalSize: decimal.NewFromFloat(pos.MarkValue), Leverage: decimal.NewFromFloat(pos.RealLeverage), MaintenanceMarginRequirement: decimal.NewFromFloat(pos.MaintMarginReq), InitialMarginRequirement: decimal.NewFromFloat(pos.PosInit), EstimatedLiquidationPrice: decimal.NewFromFloat(pos.LiquidationPrice), CollateralUsed: decimal.NewFromFloat(pos.PosCost), MarkPrice: decimal.NewFromFloat(pos.MarkPrice), CurrentSize: decimal.NewFromFloat(pos.CurrentQty), ContractSize: decimal.NewFromFloat(contractSize), ContractMultiplier: decimal.NewFromFloat(multiplier), ContractSettlementType: settlementType, AverageOpenPrice: decimal.NewFromFloat(pos.AvgEntryPrice), UnrealisedPNL: decimal.NewFromFloat(pos.UnrealisedPnl), RealisedPNL: decimal.NewFromFloat(pos.RealisedPnl), MaintenanceMarginFraction: decimal.NewFromFloat(pos.MaintMarginReq), FreeCollateral: decimal.NewFromFloat(ao.AvailableBalance), TotalCollateral: decimal.NewFromFloat(ao.AccountEquity), FrozenBalance: decimal.NewFromFloat(ao.FrozenFunds), }, nil } // GetFuturesPositionOrders returns the orders for futures positions func (e *Exchange) GetFuturesPositionOrders(ctx context.Context, r *futures.PositionsRequest) ([]futures.PositionResponse, error) { if r == nil { return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) } if r.Asset != asset.Futures { return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset) } if len(r.Pairs) == 0 { return nil, currency.ErrCurrencyPairEmpty } err := common.StartEndTimeCheck(r.StartDate, r.EndDate) if err != nil { return nil, err } if !r.EndDate.IsZero() && r.EndDate.Sub(r.StartDate) > e.Features.Supports.MaximumOrderHistory { if r.RespectOrderHistoryLimits { r.StartDate = time.Now().Add(-e.Features.Supports.MaximumOrderHistory) } else { return nil, fmt.Errorf("%w max lookup %v", futures.ErrOrderHistoryTooLarge, time.Now().Add(-e.Features.Supports.MaximumOrderHistory)) } } contracts, err := e.GetFuturesContractDetails(ctx, r.Asset) if err != nil { return nil, err } resp := make([]futures.PositionResponse, len(r.Pairs)) for x := range r.Pairs { var multiplier float64 fPair, err := e.FormatExchangeCurrency(r.Pairs[x], r.Asset) if err != nil { return nil, err } for i := range contracts { if !contracts[i].Name.Equal(fPair) { continue } multiplier = contracts[i].Multiplier } positionOrders, err := e.GetFuturesOrders(ctx, "", fPair.String(), "", "", r.StartDate, r.EndDate) if err != nil { return nil, err } resp[x].Orders = make([]order.Detail, len(positionOrders.Items)) for y := range positionOrders.Items { side, err := order.StringToOrderSide(positionOrders.Items[y].Side) if err != nil { return nil, err } oType, err := order.StringToOrderType(positionOrders.Items[y].OrderType) if err != nil { return nil, fmt.Errorf("asset type: %v err: %w", r.Asset, err) } oStatus, err := order.StringToOrderStatus(positionOrders.Items[y].Status) if err != nil { return nil, fmt.Errorf("asset type: %v err: %w", r.Asset, err) } resp[x].Orders[y] = order.Detail{ Leverage: positionOrders.Items[y].Leverage, Price: positionOrders.Items[y].Price, Amount: positionOrders.Items[y].Size * multiplier, ContractAmount: positionOrders.Items[y].Size, ExecutedAmount: positionOrders.Items[y].FilledSize, RemainingAmount: positionOrders.Items[y].Size - positionOrders.Items[y].FilledSize, CostAsset: currency.NewCode(positionOrders.Items[y].SettleCurrency), Exchange: e.Name, OrderID: positionOrders.Items[y].ID, ClientOrderID: positionOrders.Items[y].ClientOid, Type: oType, Side: side, Status: oStatus, AssetType: asset.Futures, Date: positionOrders.Items[y].CreatedAt.Time(), CloseTime: positionOrders.Items[y].EndAt.Time(), LastUpdated: positionOrders.Items[y].UpdatedAt.Time(), Pair: fPair, } } } return resp, nil } // UpdateOrderExecutionLimits updates order execution limits func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { if !e.SupportsAsset(a) { return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } var l []limits.MinMaxLevel switch a { case asset.Spot, asset.Margin: symbols, err := e.GetSymbols(ctx, "") if err != nil { return err } l = make([]limits.MinMaxLevel, 0, len(symbols)) for x := range symbols { if a == asset.Margin && !symbols[x].IsMarginEnabled { continue } pair, enabled, err := e.MatchSymbolCheckEnabled(symbols[x].Symbol, a, true) if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return err } if !enabled { continue } l = append(l, limits.MinMaxLevel{ Key: key.NewExchangeAssetPair(e.Name, a, pair), AmountStepIncrementSize: symbols[x].BaseIncrement, QuoteStepIncrementSize: symbols[x].QuoteIncrement, PriceStepIncrementSize: symbols[x].PriceIncrement, MinimumBaseAmount: symbols[x].BaseMinSize, MaximumBaseAmount: symbols[x].BaseMaxSize, MinimumQuoteAmount: symbols[x].QuoteMinSize, MaximumQuoteAmount: symbols[x].QuoteMaxSize, }) } case asset.Futures: contract, err := e.GetFuturesOpenContracts(ctx) if err != nil { return err } l = make([]limits.MinMaxLevel, 0, len(contract)) for x := range contract { pair, enabled, err := e.MatchSymbolCheckEnabled(contract[x].Symbol, a, false) if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return err } if !enabled { continue } l = append(l, limits.MinMaxLevel{ Key: key.NewExchangeAssetPair(e.Name, a, pair), AmountStepIncrementSize: contract[x].LotSize, QuoteStepIncrementSize: contract[x].TickSize, MaximumBaseAmount: contract[x].MaxOrderQty, MaximumQuoteAmount: contract[x].MaxPrice, }) } } return limits.Load(l) } // GetOpenInterest returns the open interest rate for a given asset pair func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]futures.OpenInterest, error) { for i := range k { if k[i].Asset != asset.Futures { // avoid API calls or returning errors after a successful retrieval return nil, fmt.Errorf("%w %v %v", asset.ErrNotSupported, k[i].Asset, k[i].Pair()) } } contracts, err := e.GetFuturesOpenContracts(ctx) if err != nil { return nil, err } resp := make([]futures.OpenInterest, 0, len(contracts)) for i := range contracts { var symbol currency.Pair var enabled bool symbol, enabled, err = e.MatchSymbolCheckEnabled(contracts[i].Symbol, asset.Futures, true) if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return nil, err } if !enabled { continue } var appendData bool for j := range k { if k[j].Pair().Equal(symbol) { appendData = true break } } if len(k) > 0 && !appendData { continue } resp = append(resp, futures.OpenInterest{ Key: key.NewExchangeAssetPair(e.Name, asset.Futures, symbol), OpenInterest: contracts[i].OpenInterest.Float64(), }) } return resp, nil } // GetCurrencyTradeURL returns the URL to the exchange's trade page for the given asset and currency pair func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp currency.Pair) (string, error) { _, err := e.CurrencyPairs.IsPairEnabled(cp, a) if err != nil { return "", err } cp.Delimiter = currency.DashDelimiter switch a { case asset.Spot: return tradeBaseURL + tradeSpot + cp.Upper().String(), nil case asset.Margin: return tradeBaseURL + tradeSpot + tradeMargin + cp.Upper().String(), nil case asset.Futures: cp.Delimiter = "" return tradeBaseURL + tradeFutures + tradeSpot + cp.Upper().String(), nil default: return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } // StringToTimeInForce returns an order.TimeInForce instance from string func StringToTimeInForce(tif string, postOnly bool) order.TimeInForce { tif = strings.ToUpper(tif) var out order.TimeInForce switch tif { case "GTT": out = order.GoodTillTime case "IOC": out = order.ImmediateOrCancel case "FOK": out = order.FillOrKill default: out = order.GoodTillCancel } if postOnly { out |= order.PostOnly } return out }