cmd

package
v1.17.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2021 License: MIT Imports: 52 Imported by: 10

Documentation

Index

Constants

This section is empty.

Variables

View Source
var BacktestCmd = &cobra.Command{
	Use:          "backtest",
	Short:        "backtest your strategies",
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		verboseCnt, err := cmd.Flags().GetCount("verbose")
		if err != nil {
			return err
		}

		configFile, err := cmd.Flags().GetString("config")
		if err != nil {
			return err
		}

		if len(configFile) == 0 {
			return errors.New("--config option is required")
		}

		wantBaseAssetBaseline, err := cmd.Flags().GetBool("base-asset-baseline")
		if err != nil {
			return err
		}

		wantSync, err := cmd.Flags().GetBool("sync")
		if err != nil {
			return err
		}

		syncOnly, err := cmd.Flags().GetBool("sync-only")
		if err != nil {
			return err
		}

		syncFromDateStr, err := cmd.Flags().GetString("sync-from")
		if err != nil {
			return err
		}

		exchangeNameStr, err := cmd.Flags().GetString("exchange")
		if err != nil {
			return err
		}

		exchangeName, err := types.ValidExchangeName(exchangeNameStr)
		if err != nil {
			return err
		}

		sourceExchange, err := cmdutil.NewExchange(exchangeName)
		if err != nil {
			return err
		}

		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		userConfig, err := bbgo.Load(configFile, true)
		if err != nil {
			return err
		}

		if userConfig.Backtest == nil {
			return errors.New("backtest config is not defined")
		}

		now := time.Now()

		if len(userConfig.Backtest.StartTime) == 0 {
			userConfig.Backtest.StartTime = now.AddDate(0, -6, 0).Format("2006-01-02")
		}
		if len(userConfig.Backtest.EndTime) == 0 {
			userConfig.Backtest.EndTime = now.Format("2006-01-02")
		}

		if len(userConfig.CrossExchangeStrategies) > 0 {
			log.Warnf("backtest does not support CrossExchangeStrategy, strategies won't be added.")
		}

		startTime, err := userConfig.Backtest.ParseStartTime()
		if err != nil {
			return err
		}

		log.Infof("starting backtest with startTime %s", startTime.Format(time.ANSIC))

		environ := bbgo.NewEnvironment()
		if err := BootstrapBacktestEnvironment(ctx, environ, userConfig); err != nil {
			return err
		}

		if environ.DatabaseService == nil {
			return errors.New("database service is not enabled, please check your environment variables DB_DRIVER and DB_DSN")
		}

		backtestService := &service.BacktestService{DB: environ.DatabaseService.DB}
		environ.BacktestService = backtestService

		if wantSync {
			var syncFromTime = startTime

			if len(syncFromDateStr) > 0 {
				syncFromTime, err = time.Parse(types.DateFormat, syncFromDateStr)
				if err != nil {
					return err
				}

				if syncFromTime.After(startTime) {
					return fmt.Errorf("sync-from time %s can not be latter than the backtest start time %s", syncFromTime, startTime)
				}
			} else {

				syncFromTime = syncFromTime.AddDate(0, -1, 0)
				log.Infof("adjusted sync start time to %s for backward market data", syncFromTime)
			}

			log.Info("starting synchronization...")
			for _, symbol := range userConfig.Backtest.Symbols {
				if err := backtestService.Sync(ctx, sourceExchange, symbol, syncFromTime); err != nil {
					return err
				}
			}
			log.Info("synchronization done")

			var corruptCnt = 0
			for _, symbol := range userConfig.Backtest.Symbols {
				log.Infof("verifying backtesting data...")

				for interval := range types.SupportedIntervals {
					log.Infof("verifying %s %s kline data...", symbol, interval)

					klineC, errC := backtestService.QueryKLinesCh(startTime, time.Now(), sourceExchange, []string{symbol}, []types.Interval{interval})
					var emptyKLine types.KLine
					var prevKLine types.KLine
					for k := range klineC {
						if verboseCnt > 1 {
							fmt.Print(".")
						}

						if prevKLine != emptyKLine {
							if prevKLine.StartTime.Add(interval.Duration()) != k.StartTime {
								corruptCnt++
								log.Errorf("found kline data corrupted at time: %s kline: %+v", k.StartTime, k)
								log.Errorf("between %d and %d",
									prevKLine.StartTime.Unix(),
									k.StartTime.Unix())
							}
						}

						prevKLine = k
					}

					if verboseCnt > 1 {
						fmt.Println()
					}

					if err := <-errC; err != nil {
						return err
					}
				}
			}

			log.Infof("backtest verification completed")
			if corruptCnt > 0 {
				log.Errorf("found %d corruptions", corruptCnt)
			} else {
				log.Infof("found %d corruptions", corruptCnt)
			}

			if syncOnly {
				return nil
			}
		}

		log.Warn("!!! To run backtest, you should use an isolated database for storing backtest trades !!!")
		log.Warn("!!! The trade record in the current database WILL ALL BE DELETE !!!")
		if !confirmation("Are you sure to continue?") {
			return nil
		}

		if err := environ.TradeService.DeleteAll(); err != nil {
			return err
		}

		backtestExchange := backtest.NewExchange(exchangeName, backtestService, userConfig.Backtest)
		environ.SetStartTime(startTime)
		environ.AddExchange(exchangeName.String(), backtestExchange)

		if err := environ.Init(ctx); err != nil {
			return err
		}

		trader := bbgo.NewTrader(environ)

		if verboseCnt == 2 {
			log.SetLevel(log.DebugLevel)
		} else if verboseCnt > 0 {
			log.SetLevel(log.InfoLevel)
		} else {

			log.SetLevel(log.ErrorLevel)
			trader.DisableLogging()
		}

		if err := trader.Configure(userConfig); err != nil {
			return err
		}

		if err := trader.Run(ctx); err != nil {
			return err
		}

		<-backtestExchange.Done()

		log.Infof("shutting down trader...")
		shutdownCtx, cancel := context.WithDeadline(ctx, time.Now().Add(10*time.Second))
		trader.Graceful.Shutdown(shutdownCtx)
		cancel()

		log.SetLevel(log.InfoLevel)
		for _, session := range environ.Sessions() {
			calculator := &pnl.AverageCostCalculator{
				TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(),
			}
			for symbol, trades := range session.Trades {
				market, ok := session.Market(symbol)
				if !ok {
					return fmt.Errorf("market not found: %s", symbol)
				}

				startPrice, ok := session.StartPrice(symbol)
				if !ok {
					return fmt.Errorf("start price not found: %s", symbol)
				}

				log.Infof("%s PROFIT AND LOSS REPORT", symbol)
				log.Infof("===============================================")

				lastPrice, ok := session.LastPrice(symbol)
				if !ok {
					return fmt.Errorf("last price not found: %s", symbol)
				}

				report := calculator.Calculate(symbol, trades.Trades, lastPrice)
				report.Print()

				initBalances := userConfig.Backtest.Account.Balances.BalanceMap()
				finalBalances := session.Account.Balances()

				log.Infof("INITIAL BALANCES:")
				initBalances.Print()

				log.Infof("FINAL BALANCES:")
				finalBalances.Print()

				if wantBaseAssetBaseline {
					initBaseAsset := inBaseAsset(initBalances, market, startPrice)
					finalBaseAsset := inBaseAsset(finalBalances, market, lastPrice)
					log.Infof("INITIAL ASSET ~= %s %s (1 %s = %f)", market.FormatQuantity(initBaseAsset), market.BaseCurrency, market.BaseCurrency, startPrice)
					log.Infof("FINAL ASSET ~= %s %s (1 %s = %f)", market.FormatQuantity(finalBaseAsset), market.BaseCurrency, market.BaseCurrency, lastPrice)

					log.Infof("%s BASE ASSET PERFORMANCE: %.2f%% (= (%.2f - %.2f) / %.2f)", market.BaseCurrency, (finalBaseAsset-initBaseAsset)/initBaseAsset*100.0, finalBaseAsset, initBaseAsset, initBaseAsset)
					log.Infof("%s PERFORMANCE: %.2f%% (= (%.2f - %.2f) / %.2f)", market.BaseCurrency, (lastPrice-startPrice)/startPrice*100.0, lastPrice, startPrice, startPrice)
				}
			}
		}

		return nil
	},
}
View Source
var BuildCmd = &cobra.Command{
	Use:   "build",
	Short: "build cross-platform binary",

	SilenceUsage: true,

	RunE: func(cmd *cobra.Command, args []string) error {
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		configFile, err := cmd.Flags().GetString("config")
		if err != nil {
			return err
		}

		if len(configFile) == 0 {
			return errors.New("--config option is required")
		}

		userConfig, err := bbgo.LoadBuildConfig(configFile)
		if err != nil {
			return err
		}

		if userConfig.Build == nil {
			return errors.New("build config is not defined")
		}

		for _, target := range userConfig.Build.Targets {
			log.Infof("building %s ...", target.Name)

			binary, err := bbgo.BuildTarget(ctx, userConfig, target)
			if err != nil {
				return err
			}

			log.Infof("build succeeded: %s", binary)
		}

		return nil
	},
}
View Source
var PnLCmd = &cobra.Command{
	Use:          "pnl",
	Short:        "pnl calculator",
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := context.Background()

		configFile, err := cmd.Flags().GetString("config")
		if err != nil {
			return err
		}

		if len(configFile) == 0 {
			return errors.New("--config option is required")
		}

		if _, err := os.Stat(configFile); os.IsNotExist(err) {
			return err
		}

		userConfig, err := bbgo.Load(configFile, false)
		if err != nil {
			return err
		}

		sessionName, err := cmd.Flags().GetString("session")
		if err != nil {
			return err
		}

		symbol, err := cmd.Flags().GetString("symbol")
		if err != nil {
			return err
		}

		if len(symbol) == 0 {
			return errors.New("--symbol [SYMBOL] is required")
		}

		limit, err := cmd.Flags().GetInt("limit")
		if err != nil {
			return err
		}

		environ := bbgo.NewEnvironment()

		if err := environ.ConfigureDatabase(ctx); err != nil {
			return err
		}

		if err := environ.ConfigureExchangeSessions(userConfig); err != nil {
			return err
		}

		session, ok := environ.Session(sessionName)
		if !ok {
			return fmt.Errorf("session %s not found", sessionName)
		}

		if err := environ.SyncSession(ctx, session); err != nil {
			return err
		}

		exchange := session.Exchange

		market, ok := session.Market(symbol)
		if !ok {
			return fmt.Errorf("market config %s not found", symbol)
		}

		since := time.Now().AddDate(-1, 0, 0)
		until := time.Now()

		includeTransfer, err := cmd.Flags().GetBool("include-transfer")
		if err != nil {
			return err
		}

		if includeTransfer {
			transferService, ok := exchange.(types.ExchangeTransferService)
			if !ok {
				return fmt.Errorf("session exchange %s does not implement transfer service", sessionName)
			}

			deposits, err := transferService.QueryDepositHistory(ctx, market.BaseCurrency, since, until)
			if err != nil {
				return err
			}
			_ = deposits

			withdrawals, err := transferService.QueryWithdrawHistory(ctx, market.BaseCurrency, since, until)
			if err != nil {
				return err
			}
			_ = withdrawals

			backtestService := &service.BacktestService{DB: environ.DatabaseService.DB}
			if err := backtestService.SyncKLineByInterval(ctx, exchange, symbol, types.Interval1d, since, until); err != nil {
				return err
			}
		}

		var trades []types.Trade
		tradingFeeCurrency := exchange.PlatformFeeCurrency()
		if strings.HasPrefix(symbol, tradingFeeCurrency) {
			log.Infof("loading all trading fee currency related trades: %s", symbol)
			trades, err = environ.TradeService.QueryForTradingFeeCurrency(exchange.Name(), symbol, tradingFeeCurrency)
		} else {
			trades, err = environ.TradeService.Query(service.QueryTradesOptions{
				Exchange: exchange.Name(),
				Symbol:   symbol,
				Limit:    limit,
			})
		}

		if err != nil {
			return err
		}

		log.Infof("%d trades loaded", len(trades))

		stockManager := &accounting.StockDistribution{
			Symbol:             symbol,
			TradingFeeCurrency: tradingFeeCurrency,
		}

		checkpoints, err := stockManager.AddTrades(trades)
		if err != nil {
			return err
		}

		log.Infof("found checkpoints: %+v", checkpoints)
		log.Infof("stock: %f", stockManager.Stocks.Quantity())

		tickers, err := exchange.QueryTickers(ctx, symbol)

		if err != nil {
			return err
		}

		currentTick, ok := tickers[symbol]

		if !ok {
			return errors.New("no ticker data for current price")
		}

		currentPrice := currentTick.Last

		calculator := &pnl.AverageCostCalculator{
			TradingFeeCurrency: tradingFeeCurrency,
		}

		report := calculator.Calculate(symbol, trades, currentPrice)
		report.Print()
		return nil
	},
}
View Source
var RootCmd = &cobra.Command{
	Use:   "bbgo",
	Short: "bbgo is a crypto trading bot",

	SilenceUsage: true,

	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
		disableDotEnv, err := cmd.Flags().GetBool("no-dotenv")
		if err != nil {
			return err
		}

		if !disableDotEnv {
			dotenvFile, err := cmd.Flags().GetString("dotenv")
			if err != nil {
				return err
			}

			if _, err := os.Stat(dotenvFile); err == nil {
				if err := godotenv.Load(dotenvFile); err != nil {
					return errors.Wrap(err, "error loading dotenv file")
				}
			}
		}

		configFile, err := cmd.Flags().GetString("config")
		if err != nil {
			return errors.Wrapf(err, "failed to get the config flag")
		}

		if len(configFile) > 0 {

			if _, err := os.Stat(configFile); err == nil {

				userConfig, err = bbgo.Load(configFile, false)
				if err != nil {
					return errors.Wrapf(err, "can not load config file: %s", configFile)
				}

			} else if os.IsNotExist(err) {

				userConfig = &bbgo.Config{}
			} else {

				return errors.Wrapf(err, "config file load error: %s", configFile)
			}
		}

		return nil
	},

	RunE: func(cmd *cobra.Command, args []string) error {
		return nil
	},
}
View Source
var RunCmd = &cobra.Command{
	Use:   "run",
	Short: "run strategies from config file",

	SilenceUsage: true,
	RunE:         run,
}
View Source
var SyncCmd = &cobra.Command{
	Use:          "sync",
	Short:        "sync trades, orders",
	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := context.Background()

		configFile, err := cmd.Flags().GetString("config")
		if err != nil {
			return err
		}

		if len(configFile) == 0 {
			return errors.New("--config option is required")
		}

		if _, err := os.Stat(configFile); os.IsNotExist(err) {
			return err
		}

		userConfig, err := bbgo.Load(configFile, false)
		if err != nil {
			return err
		}

		since, err := cmd.Flags().GetString("since")
		if err != nil {
			return err
		}

		environ := bbgo.NewEnvironment()
		if err := environ.ConfigureDatabase(ctx); err != nil {
			return err
		}

		if err := environ.ConfigureExchangeSessions(userConfig); err != nil {
			return err
		}

		var (
			// default sync start time
			startTime = time.Now().AddDate(-1, 0, 0)
		)

		if len(since) > 0 {
			loc, err := time.LoadLocation("Local")
			if err != nil {
				return err
			}

			startTime, err = time.ParseInLocation("2006-01-02", since, loc)
			if err != nil {
				return err
			}
		}

		sessionName, err := cmd.Flags().GetString("session")
		if err != nil {
			return err
		}

		symbol, err := cmd.Flags().GetString("symbol")
		if err != nil {
			return err
		}

		environ.SetSyncStartTime(startTime)

		var defaultSymbols []string
		if len(symbol) > 0 {
			defaultSymbols = []string{symbol}
		}

		var selectedSessions []string

		if len(sessionName) > 0 {
			selectedSessions = []string{sessionName}
		}

		sessions := environ.SelectSessions(selectedSessions...)
		for _, session := range sessions {
			if err := environ.SyncSession(ctx, session, defaultSymbols...); err != nil {
				return err
			}

			log.Infof("exchange session %s synchronization done", session.Name)
		}

		return nil
	},
}
View Source
var TransferHistoryCmd = &cobra.Command{
	Use:   "transfer-history",
	Short: "show transfer history",

	SilenceUsage: true,
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := context.Background()

		configFile, err := cmd.Flags().GetString("config")
		if err != nil {
			return err
		}

		userConfig, err := bbgo.Load(configFile, false)
		if err != nil {
			return err
		}

		environ := bbgo.NewEnvironment()
		if err := BootstrapEnvironment(ctx, environ, userConfig); err != nil {
			return err
		}

		sessionName, err := cmd.Flags().GetString("session")
		if err != nil {
			return err
		}

		asset, err := cmd.Flags().GetString("asset")
		if err != nil {
			return err
		}

		session, ok := environ.Session(sessionName)
		if !ok {
			return fmt.Errorf("session %s not found", sessionName)
		}

		// default
		var now = time.Now()
		var since = now.AddDate(-1, 0, 0)
		var until = now

		sinceStr, err := cmd.Flags().GetString("since")
		if err != nil {
			return err
		}

		if len(sinceStr) > 0 {
			loc, err := time.LoadLocation("Asia/Taipei")
			if err != nil {
				return err
			}

			since, err = time.ParseInLocation("2006-01-02", sinceStr, loc)
			if err != nil {
				return err
			}
		}

		var records timeSlice

		exchange, ok := session.Exchange.(types.ExchangeTransferService)
		if !ok {
			return fmt.Errorf("exchange session %s does not implement transfer service", sessionName)
		}

		deposits, err := exchange.QueryDepositHistory(ctx, asset, since, until)
		if err != nil {
			return err
		}
		for _, d := range deposits {
			records = append(records, timeRecord{
				Record: d,
				Time:   d.EffectiveTime(),
			})
		}

		withdraws, err := exchange.QueryWithdrawHistory(ctx, asset, since, until)
		if err != nil {
			return err
		}
		for _, w := range withdraws {
			records = append(records, timeRecord{
				Record: w,
				Time:   w.EffectiveTime(),
			})
		}

		sort.Sort(records)

		for _, record := range records {
			switch record := record.Record.(type) {

			case types.Deposit:
				logrus.Infof("%s: <--- DEPOSIT %f %s [%s]", record.Time, record.Amount, record.Asset, record.Status)

			case types.Withdraw:
				logrus.Infof("%s: ---> WITHDRAW %f %s  [%s]", record.ApplyTime, record.Amount, record.Asset, record.Status)

			default:
				logrus.Infof("unknown record: %+v", record)

			}
		}

		stats := calBaselineStats(asset, deposits, withdraws)
		for asset, quantity := range stats.TotalDeposit {
			logrus.Infof("total %s deposit: %f", asset, quantity)
		}

		for asset, quantity := range stats.TotalWithdraw {
			logrus.Infof("total %s withdraw: %f", asset, quantity)
		}

		for asset, quantity := range stats.BaselineBalance {
			logrus.Infof("baseline %s balance: %f", asset, quantity)
		}

		return nil
	},
}
View Source
var VersionCmd = &cobra.Command{
	Use:          "version",
	Short:        "show version name",
	SilenceUsage: true,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println(version.Version)
	},
}

Functions

func BootstrapBacktestEnvironment added in v1.15.5

func BootstrapBacktestEnvironment(ctx context.Context, environ *bbgo.Environment, userConfig *bbgo.Config) error

func BootstrapEnvironment added in v1.11.0

func BootstrapEnvironment(ctx context.Context, environ *bbgo.Environment, userConfig *bbgo.Config) error

func Execute

func Execute()

Types

type BaselineStats

type BaselineStats struct {
	Asset           string
	TotalDeposit    map[string]float64
	TotalWithdraw   map[string]float64
	BaselineBalance map[string]float64
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL