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") } 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 := environ.ConfigureDatabase(ctx); 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 } } 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.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.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 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().GetInt64("group-id") if err != nil { return err } all, err := cmd.Flags().GetBool("all") 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 err := environ.ConfigureDatabase(ctx); err != nil { return err } if err := environ.ConfigureExchangeSessions(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 { if all { log.Infof("canceling all orders") orders, err := e.CancelAllOrders(ctx) if err != nil { return err } for _, o := range orders { log.Info("CANCELED ", o.String()) } } else if groupID > 0 { log.Infof("canceling orders by group id: %d", groupID) orders, err := e.CancelOrdersByGroupID(ctx, groupID) if err != nil { return err } for _, o := range orders { log.Info("CANCELED ", o.String()) } } else if 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 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") } } } 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 }, }
Functions ¶
func BootstrapEnvironment ¶ added in v1.11.0
Types ¶
Source Files ¶
Click to show internal directories.
Click to hide internal directories.