package engine import ( "context" "errors" "fmt" "math" "net" "net/http" "path/filepath" "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/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/backtester/btrpc" "github.com/thrasher-corp/gocryptotrader/backtester/config" gctcommon "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/database/drivers" gctengine "github.com/thrasher-corp/gocryptotrader/engine" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/gctrpc/auth" "github.com/thrasher-corp/gocryptotrader/log" "github.com/thrasher-corp/gocryptotrader/utils" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" ) var ( errBadPort = errors.New("received bad port") errCannotHandleRequest = errors.New("cannot handle request") ) // GRPCServer struct type GRPCServer struct { btrpc.BacktesterServiceServer config *config.BacktesterConfig manager *TaskManager } // SetupRPCServer sets up the gRPC server func SetupRPCServer(cfg *config.BacktesterConfig, manager *TaskManager) (*GRPCServer, error) { if cfg == nil { return nil, fmt.Errorf("%w backtester config", gctcommon.ErrNilPointer) } if manager == nil { return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer) } return &GRPCServer{ config: cfg, manager: manager, }, nil } // StartRPCServer starts a gRPC server with TLS auth func StartRPCServer(server *GRPCServer) error { targetDir := utils.GetTLSDir(server.config.GRPC.TLSDir) if err := gctengine.CheckCerts(targetDir); err != nil { return err } log.Debugf(log.GRPCSys, "Backtester GRPC server enabled. Starting GRPC server on https://%v.\n", server.config.GRPC.ListenAddress) lis, err := net.Listen("tcp", server.config.GRPC.ListenAddress) if err != nil { return err } creds, err := credentials.NewServerTLSFromFile(filepath.Join(targetDir, "cert.pem"), filepath.Join(targetDir, "key.pem")) if err != nil { return err } opts := []grpc.ServerOption{ grpc.Creds(creds), grpc.UnaryInterceptor(grpcauth.UnaryServerInterceptor(server.authenticateClient)), } s := grpc.NewServer(opts...) btrpc.RegisterBacktesterServiceServer(s, server) go func() { if err = s.Serve(lis); err != nil { log.Error(log.GRPCSys, err) return } }() log.Debugln(log.GRPCSys, "GRPC server started!") if server.config.GRPC.GRPCProxyEnabled { return server.StartRPCRESTProxy() } return nil } // StartRPCRESTProxy starts a gRPC proxy func (s *GRPCServer) StartRPCRESTProxy() error { log.Debugf(log.GRPCSys, "GRPC proxy server support enabled. Starting gRPC proxy server on %v\n", s.config.GRPC.GRPCProxyListenAddress) targetDir := utils.GetTLSDir(s.config.GRPC.TLSDir) creds, err := credentials.NewClientTLSFromFile(filepath.Join(targetDir, "cert.pem"), "") if err != nil { return fmt.Errorf("unabled to start gRPC proxy. Err: %w", err) } mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(auth.BasicAuth{ Username: s.config.GRPC.Username, Password: s.config.GRPC.Password, }), } err = btrpc.RegisterBacktesterServiceHandlerFromEndpoint(context.Background(), mux, s.config.GRPC.ListenAddress, opts) if err != nil { return fmt.Errorf("failed to register gRPC proxy. Err: %w", err) } go func() { server := &http.Server{ Addr: s.config.GRPC.GRPCProxyListenAddress, ReadHeaderTimeout: time.Minute, ReadTimeout: time.Minute, } if err = server.ListenAndServe(); err != nil { log.Errorf(log.GRPCSys, "GRPC proxy failed to server: %s\n", err) } }() log.Debug(log.GRPCSys, "GRPC proxy server started!") return nil } func (s *GRPCServer) authenticateClient(ctx context.Context) (context.Context, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return ctx, fmt.Errorf("unable to extract metadata") } authStr, ok := md["authorization"] if !ok { return ctx, fmt.Errorf("authorization header missing") } if !strings.Contains(authStr[0], "Basic") { return ctx, fmt.Errorf("basic not found in authorization header") } decoded, err := crypto.Base64Decode(strings.Split(authStr[0], " ")[1]) if err != nil { return ctx, fmt.Errorf("unable to base64 decode authorization header") } creds := strings.Split(string(decoded), ":") username := creds[0] password := creds[1] if username != s.config.GRPC.Username || password != s.config.GRPC.Password { return ctx, fmt.Errorf("username/password mismatch") } return ctx, nil } // convertSummary converts a task summary into a RPC format func convertSummary(task *TaskSummary) *btrpc.TaskSummary { taskSummary := &btrpc.TaskSummary{ Id: task.MetaData.ID.String(), StrategyName: task.MetaData.Strategy, Closed: task.MetaData.Closed, LiveTesting: task.MetaData.LiveTesting, RealOrders: task.MetaData.RealOrders, } if !task.MetaData.DateStarted.IsZero() { taskSummary.DateStarted = task.MetaData.DateStarted.Format(gctcommon.SimpleTimeFormatWithTimezone) } if !task.MetaData.DateLoaded.IsZero() { taskSummary.DateLoaded = task.MetaData.DateLoaded.Format(gctcommon.SimpleTimeFormatWithTimezone) } if !task.MetaData.DateEnded.IsZero() { taskSummary.DateEnded = task.MetaData.DateEnded.Format(gctcommon.SimpleTimeFormatWithTimezone) } return taskSummary } // ExecuteStrategyFromFile will backtest a strategy from the filepath provided func (s *GRPCServer) ExecuteStrategyFromFile(_ context.Context, request *btrpc.ExecuteStrategyFromFileRequest) (*btrpc.ExecuteStrategyResponse, error) { if s.config == nil { return nil, fmt.Errorf("%w server config", gctcommon.ErrNilPointer) } if s.manager == nil { return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer) } if request == nil { return nil, fmt.Errorf("%w request", gctcommon.ErrNilPointer) } if request.DoNotRunImmediately && request.DoNotStore { return nil, fmt.Errorf("%w cannot manage a task with both dnr and dns", errCannotHandleRequest) } dir := request.StrategyFilePath cfg, err := config.ReadStrategyConfigFromFile(dir) if err != nil { return nil, err } io64 := int64(request.IntervalOverride) if io64 > 0 { if io64 < gctkline.FifteenSecond.Duration().Nanoseconds() { return nil, fmt.Errorf("%w, interval must be >= 15 seconds, received '%v'", gctkline.ErrInvalidInterval, time.Duration(request.IntervalOverride)) } cfg.DataSettings.Interval = gctkline.Interval(request.IntervalOverride) } sto := request.StartTimeOverride.AsTime() if sto.Unix() != 0 && !sto.IsZero() { if cfg.DataSettings.DatabaseData != nil { cfg.DataSettings.DatabaseData.StartDate = request.StartTimeOverride.AsTime() } else if cfg.DataSettings.APIData != nil { cfg.DataSettings.APIData.StartDate = request.StartTimeOverride.AsTime() } } eto := request.EndTimeOverride.AsTime() if eto.Unix() != 0 && !eto.IsZero() { if cfg.DataSettings.DatabaseData != nil { cfg.DataSettings.DatabaseData.EndDate = request.EndTimeOverride.AsTime() } else if cfg.DataSettings.APIData != nil { cfg.DataSettings.APIData.EndDate = request.EndTimeOverride.AsTime() } } err = cfg.Validate() if err != nil { return nil, err } if cfg == nil { err = fmt.Errorf("%w backtester config", gctcommon.ErrNilPointer) return nil, err } if !s.config.Report.GenerateReport { s.config.Report.OutputPath = "" s.config.Report.TemplatePath = "" } bt, err := NewBacktesterFromConfigs(cfg, s.config) if err != nil { return nil, err } if !request.DoNotStore { err = s.manager.AddTask(bt) if err != nil { return nil, err } } if !request.DoNotRunImmediately { err = bt.ExecuteStrategy(false) if err != nil { return nil, err } } btSum, err := bt.GenerateSummary() if err != nil { return nil, err } return &btrpc.ExecuteStrategyResponse{ Task: convertSummary(btSum), }, nil } // ExecuteStrategyFromConfig will backtest a strategy config built from a GRPC command // this should be a preferred method of interacting with backtester, as it allows for very quick // minor tweaks to strategy to determine the best result - SO LONG AS YOU DONT OVERFIT func (s *GRPCServer) ExecuteStrategyFromConfig(_ context.Context, request *btrpc.ExecuteStrategyFromConfigRequest) (*btrpc.ExecuteStrategyResponse, error) { if s.config == nil { return nil, fmt.Errorf("%w server config", gctcommon.ErrNilPointer) } if s.manager == nil { return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer) } if request == nil || request.Config == nil { return nil, fmt.Errorf("%w request", gctcommon.ErrNilPointer) } if request.DoNotRunImmediately && request.DoNotStore { return nil, fmt.Errorf("%w cannot manage a task with both dnr and dns", errCannotHandleRequest) } rfr, err := decimal.NewFromString(request.Config.StatisticSettings.RiskFreeRate) if err != nil { return nil, err } maximumOrdersWithLeverageRatio, err := decimal.NewFromString(request.Config.PortfolioSettings.Leverage.MaximumOrdersWithLeverageRatio) if err != nil { return nil, err } maximumOrderLeverageRate, err := decimal.NewFromString(request.Config.PortfolioSettings.Leverage.MaximumLeverageRate) if err != nil { return nil, err } maximumCollateralLeverageRate, err := decimal.NewFromString(request.Config.PortfolioSettings.Leverage.MaximumCollateralLeverageRate) if err != nil { return nil, err } buySideMinimumSize, err := decimal.NewFromString(request.Config.PortfolioSettings.BuySide.MinimumSize) if err != nil { return nil, err } buySideMaximumSize, err := decimal.NewFromString(request.Config.PortfolioSettings.BuySide.MaximumSize) if err != nil { return nil, err } buySideMaximumTotal, err := decimal.NewFromString(request.Config.PortfolioSettings.BuySide.MaximumTotal) if err != nil { return nil, err } sellSideMinimumSize, err := decimal.NewFromString(request.Config.PortfolioSettings.SellSide.MinimumSize) if err != nil { return nil, err } sellSideMaximumSize, err := decimal.NewFromString(request.Config.PortfolioSettings.SellSide.MaximumSize) if err != nil { return nil, err } sellSideMaximumTotal, err := decimal.NewFromString(request.Config.PortfolioSettings.SellSide.MaximumTotal) if err != nil { return nil, err } fundingSettings := make([]config.ExchangeLevelFunding, len(request.Config.FundingSettings.ExchangeLevelFunding)) for i := range request.Config.FundingSettings.ExchangeLevelFunding { var initialFunds, transferFee decimal.Decimal var a asset.Item initialFunds, err = decimal.NewFromString(request.Config.FundingSettings.ExchangeLevelFunding[i].InitialFunds) if err != nil { return nil, err } transferFee, err = decimal.NewFromString(request.Config.FundingSettings.ExchangeLevelFunding[i].TransferFee) if err != nil { return nil, err } a, err = asset.New(request.Config.FundingSettings.ExchangeLevelFunding[i].Asset) if err != nil { return nil, err } fundingSettings[i] = config.ExchangeLevelFunding{ ExchangeName: request.Config.FundingSettings.ExchangeLevelFunding[i].ExchangeName, Asset: a, Currency: currency.NewCode(request.Config.FundingSettings.ExchangeLevelFunding[i].Currency), InitialFunds: initialFunds, TransferFee: transferFee, } } customSettings := make(map[string]interface{}, len(request.Config.StrategySettings.CustomSettings)) for i := range request.Config.StrategySettings.CustomSettings { customSettings[request.Config.StrategySettings.CustomSettings[i].KeyField] = request.Config.StrategySettings.CustomSettings[i].KeyValue } configSettings := make([]config.CurrencySettings, len(request.Config.CurrencySettings)) for i := range request.Config.CurrencySettings { var currencySettingBuySideMinimumSize, currencySettingBuySideMaximumSize, currencySettingBuySideMaximumTotal, currencySettingSellSideMinimumSize, currencySettingSellSideMaximumSize, currencySettingSellSideMaximumTotal, minimumSlippagePercent, maximumSlippagePercent, maximumHoldingsRatio decimal.Decimal var a asset.Item currencySettingBuySideMinimumSize, err = decimal.NewFromString(request.Config.CurrencySettings[i].BuySide.MinimumSize) if err != nil { return nil, err } currencySettingBuySideMaximumSize, err = decimal.NewFromString(request.Config.CurrencySettings[i].BuySide.MaximumSize) if err != nil { return nil, err } currencySettingBuySideMaximumTotal, err = decimal.NewFromString(request.Config.CurrencySettings[i].BuySide.MaximumTotal) if err != nil { return nil, err } currencySettingSellSideMinimumSize, err = decimal.NewFromString(request.Config.CurrencySettings[i].SellSide.MinimumSize) if err != nil { return nil, err } currencySettingSellSideMaximumSize, err = decimal.NewFromString(request.Config.CurrencySettings[i].SellSide.MaximumSize) if err != nil { return nil, err } currencySettingSellSideMaximumTotal, err = decimal.NewFromString(request.Config.CurrencySettings[i].SellSide.MaximumTotal) if err != nil { return nil, err } minimumSlippagePercent, err = decimal.NewFromString(request.Config.CurrencySettings[i].MinSlippagePercent) if err != nil { return nil, err } maximumSlippagePercent, err = decimal.NewFromString(request.Config.CurrencySettings[i].MaxSlippagePercent) if err != nil { return nil, err } maximumHoldingsRatio, err = decimal.NewFromString(request.Config.CurrencySettings[i].MaximumHoldingsRatio) if err != nil { return nil, err } a, err = asset.New(request.Config.CurrencySettings[i].Asset) if err != nil { return nil, err } var maker, taker *decimal.Decimal if request.Config.CurrencySettings[i].MakerFeeOverride != "" { // nil is a valid option var m decimal.Decimal m, err = decimal.NewFromString(request.Config.CurrencySettings[i].MakerFeeOverride) if err != nil { return nil, fmt.Errorf("%v %v %v-%v maker fee %w", request.Config.CurrencySettings[i].ExchangeName, request.Config.CurrencySettings[i].Asset, request.Config.CurrencySettings[i].Base, request.Config.CurrencySettings[i].Quote, err) } maker = &m } if request.Config.CurrencySettings[i].TakerFeeOverride != "" { // nil is a valid option var t decimal.Decimal t, err = decimal.NewFromString(request.Config.CurrencySettings[i].MakerFeeOverride) if err != nil { return nil, fmt.Errorf("%v %v %v-%v taker fee %w", request.Config.CurrencySettings[i].ExchangeName, request.Config.CurrencySettings[i].Asset, request.Config.CurrencySettings[i].Base, request.Config.CurrencySettings[i].Quote, err) } taker = &t } var spotDetails *config.SpotDetails if request.Config.CurrencySettings[i].SpotDetails != nil { spotDetails = &config.SpotDetails{} if request.Config.CurrencySettings[i].SpotDetails.InitialBaseFunds != "" { var ibf decimal.Decimal ibf, err = decimal.NewFromString(request.Config.CurrencySettings[i].SpotDetails.InitialBaseFunds) if err != nil { return nil, err } spotDetails.InitialBaseFunds = &ibf } if request.Config.CurrencySettings[i].SpotDetails.InitialQuoteFunds != "" { var iqf decimal.Decimal iqf, err = decimal.NewFromString(request.Config.CurrencySettings[i].SpotDetails.InitialQuoteFunds) if err != nil { return nil, err } spotDetails.InitialQuoteFunds = &iqf } } var futuresDetails *config.FuturesDetails if request.Config.CurrencySettings[i].FuturesDetails != nil && request.Config.CurrencySettings[i].FuturesDetails.Leverage != nil { futuresDetails = &config.FuturesDetails{} var mowlr, mlr, mclr decimal.Decimal mowlr, err = decimal.NewFromString(request.Config.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio) if err != nil { return nil, err } mlr, err = decimal.NewFromString(request.Config.CurrencySettings[i].FuturesDetails.Leverage.MaximumLeverageRate) if err != nil { return nil, err } mclr, err = decimal.NewFromString(request.Config.CurrencySettings[i].FuturesDetails.Leverage.MaximumCollateralLeverageRate) if err != nil { return nil, err } futuresDetails.Leverage = config.Leverage{ CanUseLeverage: request.Config.CurrencySettings[i].FuturesDetails.Leverage.CanUseLeverage, MaximumOrdersWithLeverageRatio: mowlr, MaximumOrderLeverageRate: mlr, MaximumCollateralLeverageRate: mclr, } } configSettings[i] = config.CurrencySettings{ ExchangeName: request.Config.CurrencySettings[i].ExchangeName, Asset: a, Base: currency.NewCode(request.Config.CurrencySettings[i].Base), Quote: currency.NewCode(request.Config.CurrencySettings[i].Quote), SpotDetails: spotDetails, FuturesDetails: futuresDetails, BuySide: config.MinMax{ MinimumSize: currencySettingBuySideMinimumSize, MaximumSize: currencySettingBuySideMaximumSize, MaximumTotal: currencySettingBuySideMaximumTotal, }, SellSide: config.MinMax{ MinimumSize: currencySettingSellSideMinimumSize, MaximumSize: currencySettingSellSideMaximumSize, MaximumTotal: currencySettingSellSideMaximumTotal, }, MinimumSlippagePercent: minimumSlippagePercent, MaximumSlippagePercent: maximumSlippagePercent, MakerFee: maker, TakerFee: taker, MaximumHoldingsRatio: maximumHoldingsRatio, SkipCandleVolumeFitting: request.Config.CurrencySettings[i].SkipCandleVolumeFitting, CanUseExchangeLimits: request.Config.CurrencySettings[i].UseExchangeOrderLimits, ShowExchangeOrderLimitWarning: request.Config.CurrencySettings[i].UseExchangeOrderLimits, UseExchangePNLCalculation: request.Config.CurrencySettings[i].UseExchangePnlCalculation, } } var apiData *config.APIData if request.Config.DataSettings.ApiData != nil { apiData = &config.APIData{ StartDate: request.Config.DataSettings.ApiData.StartDate.AsTime(), EndDate: request.Config.DataSettings.ApiData.EndDate.AsTime(), InclusiveEndDate: request.Config.DataSettings.ApiData.InclusiveEndDate, } } var dbData *config.DatabaseData if request.Config.DataSettings.DatabaseData != nil { if request.Config.DataSettings.DatabaseData.Config.Config.Port > math.MaxUint16 { return nil, fmt.Errorf("%w '%v' cannot exceed '%v'", errBadPort, request.Config.DataSettings.DatabaseData.Config.Config.Port, math.MaxUint16) } cfg := database.Config{ Enabled: request.Config.DataSettings.DatabaseData.Config.Enabled, Verbose: request.Config.DataSettings.DatabaseData.Config.Verbose, Driver: request.Config.DataSettings.DatabaseData.Config.Driver, ConnectionDetails: drivers.ConnectionDetails{ Host: request.Config.DataSettings.DatabaseData.Config.Config.Host, Port: uint16(request.Config.DataSettings.DatabaseData.Config.Config.Port), Username: request.Config.DataSettings.DatabaseData.Config.Config.UserName, Password: request.Config.DataSettings.DatabaseData.Config.Config.Password, Database: request.Config.DataSettings.DatabaseData.Config.Config.Database, SSLMode: request.Config.DataSettings.DatabaseData.Config.Config.SslMode, }, } dbData = &config.DatabaseData{ StartDate: request.Config.DataSettings.DatabaseData.StartDate.AsTime(), EndDate: request.Config.DataSettings.DatabaseData.EndDate.AsTime(), Path: request.Config.DataSettings.DatabaseData.Path, Config: cfg, InclusiveEndDate: request.Config.DataSettings.DatabaseData.InclusiveEndDate, } } var liveData *config.LiveData if request.Config.DataSettings.LiveData != nil { creds := make([]config.Credentials, len(request.Config.DataSettings.LiveData.Credentials)) for i := range request.Config.DataSettings.LiveData.Credentials { creds[i] = config.Credentials{ Exchange: request.Config.DataSettings.LiveData.Credentials[i].Exchange, Keys: account.Credentials{ Key: request.Config.DataSettings.LiveData.Credentials[i].Keys.Key, Secret: request.Config.DataSettings.LiveData.Credentials[i].Keys.Secret, ClientID: request.Config.DataSettings.LiveData.Credentials[i].Keys.ClientId, PEMKey: request.Config.DataSettings.LiveData.Credentials[i].Keys.PemKey, SubAccount: request.Config.DataSettings.LiveData.Credentials[i].Keys.SubAccount, OneTimePassword: request.Config.DataSettings.LiveData.Credentials[i].Keys.OneTimePassword, }, } } liveData = &config.LiveData{ NewEventTimeout: time.Duration(request.Config.DataSettings.LiveData.NewEventTimeout), DataCheckTimer: time.Duration(request.Config.DataSettings.LiveData.DataCheckTimer), RealOrders: request.Config.DataSettings.LiveData.RealOrders, ClosePositionsOnStop: request.Config.DataSettings.LiveData.ClosePositionsOnStop, DataRequestRetryTolerance: request.Config.DataSettings.LiveData.DataRequestRetryTolerance, DataRequestRetryWaitTime: time.Duration(request.Config.DataSettings.LiveData.DataRequestRetryWaitTime), ExchangeCredentials: creds, } } var csvData *config.CSVData if request.Config.DataSettings.CsvData != nil { csvData = &config.CSVData{ FullPath: request.Config.DataSettings.CsvData.Path, } } cfg := &config.Config{ Nickname: request.Config.Nickname, Goal: request.Config.Goal, StrategySettings: config.StrategySettings{ Name: request.Config.StrategySettings.Name, SimultaneousSignalProcessing: request.Config.StrategySettings.UseSimultaneousSignalProcessing, DisableUSDTracking: request.Config.StrategySettings.DisableUsdTracking, CustomSettings: customSettings, }, FundingSettings: config.FundingSettings{ UseExchangeLevelFunding: request.Config.FundingSettings.UseExchangeLevelFunding, ExchangeLevelFunding: fundingSettings, }, CurrencySettings: configSettings, DataSettings: config.DataSettings{ Interval: gctkline.Interval(request.Config.DataSettings.Interval), DataType: request.Config.DataSettings.Datatype, APIData: apiData, DatabaseData: dbData, LiveData: liveData, CSVData: csvData, }, PortfolioSettings: config.PortfolioSettings{ Leverage: config.Leverage{ CanUseLeverage: request.Config.PortfolioSettings.Leverage.CanUseLeverage, MaximumOrdersWithLeverageRatio: maximumOrdersWithLeverageRatio, MaximumOrderLeverageRate: maximumOrderLeverageRate, MaximumCollateralLeverageRate: maximumCollateralLeverageRate, }, BuySide: config.MinMax{ MinimumSize: buySideMinimumSize, MaximumSize: buySideMaximumSize, MaximumTotal: buySideMaximumTotal, }, SellSide: config.MinMax{ MinimumSize: sellSideMinimumSize, MaximumSize: sellSideMaximumSize, MaximumTotal: sellSideMaximumTotal, }, }, StatisticSettings: config.StatisticSettings{ RiskFreeRate: rfr, }, } if !s.config.Report.GenerateReport { s.config.Report.OutputPath = "" s.config.Report.TemplatePath = "" } bt, err := NewBacktesterFromConfigs(cfg, s.config) if err != nil { return nil, err } if !request.DoNotStore { err = s.manager.AddTask(bt) if err != nil { return nil, err } } if !request.DoNotRunImmediately { err = bt.ExecuteStrategy(false) if err != nil { return nil, err } } btSum, err := bt.GenerateSummary() if err != nil { return nil, err } return &btrpc.ExecuteStrategyResponse{ Task: convertSummary(btSum), }, nil } // ListAllTasks returns all strategy tasks managed by the server func (s *GRPCServer) ListAllTasks(_ context.Context, _ *btrpc.ListAllTasksRequest) (*btrpc.ListAllTasksResponse, error) { if s.manager == nil { return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer) } list, err := s.manager.List() if err != nil { return nil, err } response := make([]*btrpc.TaskSummary, len(list)) for i := range list { response[i] = convertSummary(list[i]) } return &btrpc.ListAllTasksResponse{ Tasks: response, }, nil } // StopTask stops a strategy task in its tracks func (s *GRPCServer) StopTask(_ context.Context, req *btrpc.StopTaskRequest) (*btrpc.StopTaskResponse, error) { if s.manager == nil { return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer) } if req == nil { return nil, fmt.Errorf("%w StopTaskRequest", gctcommon.ErrNilPointer) } id, err := uuid.FromString(req.Id) if err != nil { return nil, err } task, err := s.manager.GetSummary(id) if err != nil { return nil, err } err = s.manager.StopTask(id) if err != nil { return nil, err } return &btrpc.StopTaskResponse{ StoppedTask: convertSummary(task), }, nil } // StopAllTasks stops all strategy tasks in its tracks func (s *GRPCServer) StopAllTasks(_ context.Context, _ *btrpc.StopAllTasksRequest) (*btrpc.StopAllTasksResponse, error) { if s.manager == nil { return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer) } stopped, err := s.manager.StopAllTasks() if err != nil { return nil, err } stoppedTasks := make([]*btrpc.TaskSummary, len(stopped)) for i := range stopped { stoppedTasks[i] = convertSummary(stopped[i]) } return &btrpc.StopAllTasksResponse{ TasksStopped: stoppedTasks, }, nil } // StartTask starts a strategy that was set to not start automatically func (s *GRPCServer) StartTask(_ context.Context, req *btrpc.StartTaskRequest) (*btrpc.StartTaskResponse, error) { if s.manager == nil { return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer) } if req == nil { return nil, fmt.Errorf("%w StartTaskRequest", gctcommon.ErrNilPointer) } id, err := uuid.FromString(req.Id) if err != nil { return nil, err } err = s.manager.StartTask(id) if err != nil { return nil, err } return &btrpc.StartTaskResponse{ Started: true, }, nil } // StartAllTasks starts all strategy tasks func (s *GRPCServer) StartAllTasks(_ context.Context, _ *btrpc.StartAllTasksRequest) (*btrpc.StartAllTasksResponse, error) { if s.manager == nil { return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer) } started, err := s.manager.StartAllTasks() if err != nil { return nil, err } startedTasks := make([]string, len(started)) for i := range started { startedTasks[i] = started[i].String() } return &btrpc.StartAllTasksResponse{ TasksStarted: startedTasks, }, nil } // ClearTask removes a task from memory, but only if it is not running func (s *GRPCServer) ClearTask(_ context.Context, req *btrpc.ClearTaskRequest) (*btrpc.ClearTaskResponse, error) { if s.manager == nil { return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer) } if req == nil { return nil, fmt.Errorf("%w ClearTaskRequest", gctcommon.ErrNilPointer) } id, err := uuid.FromString(req.Id) if err != nil { return nil, err } task, err := s.manager.GetSummary(id) if err != nil { return nil, err } err = s.manager.ClearTask(id) if err != nil { return nil, err } return &btrpc.ClearTaskResponse{ ClearedTask: convertSummary(task), }, nil } // ClearAllTasks removes all tasks from memory, but only if they are not running func (s *GRPCServer) ClearAllTasks(_ context.Context, _ *btrpc.ClearAllTasksRequest) (*btrpc.ClearAllTasksResponse, error) { if s.manager == nil { return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer) } clearedTasks, remainingTasks, err := s.manager.ClearAllTasks() if err != nil { return nil, err } clearedResponse := make([]*btrpc.TaskSummary, len(clearedTasks)) for i := range clearedTasks { clearedResponse[i] = convertSummary(clearedTasks[i]) } remainingResponse := make([]*btrpc.TaskSummary, len(remainingTasks)) for i := range remainingTasks { remainingResponse[i] = convertSummary(remainingTasks[i]) } return &btrpc.ClearAllTasksResponse{ ClearedTasks: clearedResponse, RemainingTasks: remainingResponse, }, nil }