package engine import ( "context" "errors" "fmt" "net" "net/http" "os" "path/filepath" "sort" "strconv" "strings" "time" "github.com/gofrs/uuid" grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/pquerna/otp/totp" "github.com/shopspring/decimal" "github.com/thrasher-corp/gct-ta/indicators" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/common/file" "github.com/thrasher-corp/gocryptotrader/common/file/archive" "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/common/timeperiods" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/database/models/postgres" "github.com/thrasher-corp/gocryptotrader/database/models/sqlite3" "github.com/thrasher-corp/gocryptotrader/database/repository/audit" exchangeDB "github.com/thrasher-corp/gocryptotrader/database/repository/exchange" "github.com/thrasher-corp/gocryptotrader/encoding/json" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" "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/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" "github.com/thrasher-corp/gocryptotrader/gctrpc" "github.com/thrasher-corp/gocryptotrader/gctrpc/auth" gctscript "github.com/thrasher-corp/gocryptotrader/gctscript/vm" "github.com/thrasher-corp/gocryptotrader/log" "github.com/thrasher-corp/gocryptotrader/portfolio" "github.com/thrasher-corp/gocryptotrader/portfolio/banking" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" "github.com/thrasher-corp/gocryptotrader/utils" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/timestamppb" ) var ( errExchangeNotLoaded = errors.New("exchange is not loaded/doesn't exist") errExchangeNotEnabled = errors.New("exchange is not enabled") errExchangeBaseNotFound = errors.New("cannot get exchange base") errInvalidArguments = errors.New("invalid arguments received") errExchangeNameUnset = errors.New("exchange name unset") errCurrencyPairUnset = errors.New("currency pair unset") errInvalidTimes = errors.New("invalid start and end times") errAssetTypeUnset = errors.New("asset type unset") errDispatchSystem = errors.New("dispatch system offline") errCurrencyNotEnabled = errors.New("currency not enabled") errCurrencyNotSpecified = errors.New("a currency must be specified") errCurrencyPairInvalid = errors.New("currency provided is not found in the available pairs list") errNoTrades = errors.New("no trades returned from supplied params") errNilRequestData = errors.New("nil request data received, cannot continue") errNoAccountInformation = errors.New("account information does not exist") errShutdownNotAllowed = errors.New("shutting down this bot instance is not allowed via gRPC, please enable by command line flag --grpcshutdown or config.json field grpcAllowBotShutdown") errGRPCShutdownSignalIsNil = errors.New("cannot shutdown, gRPC shutdown channel is nil") errInvalidStrategy = errors.New("invalid strategy") errSpecificPairNotEnabled = errors.New("specified pair is not enabled") ) // RPCServer struct type RPCServer struct { gctrpc.UnimplementedGoCryptoTraderServiceServer *Engine } func (s *RPCServer) authenticateClient(ctx context.Context) (context.Context, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return ctx, errors.New("unable to extract metadata") } authStr, ok := md["authorization"] if !ok { return ctx, errors.New("authorization header missing") } if !strings.Contains(authStr[0], "Basic") { return ctx, errors.New("basic not found in authorization header") } decoded, err := crypto.Base64Decode(strings.Split(authStr[0], " ")[1]) if err != nil { return ctx, errors.New("unable to base64 decode authorization header") } cred := strings.Split(string(decoded), ":") username := cred[0] password := cred[1] if username != s.Config.RemoteControl.Username || password != s.Config.RemoteControl.Password { return ctx, errors.New("username/password mismatch") } ctx, err = account.ParseCredentialsMetadata(ctx, md) if err != nil { return ctx, err } if _, ok := md["verbose"]; ok { ctx = request.WithVerbose(ctx) } return ctx, nil } // StartRPCServer starts a gRPC server with TLS auth func StartRPCServer(engine *Engine) { targetDir := utils.GetTLSDir(engine.Settings.DataDir) if err := CheckCerts(targetDir); err != nil { log.Errorf(log.GRPCSys, "gRPC CheckCerts failed. err: %s\n", err) return } log.Debugf(log.GRPCSys, "gRPC server support enabled. Starting gRPC server on https://%v.\n", engine.Config.RemoteControl.GRPC.ListenAddress) lis, err := net.Listen("tcp", engine.Config.RemoteControl.GRPC.ListenAddress) if err != nil { log.Errorf(log.GRPCSys, "gRPC server failed to bind to port: %s", err) return } creds, err := credentials.NewServerTLSFromFile(filepath.Join(targetDir, "cert.pem"), filepath.Join(targetDir, "key.pem")) if err != nil { log.Errorf(log.GRPCSys, "gRPC server could not load TLS keys: %s\n", err) return } s := RPCServer{Engine: engine} opts := []grpc.ServerOption{ grpc.Creds(creds), grpc.UnaryInterceptor(grpcauth.UnaryServerInterceptor(s.authenticateClient)), grpc.StreamInterceptor(grpcauth.StreamServerInterceptor(s.authenticateClient)), } server := grpc.NewServer(opts...) gctrpc.RegisterGoCryptoTraderServiceServer(server, &s) go func() { if err := server.Serve(lis); err != nil { log.Errorf(log.GRPCSys, "gRPC server failed to serve: %s\n", err) return } }() log.Debugln(log.GRPCSys, "gRPC server started!") if s.Settings.EnableGRPCProxy { s.StartRPCRESTProxy() } } // StartRPCRESTProxy starts a gRPC proxy func (s *RPCServer) StartRPCRESTProxy() { log.Debugf(log.GRPCSys, "gRPC proxy server support enabled. Starting gRPC proxy server on https://%v.\n", s.Config.RemoteControl.GRPC.GRPCProxyListenAddress) targetDir := utils.GetTLSDir(s.Settings.DataDir) certFile := filepath.Join(targetDir, "cert.pem") keyFile := filepath.Join(targetDir, "key.pem") creds, err := credentials.NewClientTLSFromFile(certFile, "") if err != nil { log.Errorf(log.GRPCSys, "Unable to start gRPC proxy. Err: %s\n", err) return } mux := runtime.NewServeMux() opts := []grpc.DialOption{ grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(auth.BasicAuth{ Username: s.Config.RemoteControl.Username, Password: s.Config.RemoteControl.Password, }), } err = gctrpc.RegisterGoCryptoTraderServiceHandlerFromEndpoint(context.Background(), mux, s.Config.RemoteControl.GRPC.ListenAddress, opts) if err != nil { log.Errorf(log.GRPCSys, "Failed to register gRPC proxy. Err: %s\n", err) return } go func() { server := &http.Server{ Addr: s.Config.RemoteControl.GRPC.GRPCProxyListenAddress, ReadHeaderTimeout: time.Minute, ReadTimeout: time.Minute, Handler: s.authClient(mux), } if err = server.ListenAndServeTLS(certFile, keyFile); err != nil { log.Errorf(log.GRPCSys, "gRPC proxy server failed to serve: %s\n", err) return } }() log.Debugln(log.GRPCSys, "gRPC proxy server started!") } func (s *RPCServer) authClient(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if !ok || username != s.Config.RemoteControl.Username || password != s.Config.RemoteControl.Password { w.Header().Set("WWW-Authenticate", `Basic realm="restricted"`) http.Error(w, "Access denied", http.StatusUnauthorized) log.Warnf(log.GRPCSys, "gRPC proxy server unauthorised access attempt. IP: %s Path: %s\n", r.RemoteAddr, r.URL.Path) return } handler.ServeHTTP(w, r) }) } // GetInfo returns info about the current GoCryptoTrader session func (s *RPCServer) GetInfo(_ context.Context, _ *gctrpc.GetInfoRequest) (*gctrpc.GetInfoResponse, error) { rpcEndpoints, err := s.getRPCEndpoints() if err != nil { return nil, err } return &gctrpc.GetInfoResponse{ Uptime: time.Since(s.uptime).String(), EnabledExchanges: int64(s.Config.CountEnabledExchanges()), AvailableExchanges: int64(len(s.Config.Exchanges)), DefaultFiatCurrency: s.Config.Currency.FiatDisplayCurrency.String(), DefaultForexProvider: s.Config.GetPrimaryForexProvider(), SubsystemStatus: s.GetSubsystemsStatus(), RpcEndpoints: rpcEndpoints, }, nil } func (s *RPCServer) getRPCEndpoints() (map[string]*gctrpc.RPCEndpoint, error) { endpoints, err := s.Engine.GetRPCEndpoints() if err != nil { return nil, err } rpcEndpoints := make(map[string]*gctrpc.RPCEndpoint) for key, val := range endpoints { rpcEndpoints[key] = &gctrpc.RPCEndpoint{ Started: val.Started, ListenAddress: val.ListenAddr, } } return rpcEndpoints, nil } // GetSubsystems returns a list of subsystems and their status func (s *RPCServer) GetSubsystems(_ context.Context, _ *gctrpc.GetSubsystemsRequest) (*gctrpc.GetSusbsytemsResponse, error) { return &gctrpc.GetSusbsytemsResponse{SubsystemsStatus: s.GetSubsystemsStatus()}, nil } // EnableSubsystem enables a engine subsystem func (s *RPCServer) EnableSubsystem(_ context.Context, r *gctrpc.GenericSubsystemRequest) (*gctrpc.GenericResponse, error) { err := s.SetSubsystem(r.Subsystem, true) if err != nil { return nil, err } return &gctrpc.GenericResponse{ Status: MsgStatusSuccess, Data: fmt.Sprintf("subsystem %s enabled", r.Subsystem), }, nil } // DisableSubsystem disables a engine subsystem func (s *RPCServer) DisableSubsystem(_ context.Context, r *gctrpc.GenericSubsystemRequest) (*gctrpc.GenericResponse, error) { err := s.SetSubsystem(r.Subsystem, false) if err != nil { return nil, err } return &gctrpc.GenericResponse{ Status: MsgStatusSuccess, Data: fmt.Sprintf("subsystem %s disabled", r.Subsystem), }, nil } // GetRPCEndpoints returns a list of API endpoints func (s *RPCServer) GetRPCEndpoints(_ context.Context, _ *gctrpc.GetRPCEndpointsRequest) (*gctrpc.GetRPCEndpointsResponse, error) { endpoint, err := s.getRPCEndpoints() return &gctrpc.GetRPCEndpointsResponse{Endpoints: endpoint}, err } // GetCommunicationRelayers returns the status of the engines communication relayers func (s *RPCServer) GetCommunicationRelayers(_ context.Context, _ *gctrpc.GetCommunicationRelayersRequest) (*gctrpc.GetCommunicationRelayersResponse, error) { relayers, err := s.CommunicationsManager.GetStatus() if err != nil { return nil, err } var resp gctrpc.GetCommunicationRelayersResponse resp.CommunicationRelayers = make(map[string]*gctrpc.CommunicationRelayer) for k, v := range relayers { resp.CommunicationRelayers[k] = &gctrpc.CommunicationRelayer{ Enabled: v.Enabled, Connected: v.Connected, } } return &resp, nil } // GetExchanges returns a list of exchanges // Param is whether or not you wish to list enabled exchanges func (s *RPCServer) GetExchanges(_ context.Context, r *gctrpc.GetExchangesRequest) (*gctrpc.GetExchangesResponse, error) { exchanges := strings.Join(s.GetExchangeNames(r.Enabled), ",") return &gctrpc.GetExchangesResponse{Exchanges: exchanges}, nil } // DisableExchange disables an exchange func (s *RPCServer) DisableExchange(_ context.Context, r *gctrpc.GenericExchangeNameRequest) (*gctrpc.GenericResponse, error) { err := s.UnloadExchange(r.Exchange) if err != nil { return nil, err } return &gctrpc.GenericResponse{Status: MsgStatusSuccess}, nil } // EnableExchange enables an exchange func (s *RPCServer) EnableExchange(_ context.Context, r *gctrpc.GenericExchangeNameRequest) (*gctrpc.GenericResponse, error) { err := s.LoadExchange(r.Exchange) if err != nil { return nil, err } return &gctrpc.GenericResponse{Status: MsgStatusSuccess}, nil } // GetExchangeOTPCode retrieves an exchanges OTP code func (s *RPCServer) GetExchangeOTPCode(_ context.Context, r *gctrpc.GenericExchangeNameRequest) (*gctrpc.GetExchangeOTPResponse, error) { if _, err := s.GetExchangeByName(r.Exchange); err != nil { return nil, err } result, err := s.GetExchangeOTPByName(r.Exchange) return &gctrpc.GetExchangeOTPResponse{OtpCode: result}, err } // GetExchangeOTPCodes retrieves OTP codes for all exchanges which have an // OTP secret installed func (s *RPCServer) GetExchangeOTPCodes(_ context.Context, _ *gctrpc.GetExchangeOTPsRequest) (*gctrpc.GetExchangeOTPsResponse, error) { result, err := s.GetExchangeOTPs() return &gctrpc.GetExchangeOTPsResponse{OtpCodes: result}, err } // GetExchangeInfo gets info for a specific exchange func (s *RPCServer) GetExchangeInfo(_ context.Context, r *gctrpc.GenericExchangeNameRequest) (*gctrpc.GetExchangeInfoResponse, error) { exchCfg, err := s.Config.GetExchangeConfig(r.Exchange) if err != nil { return nil, err } resp := &gctrpc.GetExchangeInfoResponse{ Name: exchCfg.Name, Enabled: exchCfg.Enabled, Verbose: exchCfg.Verbose, UsingSandbox: exchCfg.UseSandbox, HttpTimeout: exchCfg.HTTPTimeout.String(), HttpUseragent: exchCfg.HTTPUserAgent, HttpProxy: exchCfg.ProxyAddress, BaseCurrencies: strings.Join(exchCfg.BaseCurrencies.Strings(), ","), } resp.SupportedAssets = make(map[string]*gctrpc.PairsSupported) assets := exchCfg.CurrencyPairs.GetAssetTypes(false) for i := range assets { var enabled currency.Pairs enabled, err = exchCfg.CurrencyPairs.GetPairs(assets[i], true) if err != nil { return nil, err } var available currency.Pairs available, err = exchCfg.CurrencyPairs.GetPairs(assets[i], false) if err != nil { return nil, err } resp.SupportedAssets[assets[i].String()] = &gctrpc.PairsSupported{ EnabledPairs: enabled.Join(), AvailablePairs: available.Join(), } } return resp, nil } // GetTicker returns the ticker for a specified exchange, currency pair and // asset type func (s *RPCServer) GetTicker(_ context.Context, r *gctrpc.GetTickerRequest) (*gctrpc.TickerResponse, error) { a, err := asset.New(r.AssetType) if err != nil { return nil, err } e, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } pair := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, e, a, pair) if err != nil { return nil, err } t, err := e.GetCachedTicker(pair, a) if err != nil { return nil, err } resp := &gctrpc.TickerResponse{ Pair: r.Pair, LastUpdated: s.unixTimestamp(t.LastUpdated), Last: t.Last, High: t.High, Low: t.Low, Bid: t.Bid, Ask: t.Ask, Volume: t.Volume, PriceAth: t.PriceATH, } return resp, nil } // GetTickers returns a list of tickers for all enabled exchanges and all // enabled currency pairs func (s *RPCServer) GetTickers(_ context.Context, _ *gctrpc.GetTickersRequest) (*gctrpc.GetTickersResponse, error) { activeTickers := s.GetAllActiveTickers() tickers := make([]*gctrpc.Tickers, len(activeTickers)) for x := range activeTickers { ticks := make([]*gctrpc.TickerResponse, len(activeTickers[x].ExchangeValues)) for y, val := range activeTickers[x].ExchangeValues { ticks[y] = &gctrpc.TickerResponse{ Pair: &gctrpc.CurrencyPair{ Delimiter: val.Pair.Delimiter, Base: val.Pair.Base.String(), Quote: val.Pair.Quote.String(), }, LastUpdated: s.unixTimestamp(val.LastUpdated), Last: val.Last, High: val.High, Low: val.Low, Bid: val.Bid, Ask: val.Ask, Volume: val.Volume, PriceAth: val.PriceATH, } } tickers[x] = &gctrpc.Tickers{Exchange: activeTickers[x].ExchangeName, Tickers: ticks} } return &gctrpc.GetTickersResponse{Tickers: tickers}, nil } // GetOrderbook returns an orderbook for a specific exchange, currency pair // and asset type func (s *RPCServer) GetOrderbook(_ context.Context, r *gctrpc.GetOrderbookRequest) (*gctrpc.OrderbookResponse, error) { a, err := asset.New(r.AssetType) if err != nil { return nil, err } e, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } pair := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) ob, err := e.GetCachedOrderbook(pair, a) if err != nil { return nil, err } bids := make([]*gctrpc.OrderbookItem, len(ob.Bids)) for x := range ob.Bids { bids[x] = &gctrpc.OrderbookItem{Amount: ob.Bids[x].Amount, Price: ob.Bids[x].Price} } asks := make([]*gctrpc.OrderbookItem, len(ob.Asks)) for x := range ob.Asks { asks[x] = &gctrpc.OrderbookItem{Amount: ob.Asks[x].Amount, Price: ob.Asks[x].Price} } resp := &gctrpc.OrderbookResponse{ Pair: r.Pair, Bids: bids, Asks: asks, LastUpdated: s.unixTimestamp(ob.LastUpdated), AssetType: r.AssetType, } return resp, nil } // GetOrderbooks returns a list of orderbooks for all enabled exchanges and all // enabled currency pairs func (s *RPCServer) GetOrderbooks(_ context.Context, _ *gctrpc.GetOrderbooksRequest) (*gctrpc.GetOrderbooksResponse, error) { exchanges, err := s.ExchangeManager.GetExchanges() if err != nil { return nil, err } obResponse := make([]*gctrpc.Orderbooks, 0, len(exchanges)) var obs []*gctrpc.OrderbookResponse for _, e := range exchanges { if !e.IsEnabled() { continue } for _, a := range e.GetAssetTypes(true) { pairs, err := e.GetEnabledPairs(a) if err != nil { log.Errorf(log.RESTSys, "Exchange %s could not retrieve enabled currencies. Err: %s\n", e.GetName(), err) continue } for _, pair := range pairs { resp, err := e.GetCachedOrderbook(pair, a) if err != nil { log.Errorf(log.RESTSys, "Exchange %s failed to retrieve %s orderbook. Err: %s\n", e.GetName(), pair, err) continue } ob := &gctrpc.OrderbookResponse{ Pair: &gctrpc.CurrencyPair{ Delimiter: pair.Delimiter, Base: pair.Base.String(), Quote: pair.Quote.String(), }, AssetType: a.String(), LastUpdated: s.unixTimestamp(resp.LastUpdated), Bids: make([]*gctrpc.OrderbookItem, len(resp.Bids)), Asks: make([]*gctrpc.OrderbookItem, len(resp.Asks)), } for i := range resp.Bids { ob.Bids[i] = &gctrpc.OrderbookItem{Amount: resp.Bids[i].Amount, Price: resp.Bids[i].Price} } for i := range resp.Asks { ob.Asks[i] = &gctrpc.OrderbookItem{Amount: resp.Asks[i].Amount, Price: resp.Asks[i].Price} } obs = append(obs, ob) } } obResponse = append(obResponse, &gctrpc.Orderbooks{Exchange: e.GetName(), Orderbooks: obs}) } return &gctrpc.GetOrderbooksResponse{Orderbooks: obResponse}, nil } // GetAccountInfo returns an account balance for a specific exchange func (s *RPCServer) GetAccountInfo(ctx context.Context, r *gctrpc.GetAccountInfoRequest) (*gctrpc.GetAccountInfoResponse, error) { assetType, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } err = checkParams(r.Exchange, exch, assetType, currency.EMPTYPAIR) if err != nil { return nil, err } resp, err := exch.GetCachedAccountInfo(ctx, assetType) if err != nil { return nil, err } return createAccountInfoRequest(resp) } // UpdateAccountInfo forces an update of the account info func (s *RPCServer) UpdateAccountInfo(ctx context.Context, r *gctrpc.GetAccountInfoRequest) (*gctrpc.GetAccountInfoResponse, error) { assetType, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } err = checkParams(r.Exchange, exch, assetType, currency.EMPTYPAIR) if err != nil { return nil, err } resp, err := exch.UpdateAccountInfo(ctx, assetType) if err != nil { return nil, err } return createAccountInfoRequest(resp) } func createAccountInfoRequest(h account.Holdings) (*gctrpc.GetAccountInfoResponse, error) { accounts := make([]*gctrpc.Account, len(h.Accounts)) for x := range h.Accounts { var a gctrpc.Account a.Id = h.Accounts[x].Credentials.String() for _, y := range h.Accounts[x].Currencies { if y.Total == 0 && y.Hold == 0 && y.Free == 0 && y.AvailableWithoutBorrow == 0 && y.Borrowed == 0 { continue } a.Currencies = append(a.Currencies, &gctrpc.AccountCurrencyInfo{ Currency: y.Currency.String(), TotalValue: y.Total, Hold: y.Hold, Free: y.Free, FreeWithoutBorrow: y.AvailableWithoutBorrow, Borrowed: y.Borrowed, UpdatedAt: timestamppb.New(y.UpdatedAt), }) } accounts[x] = &a } return &gctrpc.GetAccountInfoResponse{Exchange: h.Exchange, Accounts: accounts}, nil } // GetAccountInfoStream streams an account balance for a specific exchange func (s *RPCServer) GetAccountInfoStream(r *gctrpc.GetAccountInfoRequest, stream gctrpc.GoCryptoTraderService_GetAccountInfoStreamServer) error { assetType, err := asset.New(r.AssetType) if err != nil { return err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return err } err = checkParams(r.Exchange, exch, assetType, currency.EMPTYPAIR) if err != nil { return err } initAcc, err := exch.GetCachedAccountInfo(stream.Context(), assetType) if err != nil { return err } accounts := make([]*gctrpc.Account, len(initAcc.Accounts)) for x := range initAcc.Accounts { subAccounts := make([]*gctrpc.AccountCurrencyInfo, len(initAcc.Accounts[x].Currencies)) for y := range initAcc.Accounts[x].Currencies { subAccounts[y] = &gctrpc.AccountCurrencyInfo{ Currency: initAcc.Accounts[x].Currencies[y].Currency.String(), TotalValue: initAcc.Accounts[x].Currencies[y].Total, Hold: initAcc.Accounts[x].Currencies[y].Hold, UpdatedAt: timestamppb.New(initAcc.Accounts[x].Currencies[y].UpdatedAt), } } accounts[x] = &gctrpc.Account{ Id: initAcc.Accounts[x].ID, Currencies: subAccounts, } } err = stream.Send(&gctrpc.GetAccountInfoResponse{ Exchange: initAcc.Exchange, Accounts: accounts, }) if err != nil { return err } pipe, err := account.SubscribeToExchangeAccount(r.Exchange) if err != nil { return err } defer func() { pipeErr := pipe.Release() if pipeErr != nil { log.Errorln(log.DispatchMgr, pipeErr) } }() for { data, ok := <-pipe.Channel() if !ok { return errDispatchSystem } holdings, ok := data.(*account.Holdings) if !ok { return common.GetTypeAssertError("*account.Holdings", data) } accounts := make([]*gctrpc.Account, len(holdings.Accounts)) for x := range holdings.Accounts { subAccounts := make([]*gctrpc.AccountCurrencyInfo, len(holdings.Accounts[x].Currencies)) for y := range holdings.Accounts[x].Currencies { subAccounts[y] = &gctrpc.AccountCurrencyInfo{ Currency: holdings.Accounts[x].Currencies[y].Currency.String(), TotalValue: holdings.Accounts[x].Currencies[y].Total, Hold: holdings.Accounts[x].Currencies[y].Hold, UpdatedAt: timestamppb.New(holdings.Accounts[x].Currencies[y].UpdatedAt), } } accounts[x] = &gctrpc.Account{ Id: holdings.Accounts[x].ID, Currencies: subAccounts, } } if err := stream.Send(&gctrpc.GetAccountInfoResponse{ Exchange: holdings.Exchange, Accounts: accounts, }); err != nil { return err } } } // GetConfig returns the bots config func (s *RPCServer) GetConfig(_ context.Context, _ *gctrpc.GetConfigRequest) (*gctrpc.GetConfigResponse, error) { return &gctrpc.GetConfigResponse{}, common.ErrNotYetImplemented } // GetPortfolio returns the portfoliomanager details func (s *RPCServer) GetPortfolio(_ context.Context, _ *gctrpc.GetPortfolioRequest) (*gctrpc.GetPortfolioResponse, error) { botAddrs := s.portfolioManager.GetAddresses() addrs := make([]*gctrpc.PortfolioAddress, len(botAddrs)) for x := range botAddrs { addrs[x] = &gctrpc.PortfolioAddress{ Address: botAddrs[x].Address, CoinType: botAddrs[x].CoinType.String(), Description: botAddrs[x].Description, Balance: botAddrs[x].Balance, } } resp := &gctrpc.GetPortfolioResponse{ Portfolio: addrs, } return resp, nil } // GetPortfolioSummary returns the portfoliomanager summary func (s *RPCServer) GetPortfolioSummary(_ context.Context, _ *gctrpc.GetPortfolioSummaryRequest) (*gctrpc.GetPortfolioSummaryResponse, error) { result := s.portfolioManager.GetPortfolioSummary() var resp gctrpc.GetPortfolioSummaryResponse p := func(coins []portfolio.Coin) []*gctrpc.Coin { var c []*gctrpc.Coin for x := range coins { c = append(c, &gctrpc.Coin{ Coin: coins[x].Coin.String(), Balance: coins[x].Balance, Address: coins[x].Address, Percentage: coins[x].Percentage, }, ) } return c } resp.CoinTotals = p(result.Totals) resp.CoinsOffline = p(result.Offline) resp.CoinsOfflineSummary = make(map[string]*gctrpc.OfflineCoins) for k, v := range result.OfflineSummary { var o []*gctrpc.OfflineCoinSummary for x := range v { o = append(o, &gctrpc.OfflineCoinSummary{ Address: v[x].Address, Balance: v[x].Balance, Percentage: v[x].Percentage, }, ) } resp.CoinsOfflineSummary[k.String()] = &gctrpc.OfflineCoins{ Addresses: o, } } resp.CoinsOnline = p(result.Online) resp.CoinsOnlineSummary = make(map[string]*gctrpc.OnlineCoins) for k, v := range result.OnlineSummary { o := make(map[string]*gctrpc.OnlineCoinSummary) for x, y := range v { o[x.String()] = &gctrpc.OnlineCoinSummary{ Balance: y.Balance, Percentage: y.Percentage, } } resp.CoinsOnlineSummary[k] = &gctrpc.OnlineCoins{ Coins: o, } } return &resp, nil } // AddPortfolioAddress adds an address to the portfoliomanager manager func (s *RPCServer) AddPortfolioAddress(_ context.Context, r *gctrpc.AddPortfolioAddressRequest) (*gctrpc.GenericResponse, error) { err := s.portfolioManager.AddAddress(r.Address, r.Description, currency.NewCode(r.CoinType), r.Balance) if err != nil { return nil, err } return &gctrpc.GenericResponse{Status: MsgStatusSuccess}, nil } // RemovePortfolioAddress removes an address from the portfoliomanager manager func (s *RPCServer) RemovePortfolioAddress(_ context.Context, r *gctrpc.RemovePortfolioAddressRequest) (*gctrpc.GenericResponse, error) { err := s.portfolioManager.RemoveAddress(r.Address, r.Description, currency.NewCode(r.CoinType)) if err != nil { return nil, err } return &gctrpc.GenericResponse{Status: MsgStatusSuccess}, nil } // GetForexProviders returns a list of available forex providers func (s *RPCServer) GetForexProviders(_ context.Context, _ *gctrpc.GetForexProvidersRequest) (*gctrpc.GetForexProvidersResponse, error) { providers := s.Config.GetForexProviders() if len(providers) == 0 { return nil, errors.New("forex providers is empty") } forexProviders := make([]*gctrpc.ForexProvider, len(providers)) for x := range providers { forexProviders[x] = &gctrpc.ForexProvider{ Name: providers[x].Name, Enabled: providers[x].Enabled, Verbose: providers[x].Verbose, RestPollingDelay: s.Config.Currency.ForeignExchangeUpdateDuration.String(), ApiKey: providers[x].APIKey, ApiKeyLevel: int64(providers[x].APIKeyLvl), PrimaryProvider: providers[x].PrimaryProvider, } } return &gctrpc.GetForexProvidersResponse{ForexProviders: forexProviders}, nil } // GetForexRates returns a list of forex rates func (s *RPCServer) GetForexRates(_ context.Context, _ *gctrpc.GetForexRatesRequest) (*gctrpc.GetForexRatesResponse, error) { rates, err := currency.GetExchangeRates() if err != nil { return nil, err } if len(rates) == 0 { return nil, errors.New("forex rates is empty") } forexRates := make([]*gctrpc.ForexRatesConversion, 0, len(rates)) for x := range rates { rate, err := rates[x].GetRate() if err != nil { continue } // TODO add inverse rate // inverseRate, err := rates[x].GetInversionRate() // if err != nil { // continue // } forexRates = append(forexRates, &gctrpc.ForexRatesConversion{ From: rates[x].From.String(), To: rates[x].To.String(), Rate: rate, InverseRate: 0, }) } return &gctrpc.GetForexRatesResponse{ForexRates: forexRates}, nil } // GetOrders returns all open orders, filtered by exchange, currency pair or // asset type between optional dates func (s *RPCServer) GetOrders(ctx context.Context, r *gctrpc.GetOrdersRequest) (*gctrpc.GetOrdersResponse, error) { if r == nil { return nil, errInvalidArguments } a, err := asset.New(r.AssetType) if err != nil { return nil, err } if r.Pair == nil { return nil, errCurrencyPairUnset } cp := currency.NewPairWithDelimiter( r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } err = checkParams(r.Exchange, exch, a, cp) if err != nil { return nil, err } var start, end time.Time if r.StartDate != "" { start, err = time.Parse(common.SimpleTimeFormatWithTimezone, r.StartDate) if err != nil { return nil, err } } if r.EndDate != "" { end, err = time.Parse(common.SimpleTimeFormatWithTimezone, r.EndDate) if err != nil { return nil, err } } err = common.StartEndTimeCheck(start, end) if err != nil { return nil, err } req := &order.MultiOrderRequest{ Pairs: []currency.Pair{cp}, AssetType: a, Type: order.AnyType, Side: order.AnySide, } if !start.IsZero() { req.StartTime = start } if !end.IsZero() { req.EndTime = end } var resp []order.Detail resp, err = exch.GetActiveOrders(ctx, req) if err != nil { return nil, err } orders := make([]*gctrpc.OrderDetails, len(resp)) for x := range resp { trades := make([]*gctrpc.TradeHistory, len(resp[x].Trades)) for i := range resp[x].Trades { t := &gctrpc.TradeHistory{ Id: resp[x].Trades[i].TID, Price: resp[x].Trades[i].Price, Amount: resp[x].Trades[i].Amount, Exchange: r.Exchange, AssetType: a.String(), OrderSide: resp[x].Trades[i].Side.String(), Fee: resp[x].Trades[i].Fee, Total: resp[x].Trades[i].Total, } if !resp[x].Trades[i].Timestamp.IsZero() { t.CreationTime = s.unixTimestamp(resp[x].Trades[i].Timestamp) } trades[i] = t } o := &gctrpc.OrderDetails{ Exchange: r.Exchange, Id: resp[x].OrderID, ClientOrderId: resp[x].ClientOrderID, BaseCurrency: resp[x].Pair.Base.String(), QuoteCurrency: resp[x].Pair.Quote.String(), AssetType: resp[x].AssetType.String(), OrderSide: resp[x].Side.String(), OrderType: resp[x].Type.String(), Status: resp[x].Status.String(), Price: resp[x].Price, Amount: resp[x].Amount, OpenVolume: resp[x].Amount - resp[x].ExecutedAmount, Fee: resp[x].Fee, Cost: resp[x].Cost, Trades: trades, } if !resp[x].Date.IsZero() { o.CreationTime = resp[x].Date.Format(common.SimpleTimeFormatWithTimezone) } if !resp[x].LastUpdated.IsZero() { o.UpdateTime = resp[x].LastUpdated.Format(common.SimpleTimeFormatWithTimezone) } orders[x] = o } return &gctrpc.GetOrdersResponse{Orders: orders}, nil } // GetManagedOrders returns all orders from the Order Manager for the provided exchange, // asset type and currency pair func (s *RPCServer) GetManagedOrders(_ context.Context, r *gctrpc.GetOrdersRequest) (*gctrpc.GetOrdersResponse, error) { if r == nil { return nil, errInvalidArguments } a, err := asset.New(r.AssetType) if err != nil { return nil, err } if r.Pair == nil { return nil, errCurrencyPairUnset } cp := currency.NewPairWithDelimiter( r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } err = checkParams(r.Exchange, exch, a, cp) if err != nil { return nil, err } var resp []order.Detail filter := order.Filter{ Exchange: exch.GetName(), Pair: cp, AssetType: a, } resp, err = s.OrderManager.GetOrdersFiltered(&filter) if err != nil { return nil, err } orders := make([]*gctrpc.OrderDetails, len(resp)) for x := range resp { trades := make([]*gctrpc.TradeHistory, len(resp[x].Trades)) for i := range resp[x].Trades { t := &gctrpc.TradeHistory{ Id: resp[x].Trades[i].TID, Price: resp[x].Trades[i].Price, Amount: resp[x].Trades[i].Amount, Exchange: r.Exchange, AssetType: a.String(), OrderSide: resp[x].Trades[i].Side.String(), Fee: resp[x].Trades[i].Fee, Total: resp[x].Trades[i].Total, } if !resp[x].Trades[i].Timestamp.IsZero() { t.CreationTime = s.unixTimestamp(resp[x].Trades[i].Timestamp) } trades[i] = t } o := &gctrpc.OrderDetails{ Exchange: r.Exchange, Id: resp[x].OrderID, ClientOrderId: resp[x].ClientOrderID, BaseCurrency: resp[x].Pair.Base.String(), QuoteCurrency: resp[x].Pair.Quote.String(), AssetType: resp[x].AssetType.String(), OrderSide: resp[x].Side.String(), OrderType: resp[x].Type.String(), Status: resp[x].Status.String(), Price: resp[x].Price, Amount: resp[x].Amount, OpenVolume: resp[x].Amount - resp[x].ExecutedAmount, Fee: resp[x].Fee, Cost: resp[x].Cost, Trades: trades, } if !resp[x].Date.IsZero() { o.CreationTime = resp[x].Date.Format(common.SimpleTimeFormatWithTimezone) } if !resp[x].LastUpdated.IsZero() { o.UpdateTime = resp[x].LastUpdated.Format(common.SimpleTimeFormatWithTimezone) } orders[x] = o } return &gctrpc.GetOrdersResponse{Orders: orders}, nil } // GetOrder returns order information based on exchange and order ID func (s *RPCServer) GetOrder(ctx context.Context, r *gctrpc.GetOrderRequest) (*gctrpc.OrderDetails, error) { if r == nil { return nil, errInvalidArguments } if r.Pair == nil { return nil, errCurrencyPairUnset } a, err := asset.New(r.Asset) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } pair := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, a, pair) if err != nil { return nil, err } result, err := s.OrderManager.GetOrderInfo(ctx, r.Exchange, r.OrderId, pair, a) if err != nil { return nil, fmt.Errorf("error whilst trying to retrieve info for order %s: %w", r.OrderId, err) } trades := make([]*gctrpc.TradeHistory, len(result.Trades)) for i := range result.Trades { trades[i] = &gctrpc.TradeHistory{ CreationTime: s.unixTimestamp(result.Trades[i].Timestamp), Id: result.Trades[i].TID, Price: result.Trades[i].Price, Amount: result.Trades[i].Amount, Exchange: result.Trades[i].Exchange, AssetType: result.Trades[i].Type.String(), OrderSide: result.Trades[i].Side.String(), Fee: result.Trades[i].Fee, Total: result.Trades[i].Total, } } var creationTime, updateTime string if !result.Date.IsZero() { creationTime = result.Date.Format(common.SimpleTimeFormatWithTimezone) } if !result.LastUpdated.IsZero() { updateTime = result.LastUpdated.Format(common.SimpleTimeFormatWithTimezone) } return &gctrpc.OrderDetails{ Exchange: result.Exchange, Id: result.OrderID, ClientOrderId: result.ClientOrderID, BaseCurrency: result.Pair.Base.String(), QuoteCurrency: result.Pair.Quote.String(), AssetType: result.AssetType.String(), OrderSide: result.Side.String(), OrderType: result.Type.String(), CreationTime: creationTime, Status: result.Status.String(), Price: result.Price, Amount: result.Amount, OpenVolume: result.RemainingAmount, Fee: result.Fee, Trades: trades, Cost: result.Cost, UpdateTime: updateTime, }, err } // SubmitOrder submits an order specified by exchange, currency pair and asset type func (s *RPCServer) SubmitOrder(ctx context.Context, r *gctrpc.SubmitOrderRequest) (*gctrpc.SubmitOrderResponse, error) { a, err := asset.New(r.AssetType) if err != nil { return nil, err } var marginType margin.Type if r.MarginType != "" { marginType, err = margin.StringToMarginType(r.MarginType) if err != nil { return nil, err } } if r.Pair == nil { return nil, errCurrencyPairUnset } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, a, p) if err != nil { return nil, err } side, err := order.StringToOrderSide(r.Side) if err != nil { return nil, err } oType, err := order.StringToOrderType(r.OrderType) if err != nil { return nil, err } submission := &order.Submit{ Pair: p, Side: side, Type: oType, Amount: r.Amount, Price: r.Price, ClientID: r.ClientId, ClientOrderID: r.ClientId, Exchange: r.Exchange, AssetType: a, } if r.MarginType != "" { submission.MarginType = marginType } resp, err := s.OrderManager.Submit(ctx, submission) if err != nil { return &gctrpc.SubmitOrderResponse{}, err } trades := make([]*gctrpc.Trades, len(resp.Trades)) for i := range resp.Trades { trades[i] = &gctrpc.Trades{ Amount: resp.Trades[i].Amount, Price: resp.Trades[i].Price, Fee: resp.Trades[i].Fee, FeeAsset: resp.Trades[i].FeeAsset, } } return &gctrpc.SubmitOrderResponse{ OrderId: resp.OrderID, OrderPlaced: resp.WasOrderPlaced(), Trades: trades, }, nil } // SimulateOrder simulates an order specified by exchange, currency pair and asset // type func (s *RPCServer) SimulateOrder(_ context.Context, r *gctrpc.SimulateOrderRequest) (*gctrpc.SimulateOrderResponse, error) { if r.Pair == nil { return nil, errCurrencyPairUnset } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, asset.Spot, p) if err != nil { return nil, err } o, err := exch.GetCachedOrderbook(p, asset.Spot) if err != nil { return nil, err } buy := true if !strings.EqualFold(r.Side, order.Buy.String()) && !strings.EqualFold(r.Side, order.Bid.String()) { buy = false } result, err := o.SimulateOrder(r.Amount, buy) if err != nil { return nil, err } var resp gctrpc.SimulateOrderResponse for x := range result.Orders { resp.Orders = append(resp.Orders, &gctrpc.OrderbookItem{ Price: result.Orders[x].Price, Amount: result.Orders[x].Amount, }) } resp.Amount = result.Amount resp.MaximumPrice = result.MaximumPrice resp.MinimumPrice = result.MinimumPrice resp.PercentageGainLoss = result.PercentageGainOrLoss resp.Status = result.Status return &resp, nil } // WhaleBomb finds the amount required to reach a specific price target for a given exchange, pair // and asset type func (s *RPCServer) WhaleBomb(_ context.Context, r *gctrpc.WhaleBombRequest) (*gctrpc.SimulateOrderResponse, error) { if r.Pair == nil { return nil, errCurrencyPairUnset } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } a, err := asset.New(r.AssetType) if err != nil { return nil, err } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, a, p) if err != nil { return nil, err } o, err := exch.GetCachedOrderbook(p, a) if err != nil { return nil, err } buy := true if !strings.EqualFold(r.Side, order.Buy.String()) && !strings.EqualFold(r.Side, order.Bid.String()) { buy = false } result, err := o.WhaleBomb(r.PriceTarget, buy) if err != nil { return nil, err } var resp gctrpc.SimulateOrderResponse for x := range result.Orders { resp.Orders = append(resp.Orders, &gctrpc.OrderbookItem{ Price: result.Orders[x].Price, Amount: result.Orders[x].Amount, }) } resp.Amount = result.Amount resp.MaximumPrice = result.MaximumPrice resp.MinimumPrice = result.MinimumPrice resp.PercentageGainLoss = result.PercentageGainOrLoss resp.Status = result.Status return &resp, err } // CancelOrder cancels an order specified by exchange, currency pair and asset // type func (s *RPCServer) CancelOrder(ctx context.Context, r *gctrpc.CancelOrderRequest) (*gctrpc.GenericResponse, error) { if r.Pair == nil { return nil, errCurrencyPairUnset } a, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, a, p) if err != nil { return nil, err } var side order.Side side, err = order.StringToOrderSide(r.Side) if err != nil { return nil, err } err = s.OrderManager.Cancel(ctx, &order.Cancel{ Exchange: r.Exchange, AccountID: r.AccountId, OrderID: r.OrderId, Side: side, WalletAddress: r.WalletAddress, Pair: p, AssetType: a, }) if err != nil { return nil, err } return &gctrpc.GenericResponse{ Status: MsgStatusSuccess, Data: fmt.Sprintf("order %s cancelled", r.OrderId), }, nil } // CancelBatchOrders cancels an orders specified by exchange, currency pair and asset type func (s *RPCServer) CancelBatchOrders(ctx context.Context, r *gctrpc.CancelBatchOrdersRequest) (*gctrpc.CancelBatchOrdersResponse, error) { assetType, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } pair := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, assetType, pair) if err != nil { return nil, err } var side order.Side side, err = order.StringToOrderSide(r.Side) if err != nil { return nil, err } status := make(map[string]string) orders := strings.Split(r.OrdersId, ",") req := make([]order.Cancel, len(orders)) for x := range orders { orderID := orders[x] status[orderID] = order.Cancelled.String() req[x] = order.Cancel{ AccountID: r.AccountId, OrderID: orderID, Side: side, WalletAddress: r.WalletAddress, Pair: pair, AssetType: assetType, } } // TODO: Change to order manager _, err = exch.CancelBatchOrders(ctx, req) if err != nil { return nil, err } return &gctrpc.CancelBatchOrdersResponse{ Orders: []*gctrpc.Orders{{ Exchange: r.Exchange, OrderStatus: status, }}, }, nil } // CancelAllOrders cancels all orders, filterable by exchange func (s *RPCServer) CancelAllOrders(ctx context.Context, r *gctrpc.CancelAllOrdersRequest) (*gctrpc.CancelAllOrdersResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } // TODO: Change to order manager resp, err := exch.CancelAllOrders(ctx, nil) if err != nil { return &gctrpc.CancelAllOrdersResponse{}, err } return &gctrpc.CancelAllOrdersResponse{ Count: resp.Count, // count of deleted orders }, nil } // ModifyOrder modifies an existing order if it exists func (s *RPCServer) ModifyOrder(ctx context.Context, r *gctrpc.ModifyOrderRequest) (*gctrpc.ModifyOrderResponse, error) { assetType, err := asset.New(r.Asset) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } pair := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, assetType, pair) if err != nil { return nil, err } resp, err := s.OrderManager.Modify(ctx, &order.Modify{ Exchange: r.Exchange, AssetType: assetType, Pair: pair, OrderID: r.OrderId, Amount: r.Amount, Price: r.Price, }) if err != nil { return nil, err } return &gctrpc.ModifyOrderResponse{ ModifiedOrderId: resp.OrderID, }, nil } // GetEvents returns the stored events list func (s *RPCServer) GetEvents(_ context.Context, _ *gctrpc.GetEventsRequest) (*gctrpc.GetEventsResponse, error) { return &gctrpc.GetEventsResponse{}, common.ErrNotYetImplemented } // AddEvent adds an event func (s *RPCServer) AddEvent(_ context.Context, r *gctrpc.AddEventRequest) (*gctrpc.AddEventResponse, error) { evtCondition := EventConditionParams{ CheckBids: r.ConditionParams.CheckBids, CheckAsks: r.ConditionParams.CheckAsks, Condition: r.ConditionParams.Condition, OrderbookAmount: r.ConditionParams.OrderbookAmount, Price: r.ConditionParams.Price, } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) a, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } err = checkParams(r.Exchange, exch, a, p) if err != nil { return nil, err } id, err := s.eventManager.Add(r.Exchange, r.Item, evtCondition, p, a, r.Action) if err != nil { return nil, err } return &gctrpc.AddEventResponse{Id: id}, nil } // RemoveEvent removes an event, specified by an event ID func (s *RPCServer) RemoveEvent(_ context.Context, r *gctrpc.RemoveEventRequest) (*gctrpc.GenericResponse, error) { if !s.eventManager.Remove(r.Id) { return nil, fmt.Errorf("event %d not removed", r.Id) } return &gctrpc.GenericResponse{ Status: MsgStatusSuccess, Data: fmt.Sprintf("event %d removed", r.Id), }, nil } // GetCryptocurrencyDepositAddresses returns a list of cryptocurrency deposit // addresses specified by an exchange func (s *RPCServer) GetCryptocurrencyDepositAddresses(_ context.Context, r *gctrpc.GetCryptocurrencyDepositAddressesRequest) (*gctrpc.GetCryptocurrencyDepositAddressesResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsRESTAuthenticationSupported() { return nil, fmt.Errorf("%s, %w", r.Exchange, exchange.ErrAuthenticationSupportNotEnabled) } result, err := s.GetCryptocurrencyDepositAddressesByExchange(r.Exchange) if err != nil { return nil, err } var resp gctrpc.GetCryptocurrencyDepositAddressesResponse resp.Addresses = make(map[string]*gctrpc.DepositAddresses) for k, v := range result { var depositAddrs []*gctrpc.DepositAddress for a := range v { depositAddrs = append(depositAddrs, &gctrpc.DepositAddress{ Address: v[a].Address, Tag: v[a].Tag, Chain: v[a].Chain, }) } resp.Addresses[k] = &gctrpc.DepositAddresses{Addresses: depositAddrs} } return &resp, nil } // GetCryptocurrencyDepositAddress returns a cryptocurrency deposit address // specified by exchange and cryptocurrency func (s *RPCServer) GetCryptocurrencyDepositAddress(ctx context.Context, r *gctrpc.GetCryptocurrencyDepositAddressRequest) (*gctrpc.GetCryptocurrencyDepositAddressResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsRESTAuthenticationSupported() { return nil, fmt.Errorf("%s, %w", r.Exchange, exchange.ErrAuthenticationSupportNotEnabled) } addr, err := s.GetExchangeCryptocurrencyDepositAddress(ctx, r.Exchange, "", r.Chain, currency.NewCode(r.Cryptocurrency), r.Bypass, ) if err != nil { return nil, err } return &gctrpc.GetCryptocurrencyDepositAddressResponse{ Address: addr.Address, Tag: addr.Tag, }, nil } // GetAvailableTransferChains returns the supported transfer chains specified by // exchange and cryptocurrency func (s *RPCServer) GetAvailableTransferChains(ctx context.Context, r *gctrpc.GetAvailableTransferChainsRequest) (*gctrpc.GetAvailableTransferChainsResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } curr := currency.NewCode(r.Cryptocurrency) if curr.IsEmpty() { return nil, errCurrencyNotSpecified } resp, err := exch.GetAvailableTransferChains(ctx, curr) if err != nil { return nil, err } if len(resp) == 0 { return nil, errors.New("no available transfer chains found") } return &gctrpc.GetAvailableTransferChainsResponse{ Chains: resp, }, nil } // WithdrawCryptocurrencyFunds withdraws cryptocurrency funds specified by // exchange func (s *RPCServer) WithdrawCryptocurrencyFunds(ctx context.Context, r *gctrpc.WithdrawCryptoRequest) (*gctrpc.WithdrawResponse, error) { _, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } req := &withdraw.Request{ Exchange: r.Exchange, Amount: r.Amount, Currency: currency.NewCode(strings.ToUpper(r.Currency)), Type: withdraw.Crypto, Description: r.Description, Crypto: withdraw.CryptoRequest{ Address: r.Address, AddressTag: r.AddressTag, FeeAmount: r.Fee, Chain: r.Chain, }, } exchCfg, err := s.Config.GetExchangeConfig(r.Exchange) if err != nil { return nil, err } if exchCfg.API.Credentials.OTPSecret != "" { code, errOTP := totp.GenerateCode(exchCfg.API.Credentials.OTPSecret, time.Now()) if errOTP != nil { return nil, errOTP } codeNum, errOTP := strconv.ParseInt(code, 10, 64) if errOTP != nil { return nil, errOTP } req.OneTimePassword = codeNum } if exchCfg.API.Credentials.PIN != "" { pinCode, errPin := strconv.ParseInt(exchCfg.API.Credentials.PIN, 10, 64) if errPin != nil { return nil, errPin } req.PIN = pinCode } req.TradePassword = exchCfg.API.Credentials.TradePassword resp, err := s.Engine.WithdrawManager.SubmitWithdrawal(ctx, req) if err != nil { return nil, err } return &gctrpc.WithdrawResponse{ Id: resp.ID.String(), Status: resp.Exchange.Status, }, nil } // WithdrawFiatFunds withdraws fiat funds specified by exchange func (s *RPCServer) WithdrawFiatFunds(ctx context.Context, r *gctrpc.WithdrawFiatRequest) (*gctrpc.WithdrawResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } bankAccount, err := banking.GetBankAccountByID(r.BankAccountId) if err != nil { base := exch.GetBase() if base == nil { return nil, errExchangeBaseNotFound } bankAccount, err = base.GetExchangeBankAccounts(r.BankAccountId, r.Currency) if err != nil { return nil, err } } req := &withdraw.Request{ Exchange: r.Exchange, Amount: r.Amount, Currency: currency.NewCode(strings.ToUpper(r.Currency)), Type: withdraw.Fiat, Description: r.Description, Fiat: withdraw.FiatRequest{ Bank: *bankAccount, }, } exchCfg, err := s.Config.GetExchangeConfig(r.Exchange) if err != nil { return nil, err } if exchCfg.API.Credentials.OTPSecret != "" { code, errOTP := totp.GenerateCode(exchCfg.API.Credentials.OTPSecret, time.Now()) if errOTP != nil { return nil, errOTP } codeNum, errOTP := strconv.ParseInt(code, 10, 64) if errOTP != nil { return nil, errOTP } req.OneTimePassword = codeNum } if exchCfg.API.Credentials.PIN != "" { pinCode, errPIN := strconv.ParseInt(exchCfg.API.Credentials.PIN, 10, 64) if errPIN != nil { return nil, errPIN } req.PIN = pinCode } req.TradePassword = exchCfg.API.Credentials.TradePassword resp, err := s.Engine.WithdrawManager.SubmitWithdrawal(ctx, req) if err != nil { return nil, err } return &gctrpc.WithdrawResponse{ Id: resp.ID.String(), Status: resp.Exchange.Status, }, nil } // WithdrawalEventByID returns previous withdrawal request details func (s *RPCServer) WithdrawalEventByID(_ context.Context, r *gctrpc.WithdrawalEventByIDRequest) (*gctrpc.WithdrawalEventByIDResponse, error) { if !s.Config.Database.Enabled { return nil, database.ErrDatabaseSupportDisabled } v, err := s.WithdrawManager.WithdrawalEventByID(r.Id) if err != nil { return nil, err } resp := &gctrpc.WithdrawalEventByIDResponse{ Event: &gctrpc.WithdrawalEventResponse{ Id: v.ID.String(), Exchange: &gctrpc.WithdrawlExchangeEvent{ Name: v.Exchange.Name, Id: v.Exchange.Name, Status: v.Exchange.Status, }, Request: &gctrpc.WithdrawalRequestEvent{ Currency: v.RequestDetails.Currency.String(), Description: v.RequestDetails.Description, Amount: v.RequestDetails.Amount, Type: int64(v.RequestDetails.Type), }, }, } resp.Event.CreatedAt = timestamppb.New(v.CreatedAt) if err := resp.Event.CreatedAt.CheckValid(); err != nil { log.Errorf(log.GRPCSys, "withdrawal event by id CreatedAt: %s", err) } resp.Event.UpdatedAt = timestamppb.New(v.UpdatedAt) if err := resp.Event.UpdatedAt.CheckValid(); err != nil { log.Errorf(log.GRPCSys, "withdrawal event by id UpdatedAt: %s", err) } if v.RequestDetails.Type == withdraw.Crypto { resp.Event.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent) resp.Event.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{ Address: v.RequestDetails.Crypto.Address, AddressTag: v.RequestDetails.Crypto.AddressTag, Fee: v.RequestDetails.Crypto.FeeAmount, } } else if v.RequestDetails.Type == withdraw.Fiat { if v.RequestDetails.Fiat != (withdraw.FiatRequest{}) { resp.Event.Request.Fiat = new(gctrpc.FiatWithdrawalEvent) resp.Event.Request.Fiat = &gctrpc.FiatWithdrawalEvent{ BankName: v.RequestDetails.Fiat.Bank.BankName, AccountName: v.RequestDetails.Fiat.Bank.AccountName, AccountNumber: v.RequestDetails.Fiat.Bank.AccountNumber, Bsb: v.RequestDetails.Fiat.Bank.BSBNumber, Swift: v.RequestDetails.Fiat.Bank.SWIFTCode, Iban: v.RequestDetails.Fiat.Bank.IBAN, } } } return resp, nil } // WithdrawalEventsByExchange returns previous withdrawal request details by exchange func (s *RPCServer) WithdrawalEventsByExchange(ctx context.Context, r *gctrpc.WithdrawalEventsByExchangeRequest) (*gctrpc.WithdrawalEventsByExchangeResponse, error) { if !s.Config.Database.Enabled { if r.Id == "" { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } c := currency.NewCode(strings.ToUpper(r.Currency)) a, err := asset.New(r.AssetType) if err != nil { return nil, err } ret, err := exch.GetWithdrawalsHistory(ctx, c, a) if err != nil { return nil, err } return parseWithdrawalsHistory(ret, exch.GetName(), int(r.Limit)), nil } return nil, database.ErrDatabaseSupportDisabled } if r.Id == "" { ret, err := s.WithdrawManager.WithdrawalEventByExchange(r.Exchange, int(r.Limit)) if err != nil { return nil, err } return parseMultipleEvents(ret), nil } ret, err := s.WithdrawManager.WithdrawalEventByExchangeID(r.Exchange, r.Id) if err != nil { return nil, err } return parseSingleEvents(ret), nil } // WithdrawalEventsByDate returns previous withdrawal request details by exchange func (s *RPCServer) WithdrawalEventsByDate(_ context.Context, r *gctrpc.WithdrawalEventsByDateRequest) (*gctrpc.WithdrawalEventsByExchangeResponse, error) { start, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.Start) if err != nil { return nil, fmt.Errorf("%w cannot parse start time %v", errInvalidTimes, err) } end, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.End) if err != nil { return nil, fmt.Errorf("%w cannot parse end time %v", errInvalidTimes, err) } err = common.StartEndTimeCheck(start, end) if err != nil { return nil, err } var ret []*withdraw.Response ret, err = s.WithdrawManager.WithdrawEventByDate(r.Exchange, start, end, int(r.Limit)) if err != nil { return nil, err } return parseMultipleEvents(ret), nil } // GetLoggerDetails returns a loggers details func (s *RPCServer) GetLoggerDetails(_ context.Context, r *gctrpc.GetLoggerDetailsRequest) (*gctrpc.GetLoggerDetailsResponse, error) { levels, err := log.Level(r.Logger) if err != nil { return nil, err } return &gctrpc.GetLoggerDetailsResponse{ Info: levels.Info, Debug: levels.Debug, Warn: levels.Warn, Error: levels.Error, }, nil } // SetLoggerDetails sets a loggers details func (s *RPCServer) SetLoggerDetails(_ context.Context, r *gctrpc.SetLoggerDetailsRequest) (*gctrpc.GetLoggerDetailsResponse, error) { levels, err := log.SetLevel(r.Logger, r.Level) if err != nil { return nil, err } return &gctrpc.GetLoggerDetailsResponse{ Info: levels.Info, Debug: levels.Debug, Warn: levels.Warn, Error: levels.Error, }, nil } // GetExchangePairs returns a list of exchange supported assets and related pairs func (s *RPCServer) GetExchangePairs(_ context.Context, r *gctrpc.GetExchangePairsRequest) (*gctrpc.GetExchangePairsResponse, error) { exchCfg, err := s.Config.GetExchangeConfig(r.Exchange) if err != nil { return nil, err } assetTypes := exchCfg.CurrencyPairs.GetAssetTypes(false) var a asset.Item if r.Asset != "" { a, err = asset.New(r.Asset) if err != nil { return nil, err } if !assetTypes.Contains(a) { return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) } } var resp gctrpc.GetExchangePairsResponse resp.SupportedAssets = make(map[string]*gctrpc.PairsSupported) for x := range assetTypes { if r.Asset != "" && !strings.EqualFold(assetTypes[x].String(), r.Asset) { continue } var enabled currency.Pairs enabled, err = exchCfg.CurrencyPairs.GetPairs(assetTypes[x], true) if err != nil { return nil, err } var available currency.Pairs available, err = exchCfg.CurrencyPairs.GetPairs(assetTypes[x], false) if err != nil { return nil, err } resp.SupportedAssets[assetTypes[x].String()] = &gctrpc.PairsSupported{ AvailablePairs: available.Join(), EnabledPairs: enabled.Join(), } } return &resp, nil } // SetExchangePair enables/disabled the specified pair(s) on an exchange func (s *RPCServer) SetExchangePair(_ context.Context, r *gctrpc.SetExchangePairRequest) (*gctrpc.GenericResponse, error) { exchCfg, err := s.Config.GetExchangeConfig(r.Exchange) if err != nil { return nil, err } a, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } err = checkParams(r.Exchange, exch, a, currency.EMPTYPAIR) if err != nil { return nil, err } base := exch.GetBase() if base == nil { return nil, errExchangeBaseNotFound } pairFmt, err := s.Config.GetPairFormat(r.Exchange, a) if err != nil { return nil, err } var pass bool var newErrors error for i := range r.Pairs { var p currency.Pair p, err = currency.NewPairFromStrings(r.Pairs[i].Base, r.Pairs[i].Quote) if err != nil { return nil, err } if r.Enable { err = exchCfg.CurrencyPairs.EnablePair(a, p.Format(pairFmt)) if err != nil { newErrors = common.AppendError(newErrors, fmt.Errorf("%s %w", r.Pairs[i], err)) continue } err = base.CurrencyPairs.EnablePair(a, p) if err != nil { newErrors = common.AppendError(newErrors, fmt.Errorf("%s %w", r.Pairs[i], err)) continue } pass = true continue } err = exchCfg.CurrencyPairs.DisablePair(a, p.Format(pairFmt)) if err != nil { if errors.Is(err, currency.ErrPairNotFound) { newErrors = common.AppendError(newErrors, fmt.Errorf("%s %w", r.Pairs[i], errSpecificPairNotEnabled)) continue } return nil, err } err = base.CurrencyPairs.DisablePair(a, p) if err != nil { if errors.Is(err, currency.ErrPairNotFound) { newErrors = common.AppendError(newErrors, fmt.Errorf("%s %w", r.Pairs[i], errSpecificPairNotEnabled)) continue } return nil, err } pass = true } if exch.IsWebsocketEnabled() && pass && base.Websocket.IsConnected() { err = exch.FlushWebsocketChannels() if err != nil { newErrors = common.AppendError(newErrors, err) } } if newErrors != nil { return nil, newErrors } return &gctrpc.GenericResponse{Status: MsgStatusSuccess}, nil } // GetOrderbookStream streams the requested updated orderbook func (s *RPCServer) GetOrderbookStream(r *gctrpc.GetOrderbookStreamRequest, stream gctrpc.GoCryptoTraderService_GetOrderbookStreamServer) error { a, err := asset.New(r.AssetType) if err != nil { return err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return err } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, a, p) if err != nil { return err } depth, err := orderbook.GetDepth(r.Exchange, p, a) if err != nil { return err } for { resp := &gctrpc.OrderbookResponse{ Pair: &gctrpc.CurrencyPair{Base: r.Pair.Base, Quote: r.Pair.Quote}, AssetType: r.AssetType, } base, err := depth.Retrieve() if err != nil { resp.Error = err.Error() resp.LastUpdated = time.Now().UnixMicro() } else { resp.LastUpdated = base.LastUpdated.UnixMicro() resp.Bids = make([]*gctrpc.OrderbookItem, len(base.Bids)) for i := range base.Bids { resp.Bids[i] = &gctrpc.OrderbookItem{ Amount: base.Bids[i].Amount, Price: base.Bids[i].Price, Id: base.Bids[i].ID, } } resp.Asks = make([]*gctrpc.OrderbookItem, len(base.Asks)) for i := range base.Asks { resp.Asks[i] = &gctrpc.OrderbookItem{ Amount: base.Asks[i].Amount, Price: base.Asks[i].Price, Id: base.Asks[i].ID, } } } err = stream.Send(resp) if err != nil { return err } <-depth.Wait(nil) } } // GetExchangeOrderbookStream streams all orderbooks associated with an exchange func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStreamRequest, stream gctrpc.GoCryptoTraderService_GetExchangeOrderbookStreamServer) error { if r.Exchange == "" { return errExchangeNameUnset } if _, err := s.GetExchangeByName(r.Exchange); err != nil { return err } pipe, err := orderbook.SubscribeToExchangeOrderbooks(r.Exchange) if err != nil { return err } defer func() { pipeErr := pipe.Release() if pipeErr != nil { log.Errorln(log.DispatchMgr, pipeErr) } }() for { data, ok := <-pipe.Channel() if !ok { return errDispatchSystem } d, ok := data.(orderbook.Outbound) if !ok { return common.GetTypeAssertError("orderbook.Outbound", data) } resp := &gctrpc.OrderbookResponse{} ob, err := d.Retrieve() if err != nil { resp.Error = err.Error() resp.LastUpdated = time.Now().UnixMicro() } else { resp.LastUpdated = ob.LastUpdated.UnixMicro() resp.Pair = &gctrpc.CurrencyPair{ Base: ob.Pair.Base.String(), Quote: ob.Pair.Quote.String(), } resp.AssetType = ob.Asset.String() resp.Bids = make([]*gctrpc.OrderbookItem, len(ob.Bids)) for i := range ob.Bids { resp.Bids[i] = &gctrpc.OrderbookItem{ Amount: ob.Bids[i].Amount, Price: ob.Bids[i].Price, Id: ob.Bids[i].ID, } } resp.Asks = make([]*gctrpc.OrderbookItem, len(ob.Asks)) for i := range ob.Asks { resp.Asks[i] = &gctrpc.OrderbookItem{ Amount: ob.Asks[i].Amount, Price: ob.Asks[i].Price, Id: ob.Asks[i].ID, } } } err = stream.Send(resp) if err != nil { return err } } } // GetTickerStream streams the requested updated ticker func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gctrpc.GoCryptoTraderService_GetTickerStreamServer) error { if r.Exchange == "" { return errExchangeNameUnset } if _, err := s.GetExchangeByName(r.Exchange); err != nil { return err } a, err := asset.New(r.AssetType) if err != nil { return err } if r.Pair.String() == "" { return errCurrencyPairUnset } if r.AssetType == "" { return errAssetTypeUnset } p, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) if err != nil { return err } pipe, err := ticker.SubscribeTicker(r.Exchange, p, a) if err != nil { return err } defer func() { pipeErr := pipe.Release() if pipeErr != nil { log.Errorln(log.DispatchMgr, pipeErr) } }() for { data, ok := <-pipe.Channel() if !ok { return errDispatchSystem } t, ok := data.(*ticker.Price) if !ok { return common.GetTypeAssertError("*ticker.Price", data) } err := stream.Send(&gctrpc.TickerResponse{ Pair: &gctrpc.CurrencyPair{ Base: t.Pair.Base.String(), Quote: t.Pair.Quote.String(), Delimiter: t.Pair.Delimiter, }, LastUpdated: s.unixTimestamp(t.LastUpdated), Last: t.Last, High: t.High, Low: t.Low, Bid: t.Bid, Ask: t.Ask, Volume: t.Volume, PriceAth: t.PriceATH, }) if err != nil { return err } } } // GetExchangeTickerStream streams all tickers associated with an exchange func (s *RPCServer) GetExchangeTickerStream(r *gctrpc.GetExchangeTickerStreamRequest, stream gctrpc.GoCryptoTraderService_GetExchangeTickerStreamServer) error { if r.Exchange == "" { return errExchangeNameUnset } if _, err := s.GetExchangeByName(r.Exchange); err != nil { return err } pipe, err := ticker.SubscribeToExchangeTickers(r.Exchange) if err != nil { return err } defer func() { pipeErr := pipe.Release() if pipeErr != nil { log.Errorln(log.DispatchMgr, pipeErr) } }() for { data, ok := <-pipe.Channel() if !ok { return errDispatchSystem } t, ok := data.(*ticker.Price) if !ok { return common.GetTypeAssertError("*ticker.Price", data) } err := stream.Send(&gctrpc.TickerResponse{ Pair: &gctrpc.CurrencyPair{ Base: t.Pair.Base.String(), Quote: t.Pair.Quote.String(), Delimiter: t.Pair.Delimiter, }, LastUpdated: s.unixTimestamp(t.LastUpdated), Last: t.Last, High: t.High, Low: t.Low, Bid: t.Bid, Ask: t.Ask, Volume: t.Volume, PriceAth: t.PriceATH, }) if err != nil { return err } } } // GetAuditEvent returns matching audit events from database func (s *RPCServer) GetAuditEvent(_ context.Context, r *gctrpc.GetAuditEventRequest) (*gctrpc.GetAuditEventResponse, error) { start, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.StartDate) if err != nil { return nil, fmt.Errorf("%w cannot parse start time %v", errInvalidTimes, err) } end, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.EndDate) if err != nil { return nil, fmt.Errorf("%w cannot parse end time %v", errInvalidTimes, err) } err = common.StartEndTimeCheck(start, end) if err != nil { return nil, err } events, err := audit.GetEvent(start, end, r.OrderBy, int(r.Limit)) if err != nil { return nil, err } resp := gctrpc.GetAuditEventResponse{} switch v := events.(type) { case postgres.AuditEventSlice: for x := range v { tempEvent := &gctrpc.AuditEvent{ Type: v[x].Type, Identifier: v[x].Identifier, Message: v[x].Message, Timestamp: v[x].CreatedAt.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), } resp.Events = append(resp.Events, tempEvent) } case sqlite3.AuditEventSlice: for x := range v { tempEvent := &gctrpc.AuditEvent{ Type: v[x].Type, Identifier: v[x].Identifier, Message: v[x].Message, Timestamp: v[x].CreatedAt, } resp.Events = append(resp.Events, tempEvent) } } return &resp, nil } // GetHistoricCandles returns historical candles for a given exchange func (s *RPCServer) GetHistoricCandles(ctx context.Context, r *gctrpc.GetHistoricCandlesRequest) (*gctrpc.GetHistoricCandlesResponse, error) { start, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.Start) if err != nil { return nil, fmt.Errorf("%w cannot parse start time %v", errInvalidTimes, err) } end, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.End) if err != nil { return nil, fmt.Errorf("%w cannot parse end time %v", errInvalidTimes, err) } err = common.StartEndTimeCheck(start, end) if err != nil { return nil, err } if r.Pair == nil { return nil, errCurrencyPairUnset } a, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } pair := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, a, pair) if err != nil { return nil, err } interval := kline.Interval(r.TimeInterval) resp := gctrpc.GetHistoricCandlesResponse{ Interval: interval.Short(), Pair: r.Pair, Start: start.UTC().Format(common.SimpleTimeFormatWithTimezone), End: end.UTC().Format(common.SimpleTimeFormatWithTimezone), } var klineItem *kline.Item if r.UseDb { klineItem, err = kline.LoadFromDatabase(r.Exchange, pair, a, interval, start, end) } else { if r.ExRequest { klineItem, err = exch.GetHistoricCandlesExtended(ctx, pair, a, interval, start, end) } else { klineItem, err = exch.GetHistoricCandles(ctx, pair, a, interval, start, end) } } if err != nil { return nil, err } if r.FillMissingWithTrades { var tradeDataKline *kline.Item tradeDataKline, err = fillMissingCandlesWithStoredTrades(start, end, klineItem) if err != nil { return nil, err } klineItem.Candles = append(klineItem.Candles, tradeDataKline.Candles...) } resp.Exchange = klineItem.Exchange for i := range klineItem.Candles { resp.Candle = append(resp.Candle, &gctrpc.Candle{ Time: klineItem.Candles[i].Time.UTC().Format(common.SimpleTimeFormatWithTimezone), Low: klineItem.Candles[i].Low, High: klineItem.Candles[i].High, Open: klineItem.Candles[i].Open, Close: klineItem.Candles[i].Close, Volume: klineItem.Candles[i].Volume, IsPartial: klineItem.Candles[i].ValidationIssues == kline.PartialCandle, }) } if r.Sync && !r.UseDb { _, err = kline.StoreInDatabase(klineItem, r.Force) if err != nil { if errors.Is(err, exchangeDB.ErrNoExchangeFound) { return nil, errors.New("exchange was not found in database, you can seed existing data or insert a new exchange via the dbseed") } return nil, err } } return &resp, nil } func fillMissingCandlesWithStoredTrades(startTime, endTime time.Time, klineItem *kline.Item) (*kline.Item, error) { candleTimes := make([]time.Time, len(klineItem.Candles)) for i := range klineItem.Candles { candleTimes[i] = klineItem.Candles[i].Time } ranges, err := timeperiods.FindTimeRangesContainingData(startTime, endTime, klineItem.Interval.Duration(), candleTimes) if err != nil { return nil, err } var response kline.Item for i := range ranges { if ranges[i].HasDataInRange { continue } var tradeCandles *kline.Item trades, err := trade.GetTradesInRange( klineItem.Exchange, klineItem.Asset.String(), klineItem.Pair.Base.String(), klineItem.Pair.Quote.String(), ranges[i].StartOfRange, ranges[i].EndOfRange, ) if err != nil { return klineItem, err } if len(trades) == 0 { continue } tradeCandles, err = trade.ConvertTradesToCandles(klineItem.Interval, trades...) if err != nil { return klineItem, err } if len(tradeCandles.Candles) == 0 { continue } response.Candles = append(response.Candles, tradeCandles.Candles...) for i := range response.Candles { log.Infof(log.GRPCSys, "Filled requested OHLCV data for %v %v %v interval at %v with trade data", klineItem.Exchange, klineItem.Pair.String(), klineItem.Asset, response.Candles[i].Time.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), ) } } return &response, nil } // GCTScriptStatus returns a slice of current running scripts that includes next run time and uuid func (s *RPCServer) GCTScriptStatus(_ context.Context, _ *gctrpc.GCTScriptStatusRequest) (*gctrpc.GCTScriptStatusResponse, error) { if !s.gctScriptManager.IsRunning() { return &gctrpc.GCTScriptStatusResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } if gctscript.VMSCount.Len() < 1 { return &gctrpc.GCTScriptStatusResponse{Status: "no scripts running"}, nil } resp := &gctrpc.GCTScriptStatusResponse{ Status: fmt.Sprintf("%v of %v virtual machines running", gctscript.VMSCount.Len(), s.gctScriptManager.GetMaxVirtualMachines()), } gctscript.AllVMSync.Range(func(_, v interface{}) bool { vm, ok := v.(*gctscript.VM) if !ok { log.Errorf(log.GRPCSys, "%v", common.GetTypeAssertError("*gctscript.VM", v)) return false } resp.Scripts = append(resp.Scripts, &gctrpc.GCTScript{ Uuid: vm.ID.String(), Name: vm.ShortName(), NextRun: vm.NextRun.String(), }) return true }) return resp, nil } // GCTScriptQuery queries a running script and returns script running information func (s *RPCServer) GCTScriptQuery(_ context.Context, r *gctrpc.GCTScriptQueryRequest) (*gctrpc.GCTScriptQueryResponse, error) { if !s.gctScriptManager.IsRunning() { return &gctrpc.GCTScriptQueryResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } UUID, err := uuid.FromString(r.Script.Uuid) if err != nil { //nolint:nilerr // error is returned in the GCTScriptQueryResponse return &gctrpc.GCTScriptQueryResponse{Status: MsgStatusError, Data: err.Error()}, nil } v, f := gctscript.AllVMSync.Load(UUID) if !f { return &gctrpc.GCTScriptQueryResponse{Status: MsgStatusError, Data: "UUID not found"}, nil } vm, ok := v.(*gctscript.VM) if !ok { return nil, common.GetTypeAssertError("*gctscript.VM", v) } resp := &gctrpc.GCTScriptQueryResponse{ Status: MsgStatusOK, Script: &gctrpc.GCTScript{ Name: vm.ShortName(), Uuid: vm.ID.String(), Path: vm.Path, NextRun: vm.NextRun.String(), }, } data, err := vm.Read() if err != nil { return nil, err } resp.Data = string(data) return resp, nil } // GCTScriptExecute execute a script func (s *RPCServer) GCTScriptExecute(_ context.Context, r *gctrpc.GCTScriptExecuteRequest) (*gctrpc.GenericResponse, error) { if !s.gctScriptManager.IsRunning() { return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } if r.Script.Path == "" { r.Script.Path = gctscript.ScriptPath } gctVM := s.gctScriptManager.New() if gctVM == nil { return &gctrpc.GenericResponse{Status: MsgStatusError, Data: "unable to create VM instance"}, nil } script := filepath.Join(r.Script.Path, r.Script.Name) if err := gctVM.Load(script); err != nil { return &gctrpc.GenericResponse{ //nolint:nilerr // error is returned in the generic response Status: MsgStatusError, Data: err.Error(), }, nil } go gctVM.CompileAndRun() return &gctrpc.GenericResponse{ Status: MsgStatusOK, Data: gctVM.ShortName() + " (" + gctVM.ID.String() + ") executed", }, nil } // GCTScriptStop terminate a running script func (s *RPCServer) GCTScriptStop(_ context.Context, r *gctrpc.GCTScriptStopRequest) (*gctrpc.GenericResponse, error) { if !s.gctScriptManager.IsRunning() { return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } UUID, err := uuid.FromString(r.Script.Uuid) if err != nil { return &gctrpc.GenericResponse{Status: MsgStatusError, Data: err.Error()}, nil //nolint:nilerr // error is returned in the generic response } v, f := gctscript.AllVMSync.Load(UUID) if !f { return &gctrpc.GenericResponse{Status: MsgStatusError, Data: "no running script found"}, nil } vm, ok := v.(*gctscript.VM) if !ok { return nil, common.GetTypeAssertError("*gctscript.VM", v) } err = vm.Shutdown() status := " terminated" if err != nil { status = " " + err.Error() } return &gctrpc.GenericResponse{Status: MsgStatusOK, Data: vm.ID.String() + status}, nil } // GCTScriptUpload upload a new script to ScriptPath func (s *RPCServer) GCTScriptUpload(_ context.Context, r *gctrpc.GCTScriptUploadRequest) (*gctrpc.GenericResponse, error) { if !s.gctScriptManager.IsRunning() { return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } fPath := filepath.Join(gctscript.ScriptPath, r.ScriptName) fPathExits := fPath if filepath.Ext(fPath) == ".zip" { fPathExits = fPathExits[0 : len(fPathExits)-4] } if s, err := os.Stat(fPathExits); !os.IsNotExist(err) { if !r.Overwrite { return nil, fmt.Errorf("%s script found and overwrite set to false", r.ScriptName) } f := filepath.Join(gctscript.ScriptPath, "version_history") err = os.MkdirAll(f, file.DefaultPermissionOctal) if err != nil { return nil, err } timeString := strconv.FormatInt(time.Now().UnixNano(), 10) renamedFile := filepath.Join(f, timeString+"-"+filepath.Base(fPathExits)) if s.IsDir() { err = archive.Zip(fPathExits, renamedFile+".zip") if err != nil { return nil, err } } else { err = file.Move(fPathExits, renamedFile) if err != nil { return nil, err } } } newFile, err := os.Create(fPath) if err != nil { return nil, err } _, err = newFile.Write(r.Data) if err != nil { return nil, err } err = newFile.Close() if err != nil { log.Errorln(log.Global, "Failed to close file handle, archive removal may fail") } if r.Archived { files, errExtract := archive.UnZip(fPath, filepath.Join(gctscript.ScriptPath, r.ScriptName[:len(r.ScriptName)-4])) if errExtract != nil { log.Errorf(log.Global, "Failed to archive zip file %v", errExtract) return &gctrpc.GenericResponse{Status: MsgStatusError, Data: errExtract.Error()}, nil } var failedFiles []string for x := range files { err = s.gctScriptManager.Validate(files[x]) if err != nil { failedFiles = append(failedFiles, files[x]) } } err = os.Remove(fPath) if err != nil { return nil, err } if len(failedFiles) > 0 { err = os.RemoveAll(filepath.Join(gctscript.ScriptPath, r.ScriptName[:len(r.ScriptName)-4])) if err != nil { log.Errorf(log.GCTScriptMgr, "Failed to remove file %v (%v), manual deletion required", filepath.Base(fPath), err) } return &gctrpc.GenericResponse{Status: gctscript.ErrScriptFailedValidation, Data: strings.Join(failedFiles, ", ")}, nil } } else { err = s.gctScriptManager.Validate(fPath) if err != nil { errRemove := os.Remove(fPath) if errRemove != nil { log.Errorf(log.GCTScriptMgr, "Failed to remove file %v, manual deletion required: %v", filepath.Base(fPath), errRemove) } return &gctrpc.GenericResponse{Status: gctscript.ErrScriptFailedValidation, Data: err.Error()}, nil } } return &gctrpc.GenericResponse{ Status: MsgStatusOK, Data: fmt.Sprintf("script %s written", newFile.Name()), }, nil } // GCTScriptReadScript read a script and return contents func (s *RPCServer) GCTScriptReadScript(_ context.Context, r *gctrpc.GCTScriptReadScriptRequest) (*gctrpc.GCTScriptQueryResponse, error) { if !s.gctScriptManager.IsRunning() { return &gctrpc.GCTScriptQueryResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } filename := filepath.Join(gctscript.ScriptPath, r.Script.Name) if !strings.HasPrefix(filename, filepath.Clean(gctscript.ScriptPath)+string(os.PathSeparator)) { return nil, fmt.Errorf("%s: invalid file path", filename) } data, err := os.ReadFile(filename) if err != nil { return nil, err } return &gctrpc.GCTScriptQueryResponse{ Status: MsgStatusOK, Script: &gctrpc.GCTScript{ Name: filepath.Base(filename), Path: filepath.Dir(filename), }, Data: string(data), }, nil } // GCTScriptListAll lists all scripts inside the default script path func (s *RPCServer) GCTScriptListAll(context.Context, *gctrpc.GCTScriptListAllRequest) (*gctrpc.GCTScriptStatusResponse, error) { if !s.gctScriptManager.IsRunning() { return &gctrpc.GCTScriptStatusResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } resp := &gctrpc.GCTScriptStatusResponse{} err := filepath.Walk(gctscript.ScriptPath, func(path string, _ os.FileInfo, err error) error { if err != nil { return err } if filepath.Ext(path) == common.GctExt { resp.Scripts = append(resp.Scripts, &gctrpc.GCTScript{ Name: path, }) } return nil }) if err != nil { return nil, err } return resp, nil } // GCTScriptStopAll stops all running scripts func (s *RPCServer) GCTScriptStopAll(context.Context, *gctrpc.GCTScriptStopAllRequest) (*gctrpc.GenericResponse, error) { if !s.gctScriptManager.IsRunning() { return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } err := s.gctScriptManager.ShutdownAll() if err != nil { return &gctrpc.GenericResponse{Status: "error", Data: err.Error()}, nil //nolint:nilerr // error is returned in the generic response } return &gctrpc.GenericResponse{ Status: MsgStatusOK, Data: "all running scripts have been stopped", }, nil } // GCTScriptAutoLoadToggle adds or removes an entry to the autoload list func (s *RPCServer) GCTScriptAutoLoadToggle(_ context.Context, r *gctrpc.GCTScriptAutoLoadRequest) (*gctrpc.GenericResponse, error) { if !s.gctScriptManager.IsRunning() { return &gctrpc.GenericResponse{Status: gctscript.ErrScriptingDisabled.Error()}, nil } if r.Status { err := s.gctScriptManager.Autoload(r.Script, true) if err != nil { //nolint:nilerr // error is returned in the generic response return &gctrpc.GenericResponse{Status: "error", Data: err.Error()}, nil } return &gctrpc.GenericResponse{Status: "success", Data: "script " + r.Script + " removed from autoload list"}, nil } err := s.gctScriptManager.Autoload(r.Script, false) if err != nil { return &gctrpc.GenericResponse{Status: "error", Data: err.Error()}, nil //nolint:nilerr // error is returned in the generic response } return &gctrpc.GenericResponse{Status: "success", Data: "script " + r.Script + " added to autoload list"}, nil } // SetExchangeAsset enables or disables an exchanges asset type func (s *RPCServer) SetExchangeAsset(_ context.Context, r *gctrpc.SetExchangeAssetRequest) (*gctrpc.GenericResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } exchCfg, err := s.Config.GetExchangeConfig(r.Exchange) if err != nil { return nil, err } base := exch.GetBase() if base == nil { return nil, errExchangeBaseNotFound } if r.Asset == "" { return nil, errors.New("asset type must be specified") } a, err := asset.New(r.Asset) if err != nil { return nil, err } err = base.CurrencyPairs.SetAssetEnabled(a, r.Enable) if err != nil { return nil, err } err = exchCfg.CurrencyPairs.SetAssetEnabled(a, r.Enable) if err != nil { return nil, err } if base.IsWebsocketEnabled() && base.Websocket.IsConnected() { err = exch.FlushWebsocketChannels() if err != nil { return nil, err } } return &gctrpc.GenericResponse{Status: MsgStatusSuccess}, nil } // SetAllExchangePairs enables or disables an exchanges pairs func (s *RPCServer) SetAllExchangePairs(_ context.Context, r *gctrpc.SetExchangeAllPairsRequest) (*gctrpc.GenericResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } exchCfg, err := s.Config.GetExchangeConfig(r.Exchange) if err != nil { return nil, err } base := exch.GetBase() if base == nil { return nil, errExchangeBaseNotFound } assets := base.CurrencyPairs.GetAssetTypes(false) if r.Enable { for i := range assets { var pairs currency.Pairs pairs, err = base.CurrencyPairs.GetPairs(assets[i], false) if err != nil { return nil, err } err = exchCfg.CurrencyPairs.StorePairs(assets[i], pairs, true) if err != nil { return nil, err } err = base.CurrencyPairs.StorePairs(assets[i], pairs, true) if err != nil { return nil, err } } } else { for i := range assets { err = exchCfg.CurrencyPairs.StorePairs(assets[i], nil, true) if err != nil { return nil, err } err = base.CurrencyPairs.StorePairs(assets[i], nil, true) if err != nil { return nil, err } } } if exch.IsWebsocketEnabled() && base.Websocket.IsConnected() { err = exch.FlushWebsocketChannels() if err != nil { return nil, err } } return &gctrpc.GenericResponse{Status: MsgStatusSuccess}, nil } // UpdateExchangeSupportedPairs forces an update of the supported pairs which // will update the available pairs list and remove any assets that are disabled // by the exchange func (s *RPCServer) UpdateExchangeSupportedPairs(ctx context.Context, r *gctrpc.UpdateExchangeSupportedPairsRequest) (*gctrpc.GenericResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } base := exch.GetBase() // nolint:ifshort,nolintlint // false positive and triggers only on Windows if base == nil { return nil, errExchangeBaseNotFound } if !base.GetEnabledFeatures().AutoPairUpdates { return nil, errors.New("cannot auto pair update for exchange, a manual update is needed") } err = exch.UpdateTradablePairs(ctx, false) if err != nil { return nil, err } if exch.IsWebsocketEnabled() { err = exch.FlushWebsocketChannels() if err != nil { return nil, err } } return &gctrpc.GenericResponse{Status: MsgStatusSuccess}, nil } // GetExchangeAssets returns the supported asset types func (s *RPCServer) GetExchangeAssets(_ context.Context, r *gctrpc.GetExchangeAssetsRequest) (*gctrpc.GetExchangeAssetsResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } return &gctrpc.GetExchangeAssetsResponse{ Assets: exch.GetAssetTypes(false).JoinToString(","), }, nil } // WebsocketGetInfo returns websocket connection information func (s *RPCServer) WebsocketGetInfo(_ context.Context, r *gctrpc.WebsocketGetInfoRequest) (*gctrpc.WebsocketGetInfoResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } w, err := exch.GetWebsocket() if err != nil { return nil, err } return &gctrpc.WebsocketGetInfoResponse{ Exchange: exch.GetName(), Supported: exch.SupportsWebsocket(), Enabled: exch.IsWebsocketEnabled(), Authenticated: w.CanUseAuthenticatedEndpoints(), RunningUrl: w.GetWebsocketURL(), ProxyAddress: w.GetProxyAddress(), }, nil } // WebsocketSetEnabled enables or disables the websocket client func (s *RPCServer) WebsocketSetEnabled(_ context.Context, r *gctrpc.WebsocketSetEnabledRequest) (*gctrpc.GenericResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } w, err := exch.GetWebsocket() if err != nil { return nil, fmt.Errorf("websocket not supported for exchange %s", r.Exchange) } exchCfg, err := s.Config.GetExchangeConfig(r.Exchange) if err != nil { return nil, err } if r.Enable { err = w.Enable() if err != nil { return nil, err } exchCfg.Features.Enabled.Websocket = true return &gctrpc.GenericResponse{Status: MsgStatusSuccess, Data: "websocket enabled"}, nil } err = w.Disable() if err != nil { return nil, err } exchCfg.Features.Enabled.Websocket = false return &gctrpc.GenericResponse{Status: MsgStatusSuccess, Data: "websocket disabled"}, nil } // WebsocketGetSubscriptions returns websocket subscription analysis func (s *RPCServer) WebsocketGetSubscriptions(_ context.Context, r *gctrpc.WebsocketGetSubscriptionsRequest) (*gctrpc.WebsocketGetSubscriptionsResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } w, err := exch.GetWebsocket() if err != nil { return nil, fmt.Errorf("websocket not supported for exchange %s", r.Exchange) } payload := new(gctrpc.WebsocketGetSubscriptionsResponse) payload.Exchange = exch.GetName() subs := w.GetSubscriptions() for i := range subs { params, err := json.Marshal(subs[i].Params) if err != nil { return nil, err } payload.Subscriptions = append(payload.Subscriptions, &gctrpc.WebsocketSubscription{ Channel: subs[i].Channel, Pairs: subs[i].Pairs.Join(), Asset: subs[i].Asset.String(), Params: string(params), }) } return payload, nil } // WebsocketSetProxy sets client websocket connection proxy func (s *RPCServer) WebsocketSetProxy(_ context.Context, r *gctrpc.WebsocketSetProxyRequest) (*gctrpc.GenericResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } w, err := exch.GetWebsocket() if err != nil { return nil, fmt.Errorf("websocket not supported for exchange %s", r.Exchange) } err = w.SetProxyAddress(r.Proxy) if err != nil { return nil, err } return &gctrpc.GenericResponse{ Status: MsgStatusSuccess, Data: fmt.Sprintf("new proxy has been set [%s] for %s websocket connection", r.Exchange, r.Proxy), }, nil } // WebsocketSetURL sets exchange websocket client connection URL func (s *RPCServer) WebsocketSetURL(_ context.Context, r *gctrpc.WebsocketSetURLRequest) (*gctrpc.GenericResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } w, err := exch.GetWebsocket() if err != nil { return nil, fmt.Errorf("websocket not supported for exchange %s", r.Exchange) } err = w.SetWebsocketURL(r.Url, false, true) if err != nil { return nil, err } return &gctrpc.GenericResponse{ Status: MsgStatusSuccess, Data: fmt.Sprintf("new URL has been set [%s] for %s websocket connection", r.Exchange, r.Url), }, nil } // GetSavedTrades returns trades from the database func (s *RPCServer) GetSavedTrades(_ context.Context, r *gctrpc.GetSavedTradesRequest) (*gctrpc.SavedTradesResponse, error) { if r.End == "" || r.Start == "" || r.Exchange == "" || r.Pair == nil || r.AssetType == "" || r.Pair.String() == "" { return nil, errInvalidArguments } a, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, a, p) if err != nil { return nil, err } start, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.Start) if err != nil { return nil, fmt.Errorf("%w cannot parse start time %v", errInvalidTimes, err) } end, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.End) if err != nil { return nil, fmt.Errorf("%w cannot parse end time %v", errInvalidTimes, err) } err = common.StartEndTimeCheck(start, end) if err != nil { return nil, err } var trades []trade.Data trades, err = trade.GetTradesInRange(r.Exchange, r.AssetType, r.Pair.Base, r.Pair.Quote, start, end) if err != nil { return nil, err } resp := &gctrpc.SavedTradesResponse{ ExchangeName: r.Exchange, Asset: r.AssetType, Pair: r.Pair, } for i := range trades { resp.Trades = append(resp.Trades, &gctrpc.SavedTrades{ Price: trades[i].Price, Amount: trades[i].Amount, Side: trades[i].Side.String(), Timestamp: trades[i].Timestamp.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), TradeId: trades[i].TID, }) } if len(resp.Trades) == 0 { return nil, fmt.Errorf("request for %v %v trade data between %v and %v and returned no results", r.Exchange, r.AssetType, r.Start, r.End) } return resp, nil } // ConvertTradesToCandles converts trades to candles using the interval requested // returns the data too for extra fun scrutiny func (s *RPCServer) ConvertTradesToCandles(_ context.Context, r *gctrpc.ConvertTradesToCandlesRequest) (*gctrpc.GetHistoricCandlesResponse, error) { if r.End == "" || r.Start == "" || r.Exchange == "" || r.Pair == nil || r.AssetType == "" || r.Pair.String() == "" || r.TimeInterval == 0 { return nil, errInvalidArguments } start, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.Start) if err != nil { return nil, fmt.Errorf("%w cannot parse start time %v", errInvalidTimes, err) } end, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.End) if err != nil { return nil, fmt.Errorf("%w cannot parse end time %v", errInvalidTimes, err) } err = common.StartEndTimeCheck(start, end) if err != nil { return nil, err } a, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, a, p) if err != nil { return nil, err } trades, err := trade.GetTradesInRange(r.Exchange, r.AssetType, r.Pair.Base, r.Pair.Quote, start, end) if err != nil { return nil, err } if len(trades) == 0 { return nil, errNoTrades } interval := kline.Interval(r.TimeInterval) klineItem, err := trade.ConvertTradesToCandles(interval, trades...) if err != nil { return nil, err } if len(klineItem.Candles) == 0 { return nil, errors.New("no candles generated from trades") } resp := &gctrpc.GetHistoricCandlesResponse{ Exchange: r.Exchange, Pair: r.Pair, Start: r.Start, End: r.End, Interval: interval.String(), } for i := range klineItem.Candles { resp.Candle = append(resp.Candle, &gctrpc.Candle{ Time: klineItem.Candles[i].Time.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), Low: klineItem.Candles[i].Low, High: klineItem.Candles[i].High, Open: klineItem.Candles[i].Open, Close: klineItem.Candles[i].Close, Volume: klineItem.Candles[i].Volume, IsPartial: klineItem.Candles[i].ValidationIssues == kline.PartialCandle, }) } if r.Sync { _, err = kline.StoreInDatabase(klineItem, r.Force) if err != nil { return nil, err } } return resp, nil } // FindMissingSavedCandleIntervals is used to help determine what candle data is missing func (s *RPCServer) FindMissingSavedCandleIntervals(_ context.Context, r *gctrpc.FindMissingCandlePeriodsRequest) (*gctrpc.FindMissingIntervalsResponse, error) { if r.End == "" || r.Start == "" || r.ExchangeName == "" || r.Pair == nil || r.AssetType == "" || r.Pair.String() == "" || r.Interval <= 0 { return nil, errInvalidArguments } a, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.ExchangeName) if err != nil { return nil, err } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.ExchangeName, exch, a, p) if err != nil { return nil, err } start, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.Start) if err != nil { return nil, fmt.Errorf("%w cannot parse start time %v", errInvalidTimes, err) } end, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.End) if err != nil { return nil, fmt.Errorf("%w cannot parse end time %v", errInvalidTimes, err) } err = common.StartEndTimeCheck(start, end) if err != nil { return nil, err } klineItem, err := kline.LoadFromDatabase( r.ExchangeName, p, a, kline.Interval(r.Interval), start, end, ) if err != nil { return nil, err } resp := &gctrpc.FindMissingIntervalsResponse{ ExchangeName: r.ExchangeName, AssetType: r.AssetType, Pair: r.Pair, MissingPeriods: []string{}, } candleTimes := make([]time.Time, len(klineItem.Candles)) for i := range klineItem.Candles { candleTimes[i] = klineItem.Candles[i].Time } var ranges []timeperiods.TimeRange ranges, err = timeperiods.FindTimeRangesContainingData(start, end, klineItem.Interval.Duration(), candleTimes) if err != nil { return nil, err } foundCount := 0 for i := range ranges { if !ranges[i].HasDataInRange { resp.MissingPeriods = append(resp.MissingPeriods, ranges[i].StartOfRange.UTC().Format(common.SimpleTimeFormatWithTimezone)+ " - "+ ranges[i].EndOfRange.UTC().Format(common.SimpleTimeFormatWithTimezone)) } else { foundCount++ } } if len(resp.MissingPeriods) == 0 { resp.Status = fmt.Sprintf("no missing candles found between %v and %v", r.Start, r.End, ) } else { resp.Status = fmt.Sprintf("Found %v candles. Missing %v candles in requested timeframe starting %v ending %v", foundCount, len(resp.MissingPeriods), start.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), end.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone)) } return resp, nil } // FindMissingSavedTradeIntervals is used to help determine what trade data is missing func (s *RPCServer) FindMissingSavedTradeIntervals(_ context.Context, r *gctrpc.FindMissingTradePeriodsRequest) (*gctrpc.FindMissingIntervalsResponse, error) { if r.End == "" || r.Start == "" || r.ExchangeName == "" || r.Pair == nil || r.AssetType == "" || r.Pair.String() == "" { return nil, errInvalidArguments } a, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.ExchangeName) if err != nil { return nil, err } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.ExchangeName, exch, a, p) if err != nil { return nil, err } start, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.Start) if err != nil { return nil, fmt.Errorf("%w cannot parse start time %v", errInvalidTimes, err) } end, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.End) if err != nil { return nil, fmt.Errorf("%w cannot parse end time %v", errInvalidTimes, err) } err = common.StartEndTimeCheck(start, end) if err != nil { return nil, err } start = start.Truncate(time.Hour) end = end.Truncate(time.Hour) intervalMap := make(map[int64]bool) iterationTime := start for iterationTime.Before(end) { intervalMap[iterationTime.Unix()] = false iterationTime = iterationTime.Add(time.Hour) } var trades []trade.Data trades, err = trade.GetTradesInRange( r.ExchangeName, r.AssetType, r.Pair.Base, r.Pair.Quote, start, end, ) if err != nil { return nil, err } resp := &gctrpc.FindMissingIntervalsResponse{ ExchangeName: r.ExchangeName, AssetType: r.AssetType, Pair: r.Pair, MissingPeriods: []string{}, } tradeTimes := make([]time.Time, len(trades)) for i := range trades { tradeTimes[i] = trades[i].Timestamp } var ranges []timeperiods.TimeRange ranges, err = timeperiods.FindTimeRangesContainingData(start, end, time.Hour, tradeTimes) if err != nil { return nil, err } foundCount := 0 for i := range ranges { if !ranges[i].HasDataInRange { resp.MissingPeriods = append(resp.MissingPeriods, ranges[i].StartOfRange.UTC().Format(common.SimpleTimeFormatWithTimezone)+ " - "+ ranges[i].EndOfRange.UTC().Format(common.SimpleTimeFormatWithTimezone)) } else { foundCount++ } } if len(resp.MissingPeriods) == 0 { resp.Status = fmt.Sprintf("no missing periods found between %v and %v", r.Start, r.End, ) } else { resp.Status = fmt.Sprintf("Found %v periods. Missing %v periods between %v and %v", foundCount, len(resp.MissingPeriods), start.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), end.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone)) } return resp, nil } // SetExchangeTradeProcessing allows the setting of exchange trade processing func (s *RPCServer) SetExchangeTradeProcessing(_ context.Context, r *gctrpc.SetExchangeTradeProcessingRequest) (*gctrpc.GenericResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } b := exch.GetBase() b.SetSaveTradeDataStatus(r.Status) return &gctrpc.GenericResponse{ Status: "success", }, nil } // GetHistoricTrades returns trades between a set of dates func (s *RPCServer) GetHistoricTrades(r *gctrpc.GetSavedTradesRequest, stream gctrpc.GoCryptoTraderService_GetHistoricTradesServer) error { if r.Exchange == "" || r.Pair == nil || r.AssetType == "" || r.Pair.String() == "" { return errInvalidArguments } a, err := asset.New(r.AssetType) if err != nil { return err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return err } cp := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, a, cp) if err != nil { return err } var trades []trade.Data start, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.Start) if err != nil { return fmt.Errorf("%w cannot parse start time %v", errInvalidTimes, err) } end, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.End) if err != nil { return fmt.Errorf("%w cannot parse end time %v", errInvalidTimes, err) } err = common.StartEndTimeCheck(start, end) if err != nil { return err } resp := &gctrpc.SavedTradesResponse{ ExchangeName: r.Exchange, Asset: r.AssetType, Pair: r.Pair, } for iterateStartTime := start; iterateStartTime.Before(end); iterateStartTime = iterateStartTime.Add(time.Hour) { iterateEndTime := iterateStartTime.Add(time.Hour) trades, err = exch.GetHistoricTrades(stream.Context(), cp, a, iterateStartTime, iterateEndTime) if err != nil { return err } if len(trades) == 0 { continue } grpcTrades := &gctrpc.SavedTradesResponse{ ExchangeName: r.Exchange, Asset: r.AssetType, Pair: r.Pair, } for i := range trades { tradeTS := trades[i].Timestamp.In(time.UTC) if tradeTS.After(end) { break } grpcTrades.Trades = append(grpcTrades.Trades, &gctrpc.SavedTrades{ Price: trades[i].Price, Amount: trades[i].Amount, Side: trades[i].Side.String(), Timestamp: tradeTS.Format(common.SimpleTimeFormatWithTimezone), TradeId: trades[i].TID, }) } err = stream.Send(grpcTrades) if err != nil { return err } } return stream.Send(resp) } // GetRecentTrades returns trades func (s *RPCServer) GetRecentTrades(ctx context.Context, r *gctrpc.GetSavedTradesRequest) (*gctrpc.SavedTradesResponse, error) { if r.Exchange == "" || r.Pair == nil || r.AssetType == "" || r.Pair.String() == "" { return nil, errInvalidArguments } a, err := asset.New(r.AssetType) if err != nil { return nil, err } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } cp := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, exch, a, cp) if err != nil { return nil, err } var trades []trade.Data trades, err = exch.GetRecentTrades(ctx, cp, a) if err != nil { return nil, err } resp := &gctrpc.SavedTradesResponse{ ExchangeName: r.Exchange, Asset: r.AssetType, Pair: r.Pair, } for i := range trades { resp.Trades = append(resp.Trades, &gctrpc.SavedTrades{ Price: trades[i].Price, Amount: trades[i].Amount, Side: trades[i].Side.String(), Timestamp: trades[i].Timestamp.In(time.UTC).Format(common.SimpleTimeFormatWithTimezone), TradeId: trades[i].TID, }) } if len(resp.Trades) == 0 { return nil, fmt.Errorf("request for %v %v trade data and returned no results", r.Exchange, r.AssetType) } return resp, nil } func checkParams(exchName string, e exchange.IBotExchange, a asset.Item, p currency.Pair) error { if e == nil { return fmt.Errorf("%s %w", exchName, errExchangeNotLoaded) } if !e.IsEnabled() { return fmt.Errorf("%s %w", exchName, errExchangeNotEnabled) } if a.IsValid() { b := e.GetBase() if b == nil { return fmt.Errorf("%s %w", exchName, errExchangeBaseNotFound) } err := b.CurrencyPairs.IsAssetEnabled(a) if err != nil { return err } } if p.IsEmpty() { return nil } enabledPairs, err := e.GetEnabledPairs(a) if err != nil { return err } if enabledPairs.Contains(p, true) { return nil } availablePairs, err := e.GetAvailablePairs(a) if err != nil { return err } if availablePairs.Contains(p, true) { return fmt.Errorf("%v %w", p, errCurrencyNotEnabled) } return fmt.Errorf("%v %w", p, errCurrencyPairInvalid) } func parseMultipleEvents(ret []*withdraw.Response) *gctrpc.WithdrawalEventsByExchangeResponse { v := &gctrpc.WithdrawalEventsByExchangeResponse{} for x := range ret { tempEvent := &gctrpc.WithdrawalEventResponse{ Id: ret[x].ID.String(), Exchange: &gctrpc.WithdrawlExchangeEvent{ Name: ret[x].Exchange.Name, Id: ret[x].Exchange.ID, Status: ret[x].Exchange.Status, }, Request: &gctrpc.WithdrawalRequestEvent{ Currency: ret[x].RequestDetails.Currency.String(), Description: ret[x].RequestDetails.Description, Amount: ret[x].RequestDetails.Amount, Type: int64(ret[x].RequestDetails.Type), }, } tempEvent.CreatedAt = timestamppb.New(ret[x].CreatedAt) if err := tempEvent.CreatedAt.CheckValid(); err != nil { log.Errorf(log.Global, "withdrawal parseMultipleEvents CreatedAt: %s", err) } tempEvent.UpdatedAt = timestamppb.New(ret[x].UpdatedAt) if err := tempEvent.UpdatedAt.CheckValid(); err != nil { log.Errorf(log.Global, "withdrawal parseMultipleEvents UpdatedAt: %s", err) } if ret[x].RequestDetails.Type == withdraw.Crypto { tempEvent.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent) tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{ Address: ret[x].RequestDetails.Crypto.Address, AddressTag: ret[x].RequestDetails.Crypto.AddressTag, Fee: ret[x].RequestDetails.Crypto.FeeAmount, } } else if ret[x].RequestDetails.Type == withdraw.Fiat { if ret[x].RequestDetails.Fiat != (withdraw.FiatRequest{}) { tempEvent.Request.Fiat = new(gctrpc.FiatWithdrawalEvent) tempEvent.Request.Fiat = &gctrpc.FiatWithdrawalEvent{ BankName: ret[x].RequestDetails.Fiat.Bank.BankName, AccountName: ret[x].RequestDetails.Fiat.Bank.AccountName, AccountNumber: ret[x].RequestDetails.Fiat.Bank.AccountNumber, Bsb: ret[x].RequestDetails.Fiat.Bank.BSBNumber, Swift: ret[x].RequestDetails.Fiat.Bank.SWIFTCode, Iban: ret[x].RequestDetails.Fiat.Bank.IBAN, } } } v.Event = append(v.Event, tempEvent) } return v } func parseWithdrawalsHistory(ret []exchange.WithdrawalHistory, exchName string, limit int) *gctrpc.WithdrawalEventsByExchangeResponse { v := &gctrpc.WithdrawalEventsByExchangeResponse{} for x := range ret { if limit > 0 && x >= limit { return v } tempEvent := &gctrpc.WithdrawalEventResponse{ Id: ret[x].TransferID, Exchange: &gctrpc.WithdrawlExchangeEvent{ Name: exchName, Status: ret[x].Status, }, Request: &gctrpc.WithdrawalRequestEvent{ Currency: ret[x].Currency, Description: ret[x].Description, Amount: ret[x].Amount, }, } tempEvent.UpdatedAt = timestamppb.New(ret[x].Timestamp) if err := tempEvent.UpdatedAt.CheckValid(); err != nil { log.Errorf(log.Global, "withdrawal parseWithdrawalsHistory UpdatedAt: %s", err) } tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{ Address: ret[x].CryptoToAddress, Fee: ret[x].Fee, TxId: ret[x].CryptoTxID, } v.Event = append(v.Event, tempEvent) } return v } func parseSingleEvents(ret *withdraw.Response) *gctrpc.WithdrawalEventsByExchangeResponse { tempEvent := &gctrpc.WithdrawalEventResponse{ Id: ret.ID.String(), Exchange: &gctrpc.WithdrawlExchangeEvent{ Name: ret.Exchange.Name, Id: ret.Exchange.Name, Status: ret.Exchange.Status, }, Request: &gctrpc.WithdrawalRequestEvent{ Currency: ret.RequestDetails.Currency.String(), Description: ret.RequestDetails.Description, Amount: ret.RequestDetails.Amount, Type: int64(ret.RequestDetails.Type), }, } tempEvent.CreatedAt = timestamppb.New(ret.CreatedAt) if err := tempEvent.CreatedAt.CheckValid(); err != nil { log.Errorf(log.Global, "withdrawal parseSingleEvents CreatedAt %s", err) } tempEvent.UpdatedAt = timestamppb.New(ret.UpdatedAt) if err := tempEvent.UpdatedAt.CheckValid(); err != nil { log.Errorf(log.Global, "withdrawal parseSingleEvents UpdatedAt: %s", err) } if ret.RequestDetails.Type == withdraw.Crypto { tempEvent.Request.Crypto = new(gctrpc.CryptoWithdrawalEvent) tempEvent.Request.Crypto = &gctrpc.CryptoWithdrawalEvent{ Address: ret.RequestDetails.Crypto.Address, AddressTag: ret.RequestDetails.Crypto.AddressTag, Fee: ret.RequestDetails.Crypto.FeeAmount, } } else if ret.RequestDetails.Type == withdraw.Fiat { if ret.RequestDetails.Fiat != (withdraw.FiatRequest{}) { tempEvent.Request.Fiat = new(gctrpc.FiatWithdrawalEvent) tempEvent.Request.Fiat = &gctrpc.FiatWithdrawalEvent{ BankName: ret.RequestDetails.Fiat.Bank.BankName, AccountName: ret.RequestDetails.Fiat.Bank.AccountName, AccountNumber: ret.RequestDetails.Fiat.Bank.AccountNumber, Bsb: ret.RequestDetails.Fiat.Bank.BSBNumber, Swift: ret.RequestDetails.Fiat.Bank.SWIFTCode, Iban: ret.RequestDetails.Fiat.Bank.IBAN, } } } return &gctrpc.WithdrawalEventsByExchangeResponse{ Event: []*gctrpc.WithdrawalEventResponse{tempEvent}, } } // UpsertDataHistoryJob adds or updates a data history job for the data history manager // It will upsert the entry in the database and allow for the processing of the job func (s *RPCServer) UpsertDataHistoryJob(_ context.Context, r *gctrpc.UpsertDataHistoryJobRequest) (*gctrpc.UpsertDataHistoryJobResponse, error) { if r == nil { return nil, errNilRequestData } a, err := asset.New(r.Asset) if err != nil { return nil, err } e, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } p := currency.NewPairWithDelimiter(r.Pair.Base, r.Pair.Quote, r.Pair.Delimiter) err = checkParams(r.Exchange, e, a, p) if err != nil { return nil, err } start, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.StartDate) if err != nil { return nil, fmt.Errorf("%w cannot parse start time %v", errInvalidTimes, err) } end, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.EndDate) if err != nil { return nil, fmt.Errorf("%w cannot parse end time %v", errInvalidTimes, err) } err = common.StartEndTimeCheck(start, end) if err != nil { return nil, err } job := DataHistoryJob{ Nickname: r.Nickname, Exchange: r.Exchange, Asset: a, Pair: p, StartDate: start, EndDate: end, Interval: kline.Interval(r.Interval), RunBatchLimit: r.BatchSize, RequestSizeLimit: r.RequestSizeLimit, DataType: dataHistoryDataType(r.DataType), MaxRetryAttempts: r.MaxRetryAttempts, Status: dataHistoryStatusActive, OverwriteExistingData: r.OverwriteExistingData, ConversionInterval: kline.Interval(r.ConversionInterval), DecimalPlaceComparison: r.DecimalPlaceComparison, SecondaryExchangeSource: r.SecondaryExchangeName, IssueTolerancePercentage: r.IssueTolerancePercentage, ReplaceOnIssue: r.ReplaceOnIssue, PrerequisiteJobNickname: r.PrerequisiteJobNickname, } err = s.dataHistoryManager.UpsertJob(&job, r.InsertOnly) if err != nil { return nil, err } result, err := s.dataHistoryManager.GetByNickname(r.Nickname, false) if err != nil { return nil, fmt.Errorf("%s %w", r.Nickname, err) } return &gctrpc.UpsertDataHistoryJobResponse{ JobId: result.ID.String(), Message: "successfully upserted job: " + result.Nickname, }, nil } // GetDataHistoryJobDetails returns a data history job's details // can request all data history results with r.FullDetails func (s *RPCServer) GetDataHistoryJobDetails(_ context.Context, r *gctrpc.GetDataHistoryJobDetailsRequest) (*gctrpc.DataHistoryJob, error) { if r == nil { return nil, errNilRequestData } if r.Id == "" && r.Nickname == "" { return nil, errNicknameIDUnset } if r.Nickname != "" && r.Id != "" { return nil, errOnlyNicknameOrID } var ( result *DataHistoryJob err error jobResults []*gctrpc.DataHistoryJobResult ) if r.Id != "" { var id uuid.UUID id, err = uuid.FromString(r.Id) if err != nil { return nil, fmt.Errorf("%s %w", r.Id, err) } result, err = s.dataHistoryManager.GetByID(id) if err != nil { return nil, fmt.Errorf("%s %w", r.Id, err) } } else { result, err = s.dataHistoryManager.GetByNickname(r.Nickname, r.FullDetails) if err != nil { return nil, fmt.Errorf("%s %w", r.Nickname, err) } if r.FullDetails { for _, v := range result.Results { for i := range v { jobResults = append(jobResults, &gctrpc.DataHistoryJobResult{ StartDate: v[i].IntervalStartDate.Format(time.DateTime), EndDate: v[i].IntervalEndDate.Format(time.DateTime), HasData: v[i].Status == dataHistoryStatusComplete, Message: v[i].Result, RunDate: v[i].Date.Format(time.DateTime), }) } } } } return &gctrpc.DataHistoryJob{ Id: result.ID.String(), Nickname: result.Nickname, Exchange: result.Exchange, Asset: result.Asset.String(), Pair: &gctrpc.CurrencyPair{ Delimiter: result.Pair.Delimiter, Base: result.Pair.Base.String(), Quote: result.Pair.Quote.String(), }, StartDate: result.StartDate.Format(time.DateTime), EndDate: result.EndDate.Format(time.DateTime), Interval: int64(result.Interval.Duration()), RequestSizeLimit: result.RequestSizeLimit, MaxRetryAttempts: result.MaxRetryAttempts, BatchSize: result.RunBatchLimit, Status: result.Status.String(), DataType: result.DataType.String(), ConversionInterval: int64(result.ConversionInterval.Duration()), OverwriteExistingData: result.OverwriteExistingData, PrerequisiteJobNickname: result.PrerequisiteJobNickname, DecimalPlaceComparison: result.DecimalPlaceComparison, SecondaryExchangeName: result.SecondaryExchangeSource, IssueTolerancePercentage: result.IssueTolerancePercentage, ReplaceOnIssue: result.ReplaceOnIssue, JobResults: jobResults, }, nil } // GetActiveDataHistoryJobs returns any active data history job details func (s *RPCServer) GetActiveDataHistoryJobs(_ context.Context, _ *gctrpc.GetInfoRequest) (*gctrpc.DataHistoryJobs, error) { jobs, err := s.dataHistoryManager.GetActiveJobs() if err != nil { return nil, err } response := make([]*gctrpc.DataHistoryJob, len(jobs)) for i := range jobs { response[i] = &gctrpc.DataHistoryJob{ Id: jobs[i].ID.String(), Nickname: jobs[i].Nickname, Exchange: jobs[i].Exchange, Asset: jobs[i].Asset.String(), Pair: &gctrpc.CurrencyPair{ Delimiter: jobs[i].Pair.Delimiter, Base: jobs[i].Pair.Base.String(), Quote: jobs[i].Pair.Quote.String(), }, StartDate: jobs[i].StartDate.Format(time.DateTime), EndDate: jobs[i].EndDate.Format(time.DateTime), Interval: int64(jobs[i].Interval.Duration()), RequestSizeLimit: jobs[i].RequestSizeLimit, MaxRetryAttempts: jobs[i].MaxRetryAttempts, BatchSize: jobs[i].RunBatchLimit, Status: jobs[i].Status.String(), DataType: jobs[i].DataType.String(), ConversionInterval: int64(jobs[i].ConversionInterval.Duration()), OverwriteExistingData: jobs[i].OverwriteExistingData, PrerequisiteJobNickname: jobs[i].PrerequisiteJobNickname, DecimalPlaceComparison: jobs[i].DecimalPlaceComparison, SecondaryExchangeName: jobs[i].SecondaryExchangeSource, IssueTolerancePercentage: jobs[i].IssueTolerancePercentage, ReplaceOnIssue: jobs[i].ReplaceOnIssue, } } return &gctrpc.DataHistoryJobs{Results: response}, nil } // GetDataHistoryJobsBetween returns all jobs created between supplied dates func (s *RPCServer) GetDataHistoryJobsBetween(_ context.Context, r *gctrpc.GetDataHistoryJobsBetweenRequest) (*gctrpc.DataHistoryJobs, error) { if r == nil { return nil, errNilRequestData } start, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.StartDate) if err != nil { return nil, fmt.Errorf("%w cannot parse start time %v", errInvalidTimes, err) } end, err := time.Parse(common.SimpleTimeFormatWithTimezone, r.EndDate) if err != nil { return nil, fmt.Errorf("%w cannot parse end time %v", errInvalidTimes, err) } err = common.StartEndTimeCheck(start.Local(), end) if err != nil { return nil, err } jobs, err := s.dataHistoryManager.GetAllJobStatusBetween(start, end) if err != nil { return nil, err } respJobs := make([]*gctrpc.DataHistoryJob, len(jobs)) for i := range jobs { respJobs[i] = &gctrpc.DataHistoryJob{ Id: jobs[i].ID.String(), Nickname: jobs[i].Nickname, Exchange: jobs[i].Exchange, Asset: jobs[i].Asset.String(), Pair: &gctrpc.CurrencyPair{ Delimiter: jobs[i].Pair.Delimiter, Base: jobs[i].Pair.Base.String(), Quote: jobs[i].Pair.Quote.String(), }, StartDate: jobs[i].StartDate.Format(time.DateTime), EndDate: jobs[i].EndDate.Format(time.DateTime), Interval: int64(jobs[i].Interval.Duration()), RequestSizeLimit: jobs[i].RequestSizeLimit, MaxRetryAttempts: jobs[i].MaxRetryAttempts, BatchSize: jobs[i].RunBatchLimit, Status: jobs[i].Status.String(), DataType: jobs[i].DataType.String(), ConversionInterval: int64(jobs[i].ConversionInterval.Duration()), OverwriteExistingData: jobs[i].OverwriteExistingData, PrerequisiteJobNickname: jobs[i].PrerequisiteJobNickname, DecimalPlaceComparison: jobs[i].DecimalPlaceComparison, SecondaryExchangeName: jobs[i].SecondaryExchangeSource, IssueTolerancePercentage: jobs[i].IssueTolerancePercentage, ReplaceOnIssue: jobs[i].ReplaceOnIssue, } } return &gctrpc.DataHistoryJobs{ Results: respJobs, }, nil } // GetDataHistoryJobSummary provides a general look at how a data history job is going with the "resultSummaries" property func (s *RPCServer) GetDataHistoryJobSummary(_ context.Context, r *gctrpc.GetDataHistoryJobDetailsRequest) (*gctrpc.DataHistoryJob, error) { if r == nil { return nil, errNilRequestData } if r.Nickname == "" { return nil, fmt.Errorf("get job summary %w", errNicknameUnset) } job, err := s.dataHistoryManager.GenerateJobSummary(r.Nickname) if err != nil { return nil, err } return &gctrpc.DataHistoryJob{ Nickname: job.Nickname, Exchange: job.Exchange, Asset: job.Asset.String(), Pair: &gctrpc.CurrencyPair{ Delimiter: job.Pair.Delimiter, Base: job.Pair.Base.String(), Quote: job.Pair.Quote.String(), }, StartDate: job.StartDate.Format(time.DateTime), EndDate: job.EndDate.Format(time.DateTime), Interval: int64(job.Interval.Duration()), Status: job.Status.String(), DataType: job.DataType.String(), ConversionInterval: int64(job.ConversionInterval.Duration()), OverwriteExistingData: job.OverwriteExistingData, PrerequisiteJobNickname: job.PrerequisiteJobNickname, ResultSummaries: job.ResultRanges, }, nil } // unixTimestamp returns given time in either unix seconds or unix nanoseconds, depending // on the remoteControl/gRPC/timeInNanoSeconds boolean configuration. func (s *RPCServer) unixTimestamp(x time.Time) int64 { if s.Config.RemoteControl.GRPC.TimeInNanoSeconds { return x.UnixNano() } return x.Unix() } // SetDataHistoryJobStatus sets a data history job's status func (s *RPCServer) SetDataHistoryJobStatus(_ context.Context, r *gctrpc.SetDataHistoryJobStatusRequest) (*gctrpc.GenericResponse, error) { if r == nil { return nil, errNilRequestData } if r.Nickname == "" && r.Id == "" { return nil, errNicknameIDUnset } if r.Nickname != "" && r.Id != "" { return nil, errOnlyNicknameOrID } status := "success" err := s.dataHistoryManager.SetJobStatus(r.Nickname, r.Id, dataHistoryStatus(r.Status)) if err != nil { log.Errorln(log.GRPCSys, err) status = "failed" } return &gctrpc.GenericResponse{Status: status}, err } // UpdateDataHistoryJobPrerequisite sets or removes a prerequisite job for an existing job // if the prerequisite job is "", then the relationship is removed func (s *RPCServer) UpdateDataHistoryJobPrerequisite(_ context.Context, r *gctrpc.UpdateDataHistoryJobPrerequisiteRequest) (*gctrpc.GenericResponse, error) { if r == nil { return nil, errNilRequestData } if r.Nickname == "" { return nil, errNicknameUnset } status := "success" err := s.dataHistoryManager.SetJobRelationship(r.PrerequisiteJobNickname, r.Nickname) if err != nil { return nil, err } if r.PrerequisiteJobNickname == "" { return &gctrpc.GenericResponse{Status: status, Data: fmt.Sprintf("Removed prerequisite from job '%v'", r.Nickname)}, nil } return &gctrpc.GenericResponse{Status: status, Data: fmt.Sprintf("Set job '%v' prerequisite job to '%v' and set status to paused", r.Nickname, r.PrerequisiteJobNickname)}, nil } // CurrencyStateGetAll returns a full snapshot of currency states, whether they // are able to be withdrawn, deposited or traded on an exchange. func (s *RPCServer) CurrencyStateGetAll(_ context.Context, r *gctrpc.CurrencyStateGetAllRequest) (*gctrpc.CurrencyStateResponse, error) { return s.currencyStateManager.GetAllRPC(r.Exchange) } // CurrencyStateWithdraw determines via RPC if the currency code is operational for // withdrawal from an exchange func (s *RPCServer) CurrencyStateWithdraw(_ context.Context, r *gctrpc.CurrencyStateWithdrawRequest) (*gctrpc.GenericResponse, error) { ai, err := asset.New(r.Asset) if err != nil { return nil, err } return s.currencyStateManager.CanWithdrawRPC(r.Exchange, currency.NewCode(r.Code), ai) } // CurrencyStateDeposit determines via RPC if the currency code is operational for // depositing to an exchange func (s *RPCServer) CurrencyStateDeposit(_ context.Context, r *gctrpc.CurrencyStateDepositRequest) (*gctrpc.GenericResponse, error) { ai, err := asset.New(r.Asset) if err != nil { return nil, err } return s.currencyStateManager.CanDepositRPC(r.Exchange, currency.NewCode(r.Code), ai) } // CurrencyStateTrading determines via RPC if the currency code is operational for trading func (s *RPCServer) CurrencyStateTrading(_ context.Context, r *gctrpc.CurrencyStateTradingRequest) (*gctrpc.GenericResponse, error) { ai, err := asset.New(r.Asset) if err != nil { return nil, err } return s.currencyStateManager.CanTradeRPC(r.Exchange, currency.NewCode(r.Code), ai) } // CurrencyStateTradingPair determines via RPC if the pair is operational for trading func (s *RPCServer) CurrencyStateTradingPair(_ context.Context, r *gctrpc.CurrencyStateTradingPairRequest) (*gctrpc.GenericResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } cp, err := currency.NewPairFromString(r.Pair) if err != nil { return nil, err } ai, err := asset.New(r.Asset) if err != nil { return nil, err } err = checkParams(r.Exchange, exch, ai, cp) if err != nil { return nil, err } err = exch.CanTradePair(cp, ai) if err != nil { return nil, err } return s.currencyStateManager.CanTradePairRPC(r.Exchange, cp, ai) } func (s *RPCServer) buildFuturePosition(position *futures.Position, getFundingPayments, includeFundingRates, includeOrders, includePredictedRate bool) *gctrpc.FuturePosition { response := &gctrpc.FuturePosition{ Exchange: position.Exchange, Asset: position.Asset.String(), Pair: &gctrpc.CurrencyPair{ Delimiter: position.Pair.Delimiter, Base: position.Pair.Base.String(), Quote: position.Pair.Quote.String(), }, Status: position.Status.String(), OpeningDate: position.OpeningDate.Format(common.SimpleTimeFormatWithTimezone), OpeningDirection: position.OpeningDirection.String(), OpeningPrice: position.OpeningPrice.String(), OpeningSize: position.OpeningSize.String(), CurrentDirection: position.LatestDirection.String(), CurrentPrice: position.LatestPrice.String(), CurrentSize: position.LatestSize.String(), UnrealisedPnl: position.UnrealisedPNL.String(), RealisedPnl: position.RealisedPNL.String(), OrderCount: int64(len(position.Orders)), } if getFundingPayments { var sum decimal.Decimal fundingData := &gctrpc.FundingData{} for i := range position.FundingRates.FundingRates { if includeFundingRates { fundingData.Rates = append(fundingData.Rates, &gctrpc.FundingRate{ Date: position.FundingRates.FundingRates[i].Time.Format(common.SimpleTimeFormatWithTimezone), Rate: position.FundingRates.FundingRates[i].Rate.String(), Payment: position.FundingRates.FundingRates[i].Payment.String(), }) } sum = sum.Add(position.FundingRates.FundingRates[i].Payment) } fundingData.PaymentSum = sum.String() response.FundingData = fundingData if includePredictedRate && !position.FundingRates.PredictedUpcomingRate.Time.IsZero() { fundingData.UpcomingRate = &gctrpc.FundingRate{ Date: position.FundingRates.PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone), Rate: position.FundingRates.PredictedUpcomingRate.Rate.String(), } } } if includeOrders { for i := range position.Orders { od := &gctrpc.OrderDetails{ Exchange: position.Orders[i].Exchange, Id: position.Orders[i].OrderID, ClientOrderId: position.Orders[i].ClientOrderID, BaseCurrency: position.Orders[i].Pair.Base.String(), QuoteCurrency: position.Orders[i].Pair.Quote.String(), AssetType: position.Orders[i].AssetType.String(), OrderSide: position.Orders[i].Side.String(), OrderType: position.Orders[i].Type.String(), CreationTime: position.Orders[i].Date.Format(common.SimpleTimeFormatWithTimezone), Status: position.Orders[i].Status.String(), Price: position.Orders[i].Price, Amount: position.Orders[i].Cost, OpenVolume: position.Orders[i].RemainingAmount, Fee: position.Orders[i].Fee, Cost: position.Orders[i].Cost, } if !position.Orders[i].LastUpdated.IsZero() { od.UpdateTime = position.Orders[i].LastUpdated.Format(common.SimpleTimeFormatWithTimezone) } for j := range position.Orders[i].Trades { od.Trades = append(od.Trades, &gctrpc.TradeHistory{ CreationTime: position.Orders[i].Trades[j].Timestamp.Unix(), Id: position.Orders[i].Trades[j].TID, Price: position.Orders[i].Trades[j].Price, Amount: position.Orders[i].Trades[j].Amount, Exchange: position.Orders[i].Trades[j].Exchange, AssetType: position.Orders[i].AssetType.String(), OrderSide: position.Orders[i].Trades[j].Side.String(), Fee: position.Orders[i].Trades[j].Fee, Total: position.Orders[i].Trades[j].Total, }) } response.Orders = append(response.Orders, od) } } return response } // GetManagedPosition returns an open positions from the order manager, no calling any API endpoints to return this information func (s *RPCServer) GetManagedPosition(_ context.Context, r *gctrpc.GetManagedPositionRequest) (*gctrpc.GetManagedPositionsResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetManagedPositionRequest", common.ErrNilPointer) } if err := futures.CheckFundingRatePrerequisites(r.GetFundingPayments, r.IncludePredictedRate, r.GetFundingPayments); err != nil { return nil, err } if r.Pair == nil { return nil, fmt.Errorf("%w request pair", common.ErrNilPointer) } var ( exch exchange.IBotExchange ai asset.Item cp currency.Pair err error ) exch, err = s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsEnabled() { return nil, fmt.Errorf("%w '%v'", errExchangeDisabled, exch.GetName()) } feat := exch.GetSupportedFeatures() if !feat.FuturesCapabilities.OrderManagerPositionTracking { return nil, fmt.Errorf("%w OrderManagerPositionTracking for exchange %v", common.ErrFunctionNotSupported, exch.GetName()) } ai, err = asset.New(r.Asset) if err != nil { return nil, err } if !ai.IsFutures() { return nil, fmt.Errorf("%w '%v'", futures.ErrNotFuturesAsset, ai) } cp, err = currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) if err != nil { return nil, err } err = checkParams(r.Exchange, exch, ai, cp) if err != nil { return nil, err } position, err := s.OrderManager.GetOpenFuturesPosition(r.Exchange, ai, cp) if err != nil { return nil, err } return &gctrpc.GetManagedPositionsResponse{Positions: []*gctrpc.FuturePosition{ s.buildFuturePosition(position, r.GetFundingPayments, r.IncludeFullFundingRates, r.IncludeFullOrderData, r.IncludePredictedRate), }}, nil } // GetAllManagedPositions returns all open positions from the order manager, no calling any API endpoints to return this information func (s *RPCServer) GetAllManagedPositions(_ context.Context, r *gctrpc.GetAllManagedPositionsRequest) (*gctrpc.GetManagedPositionsResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetAllManagedPositionsRequest", common.ErrNilPointer) } if err := futures.CheckFundingRatePrerequisites(r.GetFundingPayments, r.IncludePredictedRate, r.GetFundingPayments); err != nil { return nil, err } positions, err := s.OrderManager.GetAllOpenFuturesPositions() if err != nil { return nil, err } sort.Slice(positions, func(i, j int) bool { return positions[i].OpeningDate.Before(positions[j].OpeningDate) }) response := make([]*gctrpc.FuturePosition, len(positions)) for i := range positions { response[i] = s.buildFuturePosition(&positions[i], r.GetFundingPayments, r.IncludeFullFundingRates, r.IncludeFullOrderData, r.IncludePredictedRate) } return &gctrpc.GetManagedPositionsResponse{Positions: response}, nil } // GetFuturesPositionsSummary returns a summary of futures positions for an exchange asset pair from the API func (s *RPCServer) GetFuturesPositionsSummary(ctx context.Context, r *gctrpc.GetFuturesPositionsSummaryRequest) (*gctrpc.GetFuturesPositionsSummaryResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetFuturesPositionsSummaryRequest", common.ErrNilPointer) } if r.Pair == nil { return nil, currency.ErrCurrencyPairEmpty } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsEnabled() { return nil, fmt.Errorf("%s %w", r.Exchange, errExchangeNotEnabled) } feat := exch.GetSupportedFeatures() if !feat.FuturesCapabilities.Positions { return nil, fmt.Errorf("%w futures position tracking for exchange %v", common.ErrFunctionNotSupported, exch.GetName()) } ai, err := asset.New(r.Asset) if err != nil { return nil, err } if !ai.IsFutures() { return nil, fmt.Errorf("%s %w", ai, futures.ErrNotFuturesAsset) } enabledPairs, err := exch.GetEnabledPairs(ai) if err != nil { return nil, err } cp, err := enabledPairs.DeriveFrom(r.Pair.Base + r.Pair.Quote) if err != nil { return nil, err } var underlying currency.Pair if r.UnderlyingPair != nil { underlying, err = currency.NewPairFromStrings(r.UnderlyingPair.Base, r.UnderlyingPair.Quote) if err != nil { return nil, err } } var stats *futures.PositionSummary stats, err = exch.GetFuturesPositionSummary(ctx, &futures.PositionSummaryRequest{ Asset: ai, Pair: cp, UnderlyingPair: underlying, }) if err != nil { return nil, fmt.Errorf("cannot GetFuturesPositionSummary %w", err) } positionStats := &gctrpc.FuturesPositionStats{} if !stats.MaintenanceMarginRequirement.IsZero() { positionStats.MaintenanceMarginRequirement = stats.MaintenanceMarginRequirement.String() } if !stats.InitialMarginRequirement.IsZero() { positionStats.InitialMarginRequirement = stats.InitialMarginRequirement.String() } if !stats.CollateralUsed.IsZero() { positionStats.CollateralUsed = stats.CollateralUsed.String() } if !stats.MarkPrice.IsZero() { positionStats.MarkPrice = stats.MarkPrice.String() } if !stats.CurrentSize.IsZero() { positionStats.CurrentSize = stats.CurrentSize.String() } if !stats.ContractMultiplier.IsZero() { positionStats.ContractMultiplier = stats.ContractMultiplier.String() } if !stats.ContractSize.IsZero() { positionStats.ContractSize = stats.ContractSize.String() } if !stats.AverageOpenPrice.IsZero() { positionStats.AverageOpenPrice = stats.AverageOpenPrice.String() } if !stats.UnrealisedPNL.IsZero() { positionStats.RecentPnl = stats.UnrealisedPNL.String() } if !stats.MaintenanceMarginFraction.IsZero() { positionStats.MarginFraction = stats.MaintenanceMarginFraction.String() } if !stats.FreeCollateral.IsZero() { positionStats.FreeCollateral = stats.FreeCollateral.String() } if !stats.TotalCollateral.IsZero() { positionStats.TotalCollateral = stats.TotalCollateral.String() } if !stats.EstimatedLiquidationPrice.IsZero() { positionStats.EstimatedLiquidationPrice = stats.EstimatedLiquidationPrice.String() } if !stats.FrozenBalance.IsZero() { positionStats.FrozenBalance = stats.FrozenBalance.String() } if !stats.EquityOfCurrency.IsZero() { positionStats.EquityOfCurrency = stats.EquityOfCurrency.String() } if !stats.AvailableEquity.IsZero() { positionStats.AvailableEquity = stats.AvailableEquity.String() } if !stats.CashBalance.IsZero() { positionStats.CashBalance = stats.CashBalance.String() } if !stats.DiscountEquity.IsZero() { positionStats.DiscountEquity = stats.DiscountEquity.String() } if !stats.EquityUSD.IsZero() { positionStats.EquityUsd = stats.EquityUSD.String() } if !stats.IsolatedEquity.IsZero() { positionStats.IsolatedEquity = stats.IsolatedEquity.String() } if stats.ContractSettlementType != futures.UnsetSettlementType { positionStats.ContractSettlementType = stats.ContractSettlementType.String() } if !stats.IsolatedLiabilities.IsZero() { positionStats.IsolatedLiabilities = stats.IsolatedLiabilities.String() } if !stats.IsolatedUPL.IsZero() { positionStats.IsolatedUpl = stats.IsolatedUPL.String() } if !stats.NotionalLeverage.IsZero() { positionStats.NotionalLeverage = stats.NotionalLeverage.String() } if !stats.TotalEquity.IsZero() { positionStats.TotalEquity = stats.TotalEquity.String() } if !stats.StrategyEquity.IsZero() { positionStats.StrategyEquity = stats.StrategyEquity.String() } return &gctrpc.GetFuturesPositionsSummaryResponse{ Exchange: exch.GetName(), Asset: ai.String(), Pair: &gctrpc.CurrencyPair{ Delimiter: cp.Delimiter, Base: cp.Base.String(), Quote: cp.Quote.String(), }, PositionStats: positionStats, }, nil } // GetFuturesPositionsOrders returns futures position orders from exchange API func (s *RPCServer) GetFuturesPositionsOrders(ctx context.Context, r *gctrpc.GetFuturesPositionsOrdersRequest) (*gctrpc.GetFuturesPositionsOrdersResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetFuturesPositionsOrdersRequest", common.ErrNilPointer) } if r.Pair == nil { return nil, currency.ErrCurrencyPairEmpty } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } feat := exch.GetSupportedFeatures() if !feat.FuturesCapabilities.Positions { return nil, fmt.Errorf("%w futures position tracking for exchange %v", common.ErrFunctionNotSupported, exch.GetName()) } if r.SyncWithOrderManager && !feat.FuturesCapabilities.OrderManagerPositionTracking { return nil, fmt.Errorf("%w OrderManagerPositionTracking", common.ErrFunctionNotSupported) } if !exch.IsEnabled() { return nil, fmt.Errorf("%s %w", r.Exchange, errExchangeNotEnabled) } ai, err := asset.New(r.Asset) if err != nil { return nil, err } if !ai.IsFutures() { return nil, fmt.Errorf("%s %w", ai, futures.ErrNotFuturesAsset) } enabledPairs, err := exch.GetEnabledPairs(ai) if err != nil { return nil, err } cp, err := enabledPairs.DeriveFrom(r.Pair.Base + r.Pair.Quote) if err != nil { return nil, err } var start, end time.Time if r.StartDate != "" { start, err = time.Parse(common.SimpleTimeFormatWithTimezone, r.StartDate) if err != nil { return nil, err } } if r.EndDate != "" { end, err = time.Parse(common.SimpleTimeFormatWithTimezone, r.EndDate) if err != nil { return nil, err } } err = common.StartEndTimeCheck(start, end) if err != nil && !errors.Is(err, common.ErrDateUnset) { return nil, err } positionDetails, err := exch.GetFuturesPositionOrders(ctx, &futures.PositionsRequest{ Asset: ai, Pairs: currency.Pairs{cp}, StartDate: start, EndDate: end, RespectOrderHistoryLimits: r.RespectOrderHistoryLimits, }) if err != nil { return nil, err } response := &gctrpc.GetFuturesPositionsOrdersResponse{} positions := make([]*gctrpc.FuturePosition, len(positionDetails)) var anyOrders bool for i := range positionDetails { details := &gctrpc.FuturePosition{ Exchange: exch.GetName(), Asset: positionDetails[i].Asset.String(), Pair: &gctrpc.CurrencyPair{ Delimiter: positionDetails[i].Pair.Delimiter, Base: positionDetails[i].Pair.Base.String(), Quote: positionDetails[i].Pair.Quote.String(), }, ContractSettlementType: positionDetails[i].ContractSettlementType.String(), Orders: make([]*gctrpc.OrderDetails, len(positionDetails[i].Orders)), } for j := range positionDetails[i].Orders { anyOrders = true details.Orders[j] = &gctrpc.OrderDetails{ Exchange: exch.GetName(), Id: positionDetails[i].Orders[j].OrderID, ClientOrderId: positionDetails[i].Orders[j].ClientOrderID, BaseCurrency: positionDetails[i].Orders[j].Pair.Base.String(), QuoteCurrency: positionDetails[i].Orders[j].Pair.Quote.String(), AssetType: positionDetails[i].Orders[j].AssetType.String(), OrderSide: positionDetails[i].Orders[j].Side.String(), OrderType: positionDetails[i].Orders[j].Type.String(), CreationTime: positionDetails[i].Orders[j].Date.Format(common.SimpleTimeFormatWithTimezone), UpdateTime: positionDetails[i].Orders[j].LastUpdated.Format(common.SimpleTimeFormatWithTimezone), Status: positionDetails[i].Orders[j].Status.String(), Price: positionDetails[i].Orders[j].Price, Amount: positionDetails[i].Orders[j].Amount, OpenVolume: positionDetails[i].Orders[j].RemainingAmount, Fee: positionDetails[i].Orders[j].Fee, Cost: positionDetails[i].Orders[j].Cost, ContractAmount: positionDetails[i].Orders[j].ContractAmount, } } positions[i] = details } if !anyOrders { return &gctrpc.GetFuturesPositionsOrdersResponse{}, nil } response.Positions = positions if r.SyncWithOrderManager { for i := range positionDetails { err = s.OrderManager.processFuturesPositions(exch, &positionDetails[i]) if err != nil { return nil, err } } } return response, nil } // GetFundingRates returns the funding rates for an exchange, asset, pair func (s *RPCServer) GetFundingRates(ctx context.Context, r *gctrpc.GetFundingRatesRequest) (*gctrpc.GetFundingRatesResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetFundingRatesRequest", common.ErrNilPointer) } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } feat := exch.GetSupportedFeatures() if !feat.FuturesCapabilities.FundingRates { return nil, fmt.Errorf("%w FundingRates for exchange %v", common.ErrFunctionNotSupported, exch.GetName()) } a, err := asset.New(r.Asset) if err != nil { return nil, err } if !a.IsFutures() { return nil, fmt.Errorf("%s %w", a, futures.ErrNotFuturesAsset) } start := time.Now().AddDate(0, -1, 0) end := time.Now() if r.StartDate != "" { start, err = time.Parse(common.SimpleTimeFormatWithTimezone, r.StartDate) if err != nil { return nil, err } } if r.EndDate != "" { end, err = time.Parse(common.SimpleTimeFormatWithTimezone, r.EndDate) if err != nil { return nil, err } } err = common.StartEndTimeCheck(start, end) if err != nil && !errors.Is(err, common.ErrDateUnset) { return nil, err } cp, err := exch.MatchSymbolWithAvailablePairs(r.Pair.Base+r.Pair.Quote, a, false) if err != nil { return nil, err } pairs, err := exch.GetEnabledPairs(a) if err != nil { return nil, err } if !pairs.Contains(cp, true) { return nil, fmt.Errorf("%w %v", currency.ErrPairNotEnabled, cp) } funding, err := exch.GetHistoricalFundingRates(ctx, &fundingrate.HistoricalRatesRequest{ Asset: a, Pair: cp, StartDate: start, EndDate: end, IncludePayments: r.IncludePayments, IncludePredictedRate: r.IncludePredicted, RespectHistoryLimits: r.RespectHistoryLimits, PaymentCurrency: currency.NewCode(r.PaymentCurrency), }) if err != nil { return nil, err } var hasPayment bool var response gctrpc.GetFundingRatesResponse fundingData := &gctrpc.FundingData{ Exchange: r.Exchange, Asset: r.Asset, Pair: &gctrpc.CurrencyPair{ Delimiter: funding.Pair.Delimiter, Base: funding.Pair.Base.String(), Quote: funding.Pair.Quote.String(), }, StartDate: start.Format(common.SimpleTimeFormatWithTimezone), EndDate: end.Format(common.SimpleTimeFormatWithTimezone), LatestRate: &gctrpc.FundingRate{ Date: funding.LatestRate.Time.Format(common.SimpleTimeFormatWithTimezone), Rate: funding.LatestRate.Rate.String(), }, } rates := make([]*gctrpc.FundingRate, len(funding.FundingRates)) for j := range funding.FundingRates { rates[j] = &gctrpc.FundingRate{ Rate: funding.FundingRates[j].Rate.String(), Date: funding.FundingRates[j].Time.Format(common.SimpleTimeFormatWithTimezone), } if r.IncludePayments { if !funding.FundingRates[j].Payment.IsZero() { hasPayment = true } rates[j].Payment = funding.FundingRates[j].Payment.String() } } if r.IncludePayments { fundingData.PaymentSum = funding.PaymentSum.String() fundingData.PaymentCurrency = funding.PaymentCurrency.String() if !hasPayment { fundingData.PaymentMessage = "no payments found for payment currency " + funding.PaymentCurrency.String() + " please ensure you have set the correct payment currency in the request" } } if !funding.TimeOfNextRate.IsZero() { fundingData.TimeOfNextRate = funding.TimeOfNextRate.Format(common.SimpleTimeFormatWithTimezone) } fundingData.Rates = rates if r.IncludePredicted { fundingData.UpcomingRate = &gctrpc.FundingRate{ Date: funding.PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone), Rate: funding.PredictedUpcomingRate.Rate.String(), } } response.Rates = fundingData return &response, nil } // GetLatestFundingRate returns the latest funding rate for an exchange, asset, pair func (s *RPCServer) GetLatestFundingRate(ctx context.Context, r *gctrpc.GetLatestFundingRateRequest) (*gctrpc.GetLatestFundingRateResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetLatestFundingRateRequest", common.ErrNilPointer) } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } a, err := asset.New(r.Asset) if err != nil { return nil, err } if !a.IsFutures() { return nil, fmt.Errorf("%s %w", a, futures.ErrNotFuturesAsset) } cp, err := exch.MatchSymbolWithAvailablePairs(r.Pair.Base+r.Pair.Quote, a, false) if err != nil { return nil, err } pairs, err := exch.GetEnabledPairs(a) if err != nil { return nil, err } if !pairs.Contains(cp, true) { return nil, fmt.Errorf("%w %v", currency.ErrPairNotEnabled, cp) } fundingRates, err := exch.GetLatestFundingRates(ctx, &fundingrate.LatestRateRequest{ Asset: a, Pair: cp, IncludePredictedRate: r.IncludePredicted, }) if err != nil { return nil, err } if len(fundingRates) != 1 { return nil, fmt.Errorf("expected 1 funding rate, received %v", len(fundingRates)) } var response gctrpc.GetLatestFundingRateResponse fundingData := &gctrpc.FundingData{ Exchange: r.Exchange, Asset: r.Asset, Pair: &gctrpc.CurrencyPair{ Delimiter: fundingRates[0].Pair.Delimiter, Base: fundingRates[0].Pair.Base.String(), Quote: fundingRates[0].Pair.Quote.String(), }, LatestRate: &gctrpc.FundingRate{ Date: fundingRates[0].LatestRate.Time.Format(common.SimpleTimeFormatWithTimezone), Rate: fundingRates[0].LatestRate.Rate.String(), }, } if !fundingRates[0].TimeOfNextRate.IsZero() { fundingData.TimeOfNextRate = fundingRates[0].TimeOfNextRate.Format(common.SimpleTimeFormatWithTimezone) } if r.IncludePredicted { fundingData.UpcomingRate = &gctrpc.FundingRate{ Date: fundingRates[0].PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone), Rate: fundingRates[0].PredictedUpcomingRate.Rate.String(), } } response.Rate = fundingData return &response, nil } // GetCollateral returns the total collateral for an exchange's asset // as exchanges can scale collateral and represent it in a singular currency, // a user can opt to include a breakdown by currency func (s *RPCServer) GetCollateral(ctx context.Context, r *gctrpc.GetCollateralRequest) (*gctrpc.GetCollateralResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } feat := exch.GetSupportedFeatures() if !feat.FuturesCapabilities.Collateral { return nil, fmt.Errorf("%w Get Collateral for exchange %v", common.ErrFunctionNotSupported, exch.GetName()) } a, err := asset.New(r.Asset) if err != nil { return nil, err } err = checkParams(r.Exchange, exch, a, currency.EMPTYPAIR) if err != nil { return nil, err } if !a.IsFutures() { return nil, fmt.Errorf("%s %w", a, futures.ErrNotFuturesAsset) } ai, err := exch.GetCachedAccountInfo(ctx, a) if err != nil { return nil, err } creds, err := exch.GetCredentials(ctx) if err != nil { return nil, err } subAccounts := make([]string, len(ai.Accounts)) var acc *account.SubAccount for i := range ai.Accounts { subAccounts[i] = ai.Accounts[i].ID if ai.Accounts[i].ID == "main" && creds.SubAccount == "" { acc = &ai.Accounts[i] break } if strings.EqualFold(creds.SubAccount, ai.Accounts[i].ID) { acc = &ai.Accounts[i] break } } if acc == nil { return nil, fmt.Errorf("%w for %s %s and stored credentials - available subaccounts: %s", errNoAccountInformation, exch.GetName(), creds.SubAccount, strings.Join(subAccounts, ",")) } var spotPairs currency.Pairs if r.CalculateOffline { spotPairs, err = exch.GetAvailablePairs(asset.Spot) if err != nil { return nil, fmt.Errorf("GetCollateral offline calculation error via GetAvailablePairs %s %s", exch.GetName(), err) } } calculators := make([]futures.CollateralCalculator, 0, len(acc.Currencies)) for i := range acc.Currencies { total := decimal.NewFromFloat(acc.Currencies[i].Total) free := decimal.NewFromFloat(acc.Currencies[i].AvailableWithoutBorrow) cal := futures.CollateralCalculator{ CalculateOffline: r.CalculateOffline, CollateralCurrency: acc.Currencies[i].Currency, Asset: a, FreeCollateral: free, LockedCollateral: total.Sub(free), } if r.CalculateOffline && !acc.Currencies[i].Currency.Equal(currency.USD) { var tick *ticker.Price tickerCurr := currency.NewPair(acc.Currencies[i].Currency, currency.USD) if !spotPairs.Contains(tickerCurr, true) { // cannot price currency to calculate collateral continue } tick, err = exch.GetCachedTicker(tickerCurr, asset.Spot) if err != nil { log.Errorf(log.GRPCSys, "GetCollateral offline calculation error via GetCachedTicker %s %s", exch.GetName(), err) continue } if tick.Last == 0 { continue } cal.USDPrice = decimal.NewFromFloat(tick.Last) } calculators = append(calculators, cal) } calc := &futures.TotalCollateralCalculator{ CollateralAssets: calculators, CalculateOffline: r.CalculateOffline, FetchPositions: true, } c, err := exch.CalculateTotalCollateral(ctx, calc) if err != nil { return nil, err } collateralDisplayCurrency := " " + c.CollateralCurrency.String() result := &gctrpc.GetCollateralResponse{ SubAccount: creds.SubAccount, CollateralCurrency: c.CollateralCurrency.String(), AvailableCollateral: c.AvailableCollateral.String() + collateralDisplayCurrency, UsedCollateral: c.UsedCollateral.String() + collateralDisplayCurrency, } if !c.CollateralContributedByPositiveSpotBalances.IsZero() { result.CollateralContributedByPositiveSpotBalances = c.CollateralContributedByPositiveSpotBalances.String() + collateralDisplayCurrency } if !c.TotalValueOfPositiveSpotBalances.IsZero() { result.TotalValueOfPositiveSpotBalances = c.TotalValueOfPositiveSpotBalances.String() + collateralDisplayCurrency } if !c.AvailableMaintenanceCollateral.IsZero() { result.MaintenanceCollateral = c.AvailableMaintenanceCollateral.String() + collateralDisplayCurrency } if !c.UnrealisedPNL.IsZero() { result.UnrealisedPnl = c.UnrealisedPNL.String() } if c.UsedBreakdown != nil { result.UsedBreakdown = &gctrpc.CollateralUsedBreakdown{} if !c.UsedBreakdown.LockedInStakes.IsZero() { result.UsedBreakdown.LockedInStakes = c.UsedBreakdown.LockedInStakes.String() + collateralDisplayCurrency } if !c.UsedBreakdown.LockedInNFTBids.IsZero() { result.UsedBreakdown.LockedInNftBids = c.UsedBreakdown.LockedInNFTBids.String() + collateralDisplayCurrency } if !c.UsedBreakdown.LockedInFeeVoucher.IsZero() { result.UsedBreakdown.LockedInFeeVoucher = c.UsedBreakdown.LockedInFeeVoucher.String() + collateralDisplayCurrency } if !c.UsedBreakdown.LockedInSpotMarginFundingOffers.IsZero() { result.UsedBreakdown.LockedInSpotMarginFundingOffers = c.UsedBreakdown.LockedInSpotMarginFundingOffers.String() + collateralDisplayCurrency } if !c.UsedBreakdown.LockedInSpotOrders.IsZero() { result.UsedBreakdown.LockedInSpotOrders = c.UsedBreakdown.LockedInSpotOrders.String() + collateralDisplayCurrency } if !c.UsedBreakdown.LockedAsCollateral.IsZero() { result.UsedBreakdown.LockedAsCollateral = c.UsedBreakdown.LockedAsCollateral.String() + collateralDisplayCurrency } if !c.UsedBreakdown.UsedInPositions.IsZero() { result.UsedBreakdown.UsedInFutures = c.UsedBreakdown.UsedInPositions.String() + collateralDisplayCurrency } if !c.UsedBreakdown.UsedInSpotMarginBorrows.IsZero() { result.UsedBreakdown.UsedInSpotMargin = c.UsedBreakdown.UsedInSpotMarginBorrows.String() + collateralDisplayCurrency } } if r.IncludeBreakdown { for i := range c.BreakdownOfPositions { result.PositionBreakdown = append(result.PositionBreakdown, &gctrpc.CollateralByPosition{ Currency: c.BreakdownOfPositions[i].PositionCurrency.String(), Size: c.BreakdownOfPositions[i].Size.String(), OpenOrderSize: c.BreakdownOfPositions[i].OpenOrderSize.String(), PositionSize: c.BreakdownOfPositions[i].PositionSize.String(), MarkPrice: c.BreakdownOfPositions[i].MarkPrice.String() + collateralDisplayCurrency, RequiredMargin: c.BreakdownOfPositions[i].RequiredMargin.String(), TotalCollateralUsed: c.BreakdownOfPositions[i].CollateralUsed.String() + collateralDisplayCurrency, }) } for i := range c.BreakdownByCurrency { if c.BreakdownByCurrency[i].TotalFunds.IsZero() && !r.IncludeZeroValues { continue } originalDisplayCurrency := " " + c.BreakdownByCurrency[i].Currency.String() cb := &gctrpc.CollateralForCurrency{ Currency: c.BreakdownByCurrency[i].Currency.String(), ExcludedFromCollateral: c.BreakdownByCurrency[i].SkipContribution, TotalFunds: c.BreakdownByCurrency[i].TotalFunds.String() + originalDisplayCurrency, AvailableForUseAsCollateral: c.BreakdownByCurrency[i].AvailableForUseAsCollateral.String() + originalDisplayCurrency, ApproxFairMarketValue: c.BreakdownByCurrency[i].FairMarketValue.String() + collateralDisplayCurrency, Weighting: c.BreakdownByCurrency[i].Weighting.String(), CollateralContribution: c.BreakdownByCurrency[i].CollateralContribution.String() + collateralDisplayCurrency, ScaledToCurrency: c.BreakdownByCurrency[i].ScaledCurrency.String(), } if !c.BreakdownByCurrency[i].AdditionalCollateralUsed.IsZero() { cb.AdditionalCollateralUsed = c.BreakdownByCurrency[i].AdditionalCollateralUsed.String() + collateralDisplayCurrency } if !c.BreakdownByCurrency[i].ScaledUsed.IsZero() { cb.FundsInUse = c.BreakdownByCurrency[i].ScaledUsed.String() + collateralDisplayCurrency } if !c.BreakdownByCurrency[i].UnrealisedPNL.IsZero() { cb.UnrealisedPnl = c.BreakdownByCurrency[i].UnrealisedPNL.String() + collateralDisplayCurrency } if c.BreakdownByCurrency[i].ScaledUsedBreakdown != nil { breakDownDisplayCurrency := collateralDisplayCurrency if c.BreakdownByCurrency[i].Weighting.IsZero() && c.BreakdownByCurrency[i].FairMarketValue.IsZero() { // cannot determine value, show in like currency instead breakDownDisplayCurrency = originalDisplayCurrency } cb.UsedBreakdown = &gctrpc.CollateralUsedBreakdown{} if !c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedInStakes.IsZero() { cb.UsedBreakdown.LockedInStakes = c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedInStakes.String() + breakDownDisplayCurrency } if !c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedInNFTBids.IsZero() { cb.UsedBreakdown.LockedInNftBids = c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedInNFTBids.String() + breakDownDisplayCurrency } if !c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedInFeeVoucher.IsZero() { cb.UsedBreakdown.LockedInFeeVoucher = c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedInFeeVoucher.String() + breakDownDisplayCurrency } if !c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedInSpotMarginFundingOffers.IsZero() { cb.UsedBreakdown.LockedInSpotMarginFundingOffers = c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedInSpotMarginFundingOffers.String() + breakDownDisplayCurrency } if !c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedInSpotOrders.IsZero() { cb.UsedBreakdown.LockedInSpotOrders = c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedInSpotOrders.String() + breakDownDisplayCurrency } if !c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedAsCollateral.IsZero() { cb.UsedBreakdown.LockedAsCollateral = c.BreakdownByCurrency[i].ScaledUsedBreakdown.LockedAsCollateral.String() + breakDownDisplayCurrency } if !c.BreakdownByCurrency[i].ScaledUsedBreakdown.UsedInPositions.IsZero() { cb.UsedBreakdown.UsedInFutures = c.BreakdownByCurrency[i].ScaledUsedBreakdown.UsedInPositions.String() + breakDownDisplayCurrency } if !c.BreakdownByCurrency[i].ScaledUsedBreakdown.UsedInSpotMarginBorrows.IsZero() { cb.UsedBreakdown.UsedInSpotMargin = c.BreakdownByCurrency[i].ScaledUsedBreakdown.UsedInSpotMarginBorrows.String() + breakDownDisplayCurrency } } if c.BreakdownByCurrency[i].Error != nil { cb.Error = c.BreakdownByCurrency[i].Error.Error() } result.CurrencyBreakdown = append(result.CurrencyBreakdown, cb) } } return result, nil } // Shutdown terminates bot session externally func (s *RPCServer) Shutdown(_ context.Context, _ *gctrpc.ShutdownRequest) (*gctrpc.ShutdownResponse, error) { if !s.Engine.Settings.EnableGRPCShutdown { return nil, errShutdownNotAllowed } if s.Engine.GRPCShutdownSignal == nil { return nil, errGRPCShutdownSignalIsNil } s.Engine.GRPCShutdownSignal <- struct{}{} s.Engine.GRPCShutdownSignal = nil return &gctrpc.ShutdownResponse{}, nil } // GetTechnicalAnalysis using the requested technical analysis method will // return a set(s) of signals for price action analysis. func (s *RPCServer) GetTechnicalAnalysis(ctx context.Context, r *gctrpc.GetTechnicalAnalysisRequest) (*gctrpc.GetTechnicalAnalysisResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } as, err := asset.New(r.AssetType) if err != nil { return nil, err } pair, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) if err != nil { return nil, err } klines, err := exch.GetHistoricCandlesExtended(ctx, pair, as, kline.Interval(r.Interval), r.Start.AsTime(), r.End.AsTime()) if err != nil { return nil, err } signals := make(map[string]*gctrpc.ListOfSignals) switch strings.ToUpper(r.AlgorithmType) { case "TWAP": var price float64 price, err = klines.GetTWAP() if err != nil { return nil, err } signals["TWAP"] = &gctrpc.ListOfSignals{Signals: []float64{price}} case "VWAP": var prices []float64 prices, err = klines.GetVWAPs() if err != nil { return nil, err } signals["VWAP"] = &gctrpc.ListOfSignals{Signals: prices} case "ATR": var prices []float64 prices, err = klines.GetAverageTrueRange(r.Period) if err != nil { return nil, err } signals["ATR"] = &gctrpc.ListOfSignals{Signals: prices} case "BBANDS": var bollinger *kline.Bollinger bollinger, err = klines.GetBollingerBands(r.Period, r.StandardDeviationUp, r.StandardDeviationDown, indicators.MaType(r.MovingAverageType)) //nolint:gosec // TODO: Make var types consistent if err != nil { return nil, err } signals["UPPER"] = &gctrpc.ListOfSignals{Signals: bollinger.Upper} signals["MIDDLE"] = &gctrpc.ListOfSignals{Signals: bollinger.Middle} signals["LOWER"] = &gctrpc.ListOfSignals{Signals: bollinger.Lower} case "COCO": otherExch := exch if r.OtherExchange != "" { otherExch, err = s.GetExchangeByName(r.OtherExchange) if err != nil { return nil, err } } otherAs := as if r.OtherAssetType != "" { otherAs, err = asset.New(r.OtherAssetType) if err != nil { return nil, err } } if r.OtherPair.String() == "" { return nil, errors.New("other pair is empty, to compare this must be specified") } var otherPair currency.Pair otherPair, err = currency.NewPairFromStrings(r.OtherPair.Base, r.OtherPair.Quote) if err != nil { return nil, err } var otherKlines *kline.Item otherKlines, err = otherExch.GetHistoricCandlesExtended(ctx, otherPair, otherAs, kline.Interval(r.Interval), r.Start.AsTime(), r.End.AsTime()) if err != nil { return nil, err } var correlation []float64 correlation, err = klines.GetCorrelationCoefficient(otherKlines, r.Period) if err != nil { return nil, err } signals["COCO"] = &gctrpc.ListOfSignals{Signals: correlation} case "SMA": var prices []float64 prices, err = klines.GetSimpleMovingAverageOnClose(r.Period) if err != nil { return nil, err } signals["SMA"] = &gctrpc.ListOfSignals{Signals: prices} case "EMA": var prices []float64 prices, err = klines.GetExponentialMovingAverageOnClose(r.Period) if err != nil { return nil, err } signals["EMA"] = &gctrpc.ListOfSignals{Signals: prices} case "MACD": var macd *kline.MACD macd, err = klines.GetMovingAverageConvergenceDivergenceOnClose(r.FastPeriod, r.SlowPeriod, r.Period) if err != nil { return nil, err } signals["MACD"] = &gctrpc.ListOfSignals{Signals: macd.Results} signals["SIGNAL"] = &gctrpc.ListOfSignals{Signals: macd.SignalVals} signals["HISTOGRAM"] = &gctrpc.ListOfSignals{Signals: macd.Histogram} case "MFI": var prices []float64 prices, err = klines.GetMoneyFlowIndex(r.Period) if err != nil { return nil, err } signals["MFI"] = &gctrpc.ListOfSignals{Signals: prices} case "OBV": var prices []float64 prices, err = klines.GetOnBalanceVolume() if err != nil { return nil, err } signals["OBV"] = &gctrpc.ListOfSignals{Signals: prices} case "RSI": var prices []float64 prices, err = klines.GetRelativeStrengthIndexOnClose(r.Period) if err != nil { return nil, err } signals["RSI"] = &gctrpc.ListOfSignals{Signals: prices} default: return nil, fmt.Errorf("%w '%s'", errInvalidStrategy, r.AlgorithmType) } return &gctrpc.GetTechnicalAnalysisResponse{Signals: signals}, nil } // GetMarginRatesHistory returns the margin lending or borrow rates for an exchange, asset, currency along with many customisable options func (s *RPCServer) GetMarginRatesHistory(ctx context.Context, r *gctrpc.GetMarginRatesHistoryRequest) (*gctrpc.GetMarginRatesHistoryResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetMarginRatesHistoryRequest", common.ErrNilPointer) } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } a, err := asset.New(r.Asset) if err != nil { return nil, err } err = checkParams(r.Exchange, exch, a, currency.EMPTYPAIR) if err != nil { return nil, err } c := currency.NewCode(r.Currency) pairs, err := exch.GetEnabledPairs(a) if err != nil { return nil, err } if !pairs.ContainsCurrency(c) { return nil, fmt.Errorf("%w '%v' in enabled pairs", currency.ErrCurrencyNotFound, r.Currency) } start := time.Now().AddDate(0, -1, 0) end := time.Now() if r.StartDate != "" { start, err = time.Parse(common.SimpleTimeFormatWithTimezone, r.StartDate) if err != nil { return nil, err } } if r.EndDate != "" { end, err = time.Parse(common.SimpleTimeFormatWithTimezone, r.EndDate) if err != nil { return nil, err } } err = common.StartEndTimeCheck(start, end) if err != nil { return nil, err } req := &margin.RateHistoryRequest{ Exchange: exch.GetName(), Asset: a, Currency: c, StartDate: start, EndDate: end, GetPredictedRate: r.GetPredictedRate, GetLendingPayments: r.GetLendingPayments, GetBorrowRates: r.GetBorrowRates, GetBorrowCosts: r.GetBorrowCosts, CalculateOffline: r.CalculateOffline, } if req.CalculateOffline { if r.TakerFeeRate == "" { return nil, fmt.Errorf("%w for offline calculations", common.ErrCannotCalculateOffline) } req.TakeFeeRate, err = decimal.NewFromString(r.TakerFeeRate) if err != nil { return nil, err } if req.TakeFeeRate.LessThanOrEqual(decimal.Zero) { return nil, fmt.Errorf("%w for offline calculations", common.ErrCannotCalculateOffline) } if len(r.Rates) == 0 { return nil, fmt.Errorf("%w for offline calculations", common.ErrCannotCalculateOffline) } req.Rates = make([]margin.Rate, len(r.Rates)) for i := range r.Rates { var offlineRate margin.Rate offlineRate.Time, err = time.Parse(common.SimpleTimeFormatWithTimezone, r.Rates[i].Time) if err != nil { return nil, err } offlineRate.HourlyRate, err = decimal.NewFromString(r.Rates[i].HourlyRate) if err != nil { return nil, err } if r.Rates[i].BorrowCost != nil { offlineRate.BorrowCost.Size, err = decimal.NewFromString(r.Rates[i].BorrowCost.Size) if err != nil { return nil, err } } if r.Rates[i].LendingPayment != nil { offlineRate.LendingPayment.Size, err = decimal.NewFromString(r.Rates[i].LendingPayment.Size) if err != nil { return nil, err } } req.Rates[i] = offlineRate } } lendingResp, err := exch.GetMarginRatesHistory(ctx, req) if err != nil { return nil, err } if len(lendingResp.Rates) == 0 { return nil, order.ErrNoRates } resp := &gctrpc.GetMarginRatesHistoryResponse{ LatestRate: &gctrpc.MarginRate{ Time: lendingResp.Rates[len(lendingResp.Rates)-1].Time.Format(common.SimpleTimeFormatWithTimezone), HourlyRate: lendingResp.Rates[len(lendingResp.Rates)-1].HourlyRate.String(), YearlyRate: lendingResp.Rates[len(lendingResp.Rates)-1].YearlyRate.String(), MarketBorrowSize: lendingResp.Rates[len(lendingResp.Rates)-1].MarketBorrowSize.String(), }, TotalRates: int64(len(lendingResp.Rates)), } if r.GetBorrowRates { resp.LatestRate.HourlyBorrowRate = lendingResp.Rates[len(lendingResp.Rates)-1].HourlyBorrowRate.String() resp.LatestRate.YearlyBorrowRate = lendingResp.Rates[len(lendingResp.Rates)-1].YearlyBorrowRate.String() } if r.GetBorrowRates || r.GetLendingPayments { resp.TakerFeeRate = lendingResp.TakerFeeRate.String() } if r.GetLendingPayments { resp.SumLendingPayments = lendingResp.SumLendingPayments.String() resp.AvgLendingSize = lendingResp.AverageLendingSize.String() } if r.GetBorrowCosts { resp.SumBorrowCosts = lendingResp.SumBorrowCosts.String() resp.AvgBorrowSize = lendingResp.AverageBorrowSize.String() } if r.GetPredictedRate { resp.PredictedRate = &gctrpc.MarginRate{ Time: lendingResp.PredictedRate.Time.Format(common.SimpleTimeFormatWithTimezone), HourlyRate: lendingResp.PredictedRate.HourlyRate.String(), YearlyRate: lendingResp.PredictedRate.YearlyRate.String(), } if r.GetBorrowRates { resp.PredictedRate.HourlyBorrowRate = lendingResp.PredictedRate.HourlyBorrowRate.String() resp.PredictedRate.YearlyBorrowRate = lendingResp.PredictedRate.YearlyBorrowRate.String() } } if r.IncludeAllRates { resp.Rates = make([]*gctrpc.MarginRate, len(lendingResp.Rates)) for i := range lendingResp.Rates { rate := &gctrpc.MarginRate{ Time: lendingResp.Rates[i].Time.Format(common.SimpleTimeFormatWithTimezone), HourlyRate: lendingResp.Rates[i].HourlyRate.String(), YearlyRate: lendingResp.Rates[i].YearlyRate.String(), MarketBorrowSize: lendingResp.Rates[i].MarketBorrowSize.String(), } if r.GetBorrowRates { rate.HourlyBorrowRate = lendingResp.Rates[i].HourlyBorrowRate.String() rate.YearlyBorrowRate = lendingResp.Rates[i].YearlyBorrowRate.String() } if r.GetBorrowCosts { rate.BorrowCost = &gctrpc.BorrowCost{ Cost: lendingResp.Rates[i].BorrowCost.Cost.String(), Size: lendingResp.Rates[i].BorrowCost.Size.String(), } } if r.GetLendingPayments { rate.LendingPayment = &gctrpc.LendingPayment{ Payment: lendingResp.Rates[i].LendingPayment.Payment.String(), Size: lendingResp.Rates[i].LendingPayment.Size.String(), } } resp.Rates[i] = rate } } return resp, nil } // GetOrderbookMovement using the requested amount simulates a buy or sell and // returns the nominal/impact percentages and costings. func (s *RPCServer) GetOrderbookMovement(_ context.Context, r *gctrpc.GetOrderbookMovementRequest) (*gctrpc.GetOrderbookMovementResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } as, err := asset.New(r.Asset) if err != nil { return nil, err } pair, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) if err != nil { return nil, err } if pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } err = checkParams(r.Exchange, exch, as, pair) if err != nil { return nil, err } depth, err := orderbook.GetDepth(exch.GetName(), pair, as) if err != nil { return nil, err } isRest, err := depth.IsRESTSnapshot() if err != nil { return nil, err } updateProtocol := "WEBSOCKET" if isRest { updateProtocol = "REST" } var move *orderbook.Movement var bought, sold, side string if r.Sell { move, err = depth.HitTheBidsFromBest(r.Amount, r.Purchase) bought = pair.Quote.Upper().String() sold = pair.Base.Upper().String() side = order.Bid.String() } else { move, err = depth.LiftTheAsksFromBest(r.Amount, r.Purchase) bought = pair.Base.Upper().String() sold = pair.Quote.Upper().String() side = order.Ask.String() } if err != nil { return nil, err } return &gctrpc.GetOrderbookMovementResponse{ NominalPercentage: move.NominalPercentage, ImpactPercentage: move.ImpactPercentage, SlippageCost: move.SlippageCost, CurrencyBought: bought, Bought: move.Purchased, CurrencySold: sold, Sold: move.Sold, SideAffected: side, UpdateProtocol: updateProtocol, FullOrderbookSideConsumed: move.FullBookSideConsumed, NoSlippageOccurred: move.ImpactPercentage == 0, StartPrice: move.StartPrice, EndPrice: move.EndPrice, AverageOrderCost: move.AverageOrderCost, }, nil } // GetOrderbookAmountByNominal using the requested nominal percentage requirement // returns the amount on orderbook that can fit without exceeding that value. func (s *RPCServer) GetOrderbookAmountByNominal(_ context.Context, r *gctrpc.GetOrderbookAmountByNominalRequest) (*gctrpc.GetOrderbookAmountByNominalResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } as, err := asset.New(r.Asset) if err != nil { return nil, err } pair, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) if err != nil { return nil, err } if pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } err = checkParams(r.Exchange, exch, as, pair) if err != nil { return nil, err } depth, err := orderbook.GetDepth(exch.GetName(), pair, as) if err != nil { return nil, err } isRest, err := depth.IsRESTSnapshot() if err != nil { return nil, err } updateProtocol := "WEBSOCKET" if isRest { updateProtocol = "REST" } var nominal *orderbook.Movement var selling, buying, side string if r.Sell { nominal, err = depth.HitTheBidsByNominalSlippageFromBest(r.NominalPercentage) selling = pair.Upper().Base.String() buying = pair.Upper().Quote.String() side = order.Bid.String() } else { nominal, err = depth.LiftTheAsksByNominalSlippageFromBest(r.NominalPercentage) buying = pair.Upper().Base.String() selling = pair.Upper().Quote.String() side = order.Ask.String() } if err != nil { return nil, err } return &gctrpc.GetOrderbookAmountByNominalResponse{ AmountRequired: nominal.Sold, CurrencySelling: selling, AmountReceived: nominal.Purchased, CurrencyBuying: buying, SideAffected: side, ApproximateNominalSlippagePercentage: nominal.NominalPercentage, UpdateProtocol: updateProtocol, FullOrderbookSideConsumed: nominal.FullBookSideConsumed, StartPrice: nominal.StartPrice, EndPrice: nominal.EndPrice, AverageOrderCost: nominal.AverageOrderCost, }, nil } // GetOrderbookAmountByImpact using the requested impact percentage requirement // determines the amount on orderbook that can fit that will slip the orderbook. func (s *RPCServer) GetOrderbookAmountByImpact(_ context.Context, r *gctrpc.GetOrderbookAmountByImpactRequest) (*gctrpc.GetOrderbookAmountByImpactResponse, error) { exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } as, err := asset.New(r.Asset) if err != nil { return nil, err } pair, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) if err != nil { return nil, err } if pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } err = checkParams(r.Exchange, exch, as, pair) if err != nil { return nil, err } depth, err := orderbook.GetDepth(exch.GetName(), pair, as) if err != nil { return nil, err } isRest, err := depth.IsRESTSnapshot() if err != nil { return nil, err } updateProtocol := "WEBSOCKET" if isRest { updateProtocol = "REST" } var impact *orderbook.Movement var selling, buying, side string if r.Sell { impact, err = depth.HitTheBidsByImpactSlippageFromBest(r.ImpactPercentage) selling = pair.Upper().Base.String() buying = pair.Upper().Quote.String() side = order.Bid.String() } else { impact, err = depth.LiftTheAsksByImpactSlippageFromBest(r.ImpactPercentage) buying = pair.Upper().Base.String() selling = pair.Upper().Quote.String() side = order.Ask.String() } if err != nil { return nil, err } return &gctrpc.GetOrderbookAmountByImpactResponse{ AmountRequired: impact.Sold, CurrencySelling: selling, AmountReceived: impact.Purchased, CurrencyBuying: buying, SideAffected: side, ApproximateImpactSlippagePercentage: impact.ImpactPercentage, UpdateProtocol: updateProtocol, FullOrderbookSideConsumed: impact.FullBookSideConsumed, StartPrice: impact.StartPrice, EndPrice: impact.EndPrice, AverageOrderCost: impact.AverageOrderCost, }, nil } // GetCollateralMode returns the collateral type for the account asset func (s *RPCServer) GetCollateralMode(ctx context.Context, r *gctrpc.GetCollateralModeRequest) (*gctrpc.GetCollateralModeResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetCollateralModeRequest", common.ErrNilPointer) } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } feat := exch.GetSupportedFeatures() if !feat.FuturesCapabilities.CollateralMode { return nil, fmt.Errorf("%w GetCollateralMode for exchange %v", common.ErrFunctionNotSupported, exch.GetName()) } item, err := asset.New(r.Asset) if err != nil { return nil, err } if !exch.IsEnabled() { return nil, fmt.Errorf("%s %w", exch.GetName(), errExchangeNotEnabled) } if !item.IsValid() { return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) } b := exch.GetBase() if b == nil { return nil, fmt.Errorf("%s %w", exch.GetName(), errExchangeBaseNotFound) } err = b.CurrencyPairs.IsAssetEnabled(item) if err != nil { return nil, err } collateralMode, err := exch.GetCollateralMode(ctx, item) if err != nil { return nil, err } return &gctrpc.GetCollateralModeResponse{ Exchange: r.Exchange, Asset: r.Asset, CollateralMode: collateralMode.String(), }, nil } // SetCollateralMode sets the collateral type for the account asset func (s *RPCServer) SetCollateralMode(ctx context.Context, r *gctrpc.SetCollateralModeRequest) (*gctrpc.SetCollateralModeResponse, error) { if r == nil { return nil, fmt.Errorf("%w SetCollateralModeRequest", common.ErrNilPointer) } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsEnabled() { return nil, fmt.Errorf("%s %w", exch.GetName(), errExchangeNotEnabled) } feat := exch.GetSupportedFeatures() if !feat.FuturesCapabilities.CollateralMode { return nil, fmt.Errorf("%w SetCollateralMode for exchange %v", common.ErrFunctionNotSupported, exch.GetName()) } item, err := asset.New(r.Asset) if err != nil { return nil, err } b := exch.GetBase() if b == nil { return nil, fmt.Errorf("%s %w", exch.GetName(), errExchangeBaseNotFound) } err = b.CurrencyPairs.IsAssetEnabled(item) if err != nil { return nil, fmt.Errorf("%v %w", item, err) } cm, err := collateral.StringToMode(r.CollateralMode) if err != nil { return nil, fmt.Errorf("%w %v", order.ErrCollateralInvalid, r.CollateralMode) } err = exch.SetCollateralMode(ctx, item, cm) if err != nil { return nil, err } return &gctrpc.SetCollateralModeResponse{ Exchange: r.Exchange, Asset: r.Asset, Success: true, }, nil } // SetMarginType sets the margin type for the account asset pair func (s *RPCServer) SetMarginType(ctx context.Context, r *gctrpc.SetMarginTypeRequest) (*gctrpc.SetMarginTypeResponse, error) { if r == nil { return nil, fmt.Errorf("%w SetMarginTypeRequest", common.ErrNilPointer) } if r.Pair == nil { return nil, currency.ErrCurrencyPairEmpty } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsEnabled() { return nil, fmt.Errorf("%s %w", r.Exchange, errExchangeNotEnabled) } ai, err := asset.New(r.Asset) if err != nil { return nil, err } enabledPairs, err := exch.GetEnabledPairs(ai) if err != nil { return nil, err } cp, err := enabledPairs.DeriveFrom(r.Pair.Base + r.Pair.Quote) if err != nil { return nil, err } mt, err := margin.StringToMarginType(r.MarginType) if err != nil { return nil, err } err = exch.SetMarginType(ctx, ai, cp, mt) if err != nil { return nil, err } return &gctrpc.SetMarginTypeResponse{ Exchange: r.Exchange, Asset: r.Asset, Pair: r.Pair, Success: true, }, nil } // GetLeverage returns the leverage for the account asset pair func (s *RPCServer) GetLeverage(ctx context.Context, r *gctrpc.GetLeverageRequest) (*gctrpc.GetLeverageResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetLeverageRequest", common.ErrNilPointer) } if r.Pair == nil { return nil, currency.ErrCurrencyPairEmpty } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsEnabled() { return nil, fmt.Errorf("%s %w", r.Exchange, errExchangeNotEnabled) } feat := exch.GetSupportedFeatures() if !feat.FuturesCapabilities.Leverage { return nil, fmt.Errorf("%w futures position tracking for exchange %v", common.ErrFunctionNotSupported, exch.GetName()) } ai, err := asset.New(r.Asset) if err != nil { return nil, err } enabledPairs, err := exch.GetEnabledPairs(ai) if err != nil { return nil, err } cp, err := enabledPairs.DeriveFrom(r.Pair.Base + r.Pair.Quote) if err != nil { return nil, err } mt, err := margin.StringToMarginType(r.MarginType) if err != nil { return nil, err } var orderSide order.Side if r.OrderSide != "" { orderSide, err = order.StringToOrderSide(r.OrderSide) if err != nil { return nil, err } } leverage, err := exch.GetLeverage(ctx, ai, cp, mt, orderSide) if err != nil { return nil, err } return &gctrpc.GetLeverageResponse{ Exchange: r.Exchange, Asset: r.Asset, Pair: r.Pair, MarginType: r.MarginType, Leverage: leverage, OrderSide: r.OrderSide, }, nil } // SetLeverage sets the leverage for the account asset pair func (s *RPCServer) SetLeverage(ctx context.Context, r *gctrpc.SetLeverageRequest) (*gctrpc.SetLeverageResponse, error) { if r == nil { return nil, fmt.Errorf("%w SetLeverageRequest", common.ErrNilPointer) } if r.Pair == nil { return nil, currency.ErrCurrencyPairEmpty } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsEnabled() { return nil, fmt.Errorf("%s %w", r.Exchange, errExchangeNotEnabled) } feat := exch.GetSupportedFeatures() if !feat.FuturesCapabilities.Leverage { return nil, fmt.Errorf("%w futures position tracking for exchange %v", common.ErrFunctionNotSupported, exch.GetName()) } ai, err := asset.New(r.Asset) if err != nil { return nil, err } enabledPairs, err := exch.GetEnabledPairs(ai) if err != nil { return nil, err } cp, err := enabledPairs.DeriveFrom(r.Pair.Base + r.Pair.Quote) if err != nil { return nil, err } mt, err := margin.StringToMarginType(r.MarginType) if err != nil { return nil, err } var orderSide order.Side if r.OrderSide != "" { orderSide, err = order.StringToOrderSide(r.OrderSide) if err != nil { return nil, err } } err = exch.SetLeverage(ctx, ai, cp, mt, r.Leverage, orderSide) if err != nil { return nil, err } return &gctrpc.SetLeverageResponse{ Exchange: r.Exchange, Asset: r.Asset, Pair: r.Pair, MarginType: r.MarginType, OrderSide: r.OrderSide, Success: true, }, nil } // ChangePositionMargin sets a position's margin func (s *RPCServer) ChangePositionMargin(ctx context.Context, r *gctrpc.ChangePositionMarginRequest) (*gctrpc.ChangePositionMarginResponse, error) { if r == nil { return nil, fmt.Errorf("%w ChangePositionMarginRequest", common.ErrNilPointer) } if r.Pair == nil { return nil, currency.ErrCurrencyPairEmpty } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsEnabled() { return nil, fmt.Errorf("%s %w", r.Exchange, errExchangeNotEnabled) } ai, err := asset.New(r.Asset) if err != nil { return nil, err } enabledPairs, err := exch.GetEnabledPairs(ai) if err != nil { return nil, err } cp, err := enabledPairs.DeriveFrom(r.Pair.Base + r.Pair.Quote) if err != nil { return nil, err } mt, err := margin.StringToMarginType(r.MarginType) if err != nil { return nil, err } resp, err := exch.ChangePositionMargin(ctx, &margin.PositionChangeRequest{ Exchange: exch.GetName(), Pair: cp, Asset: ai, MarginType: mt, OriginalAllocatedMargin: r.OriginalAllocatedMargin, NewAllocatedMargin: r.NewAllocatedMargin, MarginSide: r.MarginSide, }) if err != nil { return nil, err } return &gctrpc.ChangePositionMarginResponse{ Exchange: r.Exchange, Asset: r.Asset, Pair: r.Pair, MarginType: r.MarginType, NewAllocatedMargin: resp.AllocatedMargin, MarginSide: r.MarginSide, }, nil } // GetOpenInterest fetches the open interest from the exchange func (s *RPCServer) GetOpenInterest(ctx context.Context, r *gctrpc.GetOpenInterestRequest) (*gctrpc.GetOpenInterestResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetOpenInterestRequest", common.ErrNilPointer) } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsEnabled() { return nil, fmt.Errorf("%s %w", r.Exchange, errExchangeNotEnabled) } feat := exch.GetSupportedFeatures() if !feat.FuturesCapabilities.OpenInterest.Supported { return nil, common.ErrFunctionNotSupported } keys := make([]key.PairAsset, len(r.Data)) for i := range r.Data { var a asset.Item a, err = asset.New(r.Data[i].Asset) if err != nil { return nil, err } keys[i].Base = currency.NewCode(r.Data[i].Pair.Base).Item keys[i].Quote = currency.NewCode(r.Data[i].Pair.Quote).Item keys[i].Asset = a } openInterest, err := exch.GetOpenInterest(ctx, keys...) if err != nil { return nil, err } resp := make([]*gctrpc.OpenInterestDataResponse, len(openInterest)) for i := range openInterest { resp[i] = &gctrpc.OpenInterestDataResponse{ Exchange: openInterest[i].Key.Exchange, Pair: &gctrpc.CurrencyPair{ Base: openInterest[i].Key.Base.String(), Quote: openInterest[i].Key.Quote.String(), }, Asset: openInterest[i].Key.Asset.String(), OpenInterest: openInterest[i].OpenInterest, } } return &gctrpc.GetOpenInterestResponse{ Data: resp, }, nil } // GetCurrencyTradeURL returns the URL for the trading pair func (s *RPCServer) GetCurrencyTradeURL(ctx context.Context, r *gctrpc.GetCurrencyTradeURLRequest) (*gctrpc.GetCurrencyTradeURLResponse, error) { if r == nil { return nil, fmt.Errorf("%w GetCurrencyTradeURLRequest", common.ErrNilPointer) } exch, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } if !exch.IsEnabled() { return nil, fmt.Errorf("%s %w", r.Exchange, errExchangeNotEnabled) } ai, err := asset.New(r.Asset) if err != nil { return nil, err } if r.Pair == nil || (r.Pair.Base == "" && r.Pair.Quote == "") { return nil, currency.ErrCurrencyPairEmpty } cp, err := exch.MatchSymbolWithAvailablePairs(r.Pair.Base+r.Pair.Quote, ai, false) if err != nil { return nil, err } url, err := exch.GetCurrencyTradeURL(ctx, ai, cp) if err != nil { return nil, err } return &gctrpc.GetCurrencyTradeURLResponse{ Url: url, }, nil }