cmd

package
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: Dec 31, 2020 License: MIT Imports: 45 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
		}

		syncFromTime, err := time.Parse(types.DateFormat, syncFromDateStr)
		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
		}

		db, err := cmdutil.ConnectMySQL()
		if err != nil {
			return err
		}

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

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

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

		backtestService := &service.BacktestService{DB: db}

		if wantSync {
			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
			}
		}

		backtestExchange := backtest.NewExchange(exchangeName, backtestService, userConfig.Backtest)

		environ := bbgo.NewEnvironment()
		environ.SetStartTime(startTime)
		environ.AddExchange(exchangeName.String(), backtestExchange)

		environ.Notifiability = bbgo.Notifiability{
			SymbolChannelRouter:  bbgo.NewPatternChannelRouter(nil),
			SessionChannelRouter: bbgo.NewPatternChannelRouter(nil),
			ObjectChannelRouter:  bbgo.NewObjectChannelRouter(),
		}

		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 userConfig.RiskControls != nil {
			log.Infof("setting risk controls: %+v", userConfig.RiskControls)
			trader.SetRiskControls(userConfig.RiskControls)
		}

		for _, entry := range userConfig.ExchangeStrategies {
			log.Infof("attaching strategy %T on %s instead of %v", entry.Strategy, exchangeName.String(), entry.Mounts)
			trader.AttachStrategyOn(exchangeName.String(), entry.Strategy)
		}

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

		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, 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")
		}

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

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

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

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

		buildDir := filepath.Join("build", "bbgow")

		binary, err := build(ctx, buildDir, userConfig, goOS, goArch, &output)
		if err != nil {
			return err
		}

		log.Infof("build succeeded: %s", binary)
		return nil
	},
}
View Source
var CancelCmd = &cobra.Command{
	Use:   "cancel",
	Short: "cancel orders",
	Long:  "this command can cancel orders from exchange",

	SilenceUsage: true,

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

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

		groupID, err := cmd.Flags().GetString("groupID")
		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")
		}

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

		environ := bbgo.NewEnvironment()

		if viper.IsSet("mysql-url") {
			db, err := cmdutil.ConnectMySQL()
			if err != nil {
				return err
			}
			environ.SetDB(db)
		}

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

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

		var sessions = environ.Sessions()

		if n, err := cmd.Flags().GetString("session"); err == nil && len(n) > 0 {
			ses, ok := sessions[n]
			if !ok {
				return fmt.Errorf("session %s not found", n)
			}

			sessions = map[string]*bbgo.ExchangeSession{n: ses}
		}

		for sessionID, session := range sessions {
			var log = logrus.WithField("session", sessionID)

			e, ok := session.Exchange.(advancedOrderCancelApi)
			if ok && len(groupID) > 0 {
				log.Infof("canceling orders by group id: %s", groupID)

				orders, err := e.CancelOrdersByGroupID(ctx, groupID)
				if err != nil {
					return err
				}

				for _, o := range orders {
					log.Info("CANCELED ", o.String())
				}

			} else if ok && len(symbol) > 0 {
				log.Infof("canceling orders by symbol: %s", symbol)

				orders, err := e.CancelOrdersBySymbol(ctx, symbol)
				if err != nil {
					return err
				}

				for _, o := range orders {
					log.Info("CANCELED ", o.String())
				}

			} else if len(symbol) > 0 {
				openOrders, err := session.Exchange.QueryOpenOrders(ctx, symbol)
				if err != nil {
					return err
				}

				if err := session.Exchange.CancelOrders(ctx, openOrders...); err != nil {
					return err
				}
			} else {
				log.Error("unsupported operation")
			}
		}

		return nil
	},
}
View Source
var MigrateCmd = &cobra.Command{
	Use:          "migrate",
	Short:        "run database migration",
	SilenceUsage: true,
	Args:         cobra.MinimumNArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := context.Background()

		noUpdate, err := cmd.Flags().GetBool("no-update")
		if err != nil {
			return err
		}

		mysqlURL := viper.GetString("mysql-url")
		mysqlURL = fmt.Sprintf("%s?parseTime=true", mysqlURL)
		db, err := goose.OpenDBWithDriver("mysql", mysqlURL)
		if err != nil {
			return err
		}

		dotDir := bbgo.HomeDir()
		sourceDir := bbgo.SourceDir()
		migrationDir := path.Join(sourceDir, "migrations")

		logrus.Infof("creating dir: %s", dotDir)
		if err := os.Mkdir(dotDir, 0777); err != nil {

		}

		logrus.Infof("checking %s", sourceDir)
		_, err = os.Stat(sourceDir)
		if err != nil {
			logrus.Infof("cloning bbgo source into %s ...", sourceDir)
			cmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/c9s/bbgo", sourceDir)
			if err := cmd.Run(); err != nil {
				return err
			}
		} else if !noUpdate {
			logrus.Infof("updating: %s ...", sourceDir)
			cmd := exec.CommandContext(ctx, "git", "--work-tree", sourceDir, "pull")
			if err := cmd.Run(); err != nil {
				return err
			}
		}

		logrus.Infof("using migration file dir: %s", migrationDir)

		command := args[0]
		if err := goose.Run(command, db, migrationDir); err != nil {
			logrus.Fatalf("goose run: %v", err)
		}

		defer db.Close()

		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()

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

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

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

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

		db, err := cmdutil.ConnectMySQL()
		if err != nil {
			return err
		}

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

		// default start time
		var startTime = time.Now().AddDate(0, -3, 0)
		if len(since) > 0 {
			loc, err := time.LoadLocation("Asia/Taipei")
			if err != nil {
				return err
			}

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

		tradeService := &service.TradeService{DB: db}
		orderService := &service.OrderService{DB: db}
		syncService := &service.SyncService{
			TradeService: tradeService,
			OrderService: orderService,
		}

		logrus.Info("syncing trades from exchange...")
		if err := syncService.SyncTrades(ctx, exchange, symbol, startTime); err != nil {
			return err
		}

		logrus.Info("syncing orders from exchange...")
		if err := syncService.SyncOrders(ctx, exchange, symbol, startTime); err != nil {
			return err
		}

		var trades []types.Trade
		tradingFeeCurrency := exchange.PlatformFeeCurrency()
		if strings.HasPrefix(symbol, tradingFeeCurrency) {
			logrus.Infof("loading all trading fee currency related trades: %s", symbol)
			trades, err = tradeService.QueryForTradingFeeCurrency(exchange.Name(), symbol, tradingFeeCurrency)
		} else {
			trades, err = tradeService.Query(exchange.Name(), symbol)
		}

		if err != nil {
			return err
		}

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

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

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

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

		now := time.Now()
		kLines, err := exchange.QueryKLines(ctx, symbol, types.Interval1m, types.KLineQueryOptions{
			Limit:   100,
			EndTime: &now,
		})

		if len(kLines) == 0 {
			return errors.New("no kline data for current price")
		}

		currentPrice := kLines[len(kLines)-1].Close
		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 trade bot",
	Long:  "bitcoin trader",

	SilenceUsage: true,

	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: func(cmd *cobra.Command, args []string) error {
		configFile, err := cmd.Flags().GetString("config")
		if err != nil {
			return err
		}

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

		noCompile, err := cmd.Flags().GetBool("no-compile")
		if err != nil {
			return err
		}

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

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

		shouldCompile := len(userConfig.Imports) > 0

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

			log.Infof("running config without wrapper binary...")
			if err := runConfig(ctx, userConfig); err != nil {
				return err
			}

			return nil
		}

		var runArgs = []string{"run", "--no-compile"}
		cmd.Flags().Visit(func(flag *flag.Flag) {
			runArgs = append(runArgs, "--"+flag.Name, flag.Value.String())
		})
		runArgs = append(runArgs, args...)

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

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

		runCmd, err := buildAndRun(ctx, userConfig, goOS, goArch, runArgs...)
		if err != nil {
			return err
		}

		if sig := cmdutil.WaitForSignal(ctx, syscall.SIGTERM, syscall.SIGINT); sig != nil {
			log.Infof("sending signal to the child process...")
			if err := runCmd.Process.Signal(sig); err != nil {
				return err
			}

			if err := runCmd.Wait(); err != nil {
				return err
			}
		}

		return nil
	},
}
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()

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

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

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

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

		db, err := cmdutil.ConnectMySQL()
		if err != nil {
			return err
		}

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

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

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

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

		tradeService := &service.TradeService{DB: db}
		orderService := &service.OrderService{DB: db}
		syncService := &service.SyncService{
			TradeService: tradeService,
			OrderService: orderService,
		}

		log.Info("syncing trades from exchange...")
		if err := syncService.SyncTrades(ctx, exchange, symbol, startTime); err != nil {
			return err
		}

		log.Info("syncing orders from exchange...")
		if err := syncService.SyncOrders(ctx, exchange, symbol, startTime); err != nil {
			return err
		}

		log.Info("synchronization done")

		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()
		_ = ctx

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

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

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

		// 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
			}
		}

		exchange, _ := cmdutil.NewExchange(exchangeName)

		var records timeSlice

		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: %s <== (deposit) %f [%s]", record.Time, record.Asset, record.Amount, record.Status)

			case types.Withdraw:
				logrus.Infof("%s: %s ==> (withdraw) %f [%s]", record.ApplyTime, record.Asset, record.Amount, 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
	},
}

Functions

func Execute

func Execute()

func InBaseAsset

func InBaseAsset(balances types.BalanceMap, market types.Market, price float64) float64

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