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 } if viper.GetBool("debug") { verboseCnt = 2 } 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 } force, err := cmd.Flags().GetBool("force") if err != nil { return err } outputDirectory, err := cmd.Flags().GetString("output") if err != nil { return err } jsonOutputEnabled := len(outputDirectory) > 0 syncOnly, err := cmd.Flags().GetBool("sync-only") if err != nil { return err } syncFromDateStr, err := cmd.Flags().GetString("sync-from") if err != nil { return err } shouldVerify, err := cmd.Flags().GetBool("verify") if err != nil { return err } userConfig, err := bbgo.Load(configFile, true) if err != nil { return err } if verboseCnt == 2 { log.SetLevel(log.DebugLevel) } else if verboseCnt > 0 { log.SetLevel(log.InfoLevel) } else { log.SetLevel(log.ErrorLevel) } if userConfig.Backtest == nil { return errors.New("backtest config is not defined") } ctx, cancel := context.WithCancel(context.Background()) defer cancel() var now = time.Now() var startTime, endTime time.Time startTime = userConfig.Backtest.StartTime.Time() if userConfig.Backtest.EndTime != nil { endTime = userConfig.Backtest.EndTime.Time() } else { endTime = now } _ = endTime 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 var sourceExchanges = make(map[types.ExchangeName]types.Exchange) if len(userConfig.Backtest.Sessions) > 0 { for _, name := range userConfig.Backtest.Sessions { exName, err := types.ValidExchangeName(name) if err != nil { return err } publicExchange, err := cmdutil.NewExchangePublic(exName) if err != nil { return err } sourceExchanges[exName] = publicExchange } } else { for _, exName := range types.SupportedExchanges { publicExchange, err := cmdutil.NewExchangePublic(exName) if err != nil { return err } sourceExchanges[exName] = publicExchange } } if wantSync { var syncFromTime time.Time 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 = startTime.AddDate(0, -1, 0) log.Infof("adjusted sync start time %s to %s for backward market data", startTime, syncFromTime) } log.Infof("starting synchronization: %v", userConfig.Backtest.Symbols) for _, symbol := range userConfig.Backtest.Symbols { for _, sourceExchange := range sourceExchanges { exCustom, ok := sourceExchange.(types.CustomIntervalProvider) var supportIntervals map[types.Interval]int if ok { supportIntervals = exCustom.SupportedInterval() } else { supportIntervals = types.SupportedIntervals } for interval := range supportIntervals { firstKLine, err := backtestService.QueryFirstKLine(sourceExchange.Name(), symbol, interval) if err != nil { return errors.Wrapf(err, "failed to query backtest kline") } if firstKLine != nil { if err := backtestService.SyncExist(ctx, sourceExchange, symbol, syncFromTime, time.Now(), interval); err != nil { return err } } else { if err := backtestService.Sync(ctx, sourceExchange, symbol, syncFromTime, time.Now(), interval); err != nil { return err } } } } } log.Info("synchronization done") for _, sourceExchange := range sourceExchanges { if shouldVerify { err2, done := backtestService.Verify(userConfig.Backtest.Symbols, startTime, time.Now(), sourceExchange, verboseCnt) if done { return err2 } } } if syncOnly { return nil } } if userConfig.Backtest.RecordTrades { log.Warn("!!! Trade recording is enabled for back-testing !!!") log.Warn("!!! To run back-testing, you should use an isolated database for storing back-testing trades !!!") log.Warn("!!! The trade record in the current database WILL ALL BE DELETED BEFORE THIS BACK-TESTING !!!") if !force { if !confirmation("Are you sure to continue?") { return nil } } if err := environ.TradeService.DeleteAll(); err != nil { return err } } environ.SetStartTime(startTime) for name, sourceExchange := range sourceExchanges { backtestExchange, err := backtest.NewExchange(sourceExchange.Name(), sourceExchange, backtestService, userConfig.Backtest) if err != nil { return errors.Wrap(err, "failed to create backtest exchange") } environ.AddExchange(name.String(), backtestExchange) } if err := environ.Init(ctx); err != nil { return err } trader := bbgo.NewTrader(environ) if verboseCnt == 0 { trader.DisableLogging() } if err := trader.Configure(userConfig); err != nil { return err } if err := trader.Run(ctx); err != nil { return err } type KChanEx struct { KChan chan types.KLine Exchange *backtest.Exchange } for _, session := range environ.Sessions() { backtestExchange := session.Exchange.(*backtest.Exchange) backtestExchange.InitMarketData() } var klineChans []KChanEx for _, session := range environ.Sessions() { exchange := session.Exchange.(*backtest.Exchange) c, err := exchange.GetMarketData() if err != nil { return err } klineChans = append(klineChans, KChanEx{KChan: c, Exchange: exchange}) } runCtx, cancelRun := context.WithCancel(ctx) go func() { defer cancelRun() for { count := len(klineChans) for _, kchanex := range klineChans { kLine, more := <-kchanex.KChan if more { kchanex.Exchange.ConsumeKLine(kLine) } else { if err := kchanex.Exchange.CloseMarketData(); err != nil { log.Errorf("%v", err) return } count-- } } if count == 0 { break } } }() cmdutil.WaitForSignal(runCtx, syscall.SIGINT, syscall.SIGTERM) log.Infof("shutting down trader...") shutdownCtx, cancelShutdown := context.WithDeadline(runCtx, time.Now().Add(10*time.Second)) trader.Graceful.Shutdown(shutdownCtx) cancelShutdown() log.SetLevel(log.InfoLevel) for _, session := range environ.Sessions() { backtestExchange := session.Exchange.(*backtest.Exchange) exchangeName := session.Exchange.Name().String() for symbol, trades := range session.Trades { market, ok := session.Market(symbol) if !ok { return fmt.Errorf("market not found: %s, %s", symbol, exchangeName) } calculator := &pnl.AverageCostCalculator{ TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(), Market: market, } startPrice, ok := session.StartPrice(symbol) if !ok { return fmt.Errorf("start price not found: %s, %s. run --sync first", symbol, exchangeName) } lastPrice, ok := session.LastPrice(symbol) if !ok { return fmt.Errorf("last price not found: %s, %s", symbol, exchangeName) } color.Green("%s %s PROFIT AND LOSS REPORT", strings.ToUpper(exchangeName), symbol) color.Green("===============================================") report := calculator.Calculate(symbol, trades.Trades, lastPrice) report.Print() initBalances := userConfig.Backtest.Account[exchangeName].Balances.BalanceMap() finalBalances := session.GetAccount().Balances() log.Infof("INITIAL BALANCES:") initBalances.Print() log.Infof("FINAL BALANCES:") finalBalances.Print() if jsonOutputEnabled { result := BackTestReport{ Symbol: symbol, LastPrice: lastPrice, StartPrice: startPrice, PnLReport: report, InitialBalances: initBalances, FinalBalances: finalBalances, } jsonOutput, err := json.MarshalIndent(&result, "", " ") if err != nil { return err } if err := ioutil.WriteFile(filepath.Join(outputDirectory, symbol+".json"), jsonOutput, 0644); err != nil { return err } } initQuoteAsset := inQuoteAsset(initBalances, market, startPrice) finalQuoteAsset := inQuoteAsset(finalBalances, market, lastPrice) log.Infof("INITIAL ASSET IN %s ~= %s %s (1 %s = %v)", market.QuoteCurrency, market.FormatQuantity(initQuoteAsset), market.QuoteCurrency, market.BaseCurrency, startPrice) log.Infof("FINAL ASSET IN %s ~= %s %s (1 %s = %v)", market.QuoteCurrency, market.FormatQuantity(finalQuoteAsset), market.QuoteCurrency, market.BaseCurrency, lastPrice) if report.Profit.Sign() > 0 { color.Green("REALIZED PROFIT: +%v %s", report.Profit, market.QuoteCurrency) } else { color.Red("REALIZED PROFIT: %v %s", report.Profit, market.QuoteCurrency) } if report.UnrealizedProfit.Sign() > 0 { color.Green("UNREALIZED PROFIT: +%v %s", report.UnrealizedProfit, market.QuoteCurrency) } else { color.Red("UNREALIZED PROFIT: %v %s", report.UnrealizedProfit, market.QuoteCurrency) } if finalQuoteAsset.Compare(initQuoteAsset) > 0 { color.Green("ASSET INCREASED: +%v %s (+%s)", finalQuoteAsset.Sub(initQuoteAsset), market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2)) } else { color.Red("ASSET DECREASED: %v %s (%s)", finalQuoteAsset.Sub(initQuoteAsset), market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2)) } if wantBaseAssetBaseline { if lastPrice.Compare(startPrice) > 0 { color.Green("%s BASE ASSET PERFORMANCE: +%s (= (%s - %s) / %s)", market.BaseCurrency, lastPrice.Sub(startPrice).Div(startPrice).FormatPercentage(2), lastPrice.FormatString(2), startPrice.FormatString(2), startPrice.FormatString(2)) } else { color.Red("%s BASE ASSET PERFORMANCE: %s (= (%s - %s) / %s)", market.BaseCurrency, lastPrice.Sub(startPrice).Div(startPrice).FormatPercentage(2), lastPrice.FormatString(2), startPrice.FormatString(2), startPrice.FormatString(2)) } } } } 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 } if err = environ.Init(ctx); 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: %v", 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") } } } if viper.GetBool("debug") { log.Infof("debug mode is enabled") log.SetLevel(log.DebugLevel) } if viper.GetBool("metrics") { http.Handle("/metrics", promhttp.Handler()) go func() { port := viper.GetString("metrics-port") log.Infof("starting metrics server at :%s", port) err := http.ListenAndServe(":"+port, nil) if err != nil { log.WithError(err).Errorf("metrics server error") } }() } 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 --session=[exchange_name] --symbol=[pair_name] [--since=yyyy/mm/dd]", Short: "sync trades and orders 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 } 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 } sessionName, err := cmd.Flags().GetString("session") if err != nil { return err } symbol, err := cmd.Flags().GetString("symbol") if err != nil { return err } var ( // default sync start time defaultSyncStartTime = time.Now().AddDate(-1, 0, 0) ) var syncStartTime = defaultSyncStartTime if userConfig.Sync != nil && userConfig.Sync.Since != nil { syncStartTime = userConfig.Sync.Since.Time() } if len(since) > 0 { syncStartTime, err = time.ParseInLocation("2006-01-02", since, time.Local) if err != nil { return err } } environ.SetSyncStartTime(syncStartTime) // syncSymbols is the symbol list to sync var syncSymbols []string if userConfig.Sync != nil && len(userConfig.Sync.Symbols) > 0 { syncSymbols = userConfig.Sync.Symbols } if len(symbol) > 0 { syncSymbols = []string{symbol} } var selectedSessions []string if userConfig.Sync != nil && len(userConfig.Sync.Sessions) > 0 { selectedSessions = userConfig.Sync.Sessions } if len(sessionName) > 0 { selectedSessions = []string{sessionName} } sessions := environ.SelectSessions(selectedSessions...) for _, session := range sessions { if err := environ.SyncSession(ctx, session, syncSymbols...); 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 %v %s [%s]", record.Time, record.Amount, record.Asset, record.Status) case types.Withdraw: logrus.Infof("%s: ---> WITHDRAW %v %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: %v", asset, quantity) } for asset, quantity := range stats.TotalWithdraw { logrus.Infof("total %s withdraw: %v", asset, quantity) } for asset, quantity := range stats.BaselineBalance { logrus.Infof("baseline %s balance: %v", asset, quantity) } return nil }, }
Functions ¶
func BootstrapBacktestEnvironment ¶ added in v1.15.5
func BootstrapEnvironment ¶ added in v1.11.0
Types ¶
type BackTestReport ¶ added in v1.28.0
type BackTestReport struct { Symbol string `json:"symbol,omitempty"` LastPrice fixedpoint.Value `json:"lastPrice,omitempty"` StartPrice fixedpoint.Value `json:"startPrice,omitempty"` PnLReport *pnl.AverageCostPnlReport `json:"pnlReport,omitempty"` InitialBalances types.BalanceMap `json:"initialBalances,omitempty"` FinalBalances types.BalanceMap `json:"finalBalances,omitempty"` }
type BaselineStats ¶
type BaselineStats struct { Asset string TotalDeposit map[string]fixedpoint.Value TotalWithdraw map[string]fixedpoint.Value BaselineBalance map[string]fixedpoint.Value }
Source Files ¶
Click to show internal directories.
Click to hide internal directories.