Merge pull request #24 from c9s/feature/pnl-config

feature: PnL API and wrapper binary compiler
This commit is contained in:
Yo-An Lin 2020-10-24 16:29:48 +08:00 committed by GitHub
commit 870bae19d3
14 changed files with 378 additions and 186 deletions

3
.gitignore vendored
View File

@ -20,3 +20,6 @@
/.env.local
/.env.*.local
/build
/bbgow

View File

@ -1,11 +1,9 @@
package main
import (
"github.com/c9s/bbgo/cmd"
_ "github.com/go-sql-driver/mysql"
"github.com/c9s/bbgo/pkg/cmd"
)
func main() {
cmd.Run()
cmd.Execute()
}

View File

@ -1,102 +0,0 @@
package cmd
import (
"context"
"syscall"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
"github.com/c9s/bbgo/pkg/config"
"github.com/c9s/bbgo/pkg/notifier/slacknotifier"
"github.com/c9s/bbgo/pkg/slack/slacklog"
_ "github.com/c9s/bbgo/pkg/strategy/buyandhold"
)
var errSlackTokenUndefined = errors.New("slack token is not defined.")
func init() {
runCmd.Flags().String("config", "config/bbgo.yaml", "strategy config file")
runCmd.Flags().String("since", "", "pnl since time")
RootCmd.AddCommand(runCmd)
}
var runCmd = &cobra.Command{
Use: "run",
Short: "run strategies",
// SilenceUsage is an option to silence usage when an error occurs.
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 not given")
}
userConfig, err := config.Load(configFile)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
slackToken := viper.GetString("slack-token")
if len(slackToken) == 0 {
return errSlackTokenUndefined
}
log.AddHook(slacklog.NewLogHook(slackToken, viper.GetString("slack-error-channel")))
var notifier = slacknotifier.New(slackToken, viper.GetString("slack-channel"))
db, err := cmdutil.ConnectMySQL()
if err != nil {
return err
}
environ := bbgo.NewDefaultEnvironment(db)
environ.ReportTrade(notifier)
trader := bbgo.NewTrader(environ)
for _, entry := range userConfig.ExchangeStrategies {
for _, mount := range entry.Mounts {
log.Infof("attaching strategy %T on %s...", entry.Strategy, mount)
trader.AttachStrategyOn(mount, entry.Strategy)
}
}
for _, strategy := range userConfig.CrossExchangeStrategies {
log.Infof("attaching strategy %T", strategy)
trader.AttachCrossExchangeStrategy(strategy)
}
// TODO: load these from config file
trader.ReportPnL(notifier).
AverageCostBySymbols("BTCUSDT", "BNBUSDT").
Of("binance").When("@daily", "@hourly")
trader.ReportPnL(notifier).
AverageCostBySymbols("MAXUSDT").
Of("max").When("@daily", "@hourly")
err = trader.Run(ctx)
if err != nil {
return err
}
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
return err
},
}

View File

@ -1,4 +1,6 @@
---
imports:
- github.com/c9s/bbgo/pkg/strategy/buyandhold
notifications:
slack:
defaultChannel: "bbgo"
@ -11,6 +13,15 @@ reportTrades:
"bnbusdt": "bbgo-bnbusdt"
"sxpusdt": "bbgo-sxpusdt"
reportPnL:
- averageCostBySymbols:
- "BTCUSDT"
- "BNBUSDT"
of: binance
when:
- "@daily"
- "@hourly"
sessions:
max:
exchange: max
@ -26,5 +37,5 @@ exchangeStrategies:
buyandhold:
symbol: "BTCUSDT"
interval: "1m"
baseQuantity: 0.1
minDropPercentage: -0.05
baseQuantity: 0.01
minDropPercentage: -0.02

View File

@ -2,7 +2,6 @@ package pnl
import (
"strings"
"time"
"github.com/sirupsen/logrus"
@ -10,8 +9,6 @@ import (
)
type AverageCostCalculator struct {
Symbol string
StartTime time.Time
TradingFeeCurrency string
}
@ -89,9 +86,9 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
return &AverageCostPnlReport{
Symbol: symbol,
StartTime: c.StartTime,
CurrentPrice: currentPrice,
NumTrades: len(trades),
StartTime: trades[0].Time,
BidVolume: bidVolume,
AskVolume: askVolume,

View File

@ -2,7 +2,6 @@ package bbgo
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/robfig/cron/v3"
@ -96,7 +95,6 @@ func (reporter *AverageCostPnLReporter) Run() {
for _, sessionName := range reporter.Sessions {
session := reporter.environment.sessions[sessionName]
calculator := &pnl.AverageCostCalculator{
StartTime: time.Time{},
TradingFeeCurrency: session.Exchange.PlatformFeeCurrency(),
}

View File

@ -8,7 +8,7 @@ import (
"path"
"github.com/c9s/goose"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -44,32 +44,32 @@ var MigrateCmd = &cobra.Command{
sourceDir := bbgo.SourceDir()
migrationDir := path.Join(sourceDir, "migrations")
log.Infof("creating dir: %s", dotDir)
logrus.Infof("creating dir: %s", dotDir)
if err := os.Mkdir(dotDir, 0777); err != nil {
// return err
}
log.Infof("checking %s", sourceDir)
logrus.Infof("checking %s", sourceDir)
_, err = os.Stat(sourceDir)
if err != nil {
log.Infof("cloning bbgo source into %s ...", sourceDir)
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 {
log.Infof("updating: %s ...", sourceDir)
logrus.Infof("updating: %s ...", sourceDir)
cmd := exec.CommandContext(ctx, "git", "--work-tree", sourceDir, "pull")
if err := cmd.Run(); err != nil {
return err
}
}
log.Infof("using migration file dir: %s", migrationDir)
logrus.Infof("using migration file dir: %s", migrationDir)
command := args[0]
if err := goose.Run(command, db, migrationDir); err != nil {
log.Fatalf("goose run: %v", err)
logrus.Fatalf("goose run: %v", err)
}
defer db.Close()

View File

@ -5,7 +5,7 @@ import (
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/c9s/bbgo/pkg/accounting"
@ -16,13 +16,13 @@ import (
)
func init() {
pnlCmd.Flags().String("exchange", "", "target exchange")
pnlCmd.Flags().String("symbol", "BTCUSDT", "trading symbol")
pnlCmd.Flags().String("since", "", "pnl since time")
RootCmd.AddCommand(pnlCmd)
PnLCmd.Flags().String("exchange", "", "target exchange")
PnLCmd.Flags().String("symbol", "BTCUSDT", "trading symbol")
PnLCmd.Flags().String("since", "", "pnl since time")
RootCmd.AddCommand(PnLCmd)
}
var pnlCmd = &cobra.Command{
var PnLCmd = &cobra.Command{
Use: "pnl",
Short: "pnl calculator",
SilenceUsage: true,
@ -75,7 +75,7 @@ var pnlCmd = &cobra.Command{
tradeService := &service.TradeService{DB: db}
tradeSync := &service.TradeSync{Service: tradeService}
log.Info("syncing trades from exchange...")
logrus.Info("syncing trades from exchange...")
if err := tradeSync.Sync(ctx, exchange, symbol, startTime); err != nil {
return err
}
@ -83,7 +83,7 @@ var pnlCmd = &cobra.Command{
var trades []types.Trade
tradingFeeCurrency := exchange.PlatformFeeCurrency()
if strings.HasPrefix(symbol, tradingFeeCurrency) {
log.Infof("loading all trading fee currency related trades: %s", symbol)
logrus.Infof("loading all trading fee currency related trades: %s", symbol)
trades, err = tradeService.QueryForTradingFeeCurrency(symbol, tradingFeeCurrency)
} else {
trades, err = tradeService.Query(symbol)
@ -93,7 +93,7 @@ var pnlCmd = &cobra.Command{
return err
}
log.Infof("%d trades loaded", len(trades))
logrus.Infof("%d trades loaded", len(trades))
stockManager := &accounting.StockDistribution{
Symbol: symbol,
@ -105,14 +105,13 @@ var pnlCmd = &cobra.Command{
return err
}
log.Infof("found checkpoints: %+v", checkpoints)
log.Infof("stock: %f", stockManager.Stocks.Quantity())
logrus.Infof("found checkpoints: %+v", checkpoints)
logrus.Infof("stock: %f", stockManager.Stocks.Quantity())
currentPrice, err := exchange.QueryAveragePrice(ctx, symbol)
calculator := &pnl.AverageCostCalculator{
TradingFeeCurrency: tradingFeeCurrency,
StartTime: startTime,
}
report := calculator.Calculate(symbol, trades, currentPrice)

View File

@ -6,12 +6,14 @@ import (
"strings"
"time"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/lestrrat-go/file-rotatelogs"
"github.com/rifflock/lfshook"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
"github.com/x-cray/logrus-prefixed-formatter"
_ "github.com/go-sql-driver/mysql"
)
var RootCmd = &cobra.Command{
@ -34,7 +36,7 @@ func init() {
// the command it's assigned to as well as every command under that command.
// For global flags, assign a flag as a persistent flag on the root.
RootCmd.PersistentFlags().String("slack-token", "", "slack token")
RootCmd.PersistentFlags().String("slack-trading-channel", "dev-bbgo", "slack trading channel")
RootCmd.PersistentFlags().String("slack-channel", "dev-bbgo", "slack trading channel")
RootCmd.PersistentFlags().String("slack-error-channel", "bbgo-error", "slack error channel")
RootCmd.PersistentFlags().String("binance-api-key", "", "binance api key")
@ -44,7 +46,7 @@ func init() {
RootCmd.PersistentFlags().String("max-api-secret", "", "max api secret")
}
func Run() {
func Execute() {
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
// Enable environment variable binding, the env vars are not overloaded yet.

192
pkg/cmd/run.go Normal file
View File

@ -0,0 +1,192 @@
package cmd
import (
"bytes"
"context"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"syscall"
"text/template"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
"github.com/c9s/bbgo/pkg/config"
"github.com/c9s/bbgo/pkg/notifier/slacknotifier"
"github.com/c9s/bbgo/pkg/slack/slacklog"
// import built-in strategies
_ "github.com/c9s/bbgo/pkg/strategy/buyandhold"
)
var errSlackTokenUndefined = errors.New("slack token is not defined.")
func init() {
RunCmd.Flags().Bool("no-compile", false, "do not compile wrapper binary")
RunCmd.Flags().String("config", "config/bbgo.yaml", "strategy config file")
RunCmd.Flags().String("since", "", "pnl since time")
RootCmd.AddCommand(RunCmd)
}
var runTemplate = template.Must(template.New("main").Parse(`package main
// DO NOT MODIFY THIS FILE. THIS FILE IS GENERATED FOR IMPORTING STRATEGIES
import (
"github.com/c9s/bbgo/pkg/cmd"
{{- range .Imports }}
_ "{{ . }}"
{{- end }}
)
func main() {
cmd.Execute()
}
`))
func compileRunFile(filepath string, config *config.Config) error {
var buf = bytes.NewBuffer(nil)
if err := runTemplate.Execute(buf, config); err != nil {
return err
}
return ioutil.WriteFile(filepath, buf.Bytes(), 0644)
}
func runConfig(ctx context.Context, config *config.Config) error {
slackToken := viper.GetString("slack-token")
if len(slackToken) == 0 {
return errSlackTokenUndefined
}
log.AddHook(slacklog.NewLogHook(slackToken, viper.GetString("slack-error-channel")))
var notifier = slacknotifier.New(slackToken, viper.GetString("slack-channel"))
db, err := cmdutil.ConnectMySQL()
if err != nil {
return err
}
environ := bbgo.NewDefaultEnvironment(db)
environ.ReportTrade(notifier)
trader := bbgo.NewTrader(environ)
for _, entry := range config.ExchangeStrategies {
for _, mount := range entry.Mounts {
log.Infof("attaching strategy %T on %s...", entry.Strategy, mount)
trader.AttachStrategyOn(mount, entry.Strategy)
}
}
for _, strategy := range config.CrossExchangeStrategies {
log.Infof("attaching strategy %T", strategy)
trader.AttachCrossExchangeStrategy(strategy)
}
for _, report := range config.PnLReporters {
if len(report.AverageCostBySymbols) > 0 {
trader.ReportPnL(notifier).
AverageCostBySymbols(report.AverageCostBySymbols...).
Of(report.Of...).
When(report.When...)
} else {
return errors.Errorf("unsupported PnL reporter: %+v", report)
}
}
return trader.Run(ctx)
}
var RunCmd = &cobra.Command{
Use: "run",
Short: "run strategies from config file",
// SilenceUsage is an option to silence usage when an error occurs.
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 := config.Load(configFile)
if err != nil {
return err
}
if noCompile {
if err := runConfig(ctx, userConfig); err != nil {
return err
}
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
return nil
} else {
buildDir := filepath.Join("build", "bbgow")
if _, err := os.Stat(buildDir); os.IsNotExist(err) {
if err := os.MkdirAll(buildDir, 0777); err != nil {
return errors.Wrapf(err, "can not create build directory: %s", buildDir)
}
}
mainFile := filepath.Join(buildDir, "main.go")
if err := compileRunFile(mainFile, userConfig); err != nil {
return errors.Wrap(err, "compile error")
}
// TODO: use "\" for Windows
cwd, err := os.Getwd()
if err != nil {
return err
}
buildTarget := filepath.Join(cwd, buildDir)
log.Infof("building binary from %s...", buildTarget)
buildCmd := exec.CommandContext(ctx, "go", "build", "-tags", "wrapper", "-o", "bbgow", buildTarget)
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
if err := buildCmd.Run(); err != nil {
return err
}
var flagsArgs = []string{"run", "--no-compile"}
cmd.Flags().Visit(func(flag *flag.Flag) {
flagsArgs = append(flagsArgs, flag.Name, flag.Value.String())
})
flagsArgs = append(flagsArgs, args...)
executePath := filepath.Join(cwd, "bbgow")
runCmd := exec.CommandContext(ctx, executePath, flagsArgs...)
runCmd.Stdout = os.Stdout
runCmd.Stderr = os.Stderr
if err := runCmd.Run(); err != nil {
return err
}
}
return nil
},
}

View File

@ -5,7 +5,7 @@ import (
"sort"
"time"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
@ -13,10 +13,10 @@ import (
)
func init() {
transferHistoryCmd.Flags().String("exchange", "", "target exchange")
transferHistoryCmd.Flags().String("asset", "", "trading symbol")
transferHistoryCmd.Flags().String("since", "", "since time")
RootCmd.AddCommand(transferHistoryCmd)
TransferHistoryCmd.Flags().String("exchange", "", "target exchange")
TransferHistoryCmd.Flags().String("asset", "", "trading symbol")
TransferHistoryCmd.Flags().String("since", "", "since time")
RootCmd.AddCommand(TransferHistoryCmd)
}
type timeRecord struct {
@ -38,7 +38,7 @@ func (p timeSlice) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
var transferHistoryCmd = &cobra.Command{
var TransferHistoryCmd = &cobra.Command{
Use: "transfer-history",
Short: "show transfer history",
@ -116,28 +116,28 @@ var transferHistoryCmd = &cobra.Command{
switch record := record.Record.(type) {
case types.Deposit:
log.Infof("%s: %s <== (deposit) %f [%s]", record.Time, record.Asset, record.Amount, record.Status)
logrus.Infof("%s: %s <== (deposit) %f [%s]", record.Time, record.Asset, record.Amount, record.Status)
case types.Withdraw:
log.Infof("%s: %s ==> (withdraw) %f [%s]", record.ApplyTime, record.Asset, record.Amount, record.Status)
logrus.Infof("%s: %s ==> (withdraw) %f [%s]", record.ApplyTime, record.Asset, record.Amount, record.Status)
default:
log.Infof("unknown record: %+v", record)
logrus.Infof("unknown record: %+v", record)
}
}
stats := calBaselineStats(asset, deposits, withdraws)
for asset, quantity := range stats.TotalDeposit {
log.Infof("total %s deposit: %f", asset, quantity)
logrus.Infof("total %s deposit: %f", asset, quantity)
}
for asset, quantity := range stats.TotalWithdraw {
log.Infof("total %s withdraw: %f", asset, quantity)
logrus.Infof("total %s withdraw: %f", asset, quantity)
}
for asset, quantity := range stats.BaselineBalance {
log.Infof("baseline %s balance: %f", asset, quantity)
logrus.Infof("baseline %s balance: %f", asset, quantity)
}
return nil

View File

@ -16,67 +16,148 @@ type SingleExchangeStrategyConfig struct {
Strategy bbgo.SingleExchangeStrategy
}
type StringSlice []string
func (s *StringSlice) decode(a interface{}) error {
switch d := a.(type) {
case string:
*s = append(*s, d)
case []string:
*s = append(*s, d...)
case []interface{}:
for _, de := range d {
if err := s.decode(de); err != nil {
return err
}
}
default:
return errors.Errorf("unexpected type %T for StringSlice: %+v", d, d)
}
return nil
}
func (s *StringSlice) UnmarshalJSON(b []byte) error {
var a interface{}
var err = json.Unmarshal(b, &a)
if err != nil {
return err
}
return s.decode(a)
}
type PnLReporter struct {
AverageCostBySymbols StringSlice `json:"averageCostBySymbols"`
Of StringSlice `json:"of" yaml:"of"`
When StringSlice `json:"when" yaml:"when"`
}
type Config struct {
Imports []string `json:"imports" yaml:"imports"`
ExchangeStrategies []SingleExchangeStrategyConfig
CrossExchangeStrategies []bbgo.CrossExchangeStrategy
PnLReporters []PnLReporter `json:"reportPnL" yaml:"reportPnL"`
}
type Stash map[string]interface{}
func loadStash(configFile string) (Stash, error) {
config, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, err
}
func loadStash(config []byte) (Stash, error) {
stash := make(Stash)
if err := yaml.Unmarshal(config, stash); err != nil {
return nil, err
}
return stash, err
return stash, nil
}
func Load(configFile string) (*Config, error) {
var config Config
stash, err := loadStash(configFile)
content, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, err
}
strategies, err := loadExchangeStrategies(stash)
stash, err := loadStash(content)
if err != nil {
return nil, err
}
config.ExchangeStrategies = strategies
crossExchangeStrategies, err := loadCrossExchangeStrategies(stash)
if err != nil {
if err := loadImports(&config, stash); err != nil {
return nil, err
}
config.CrossExchangeStrategies = crossExchangeStrategies
if err := loadExchangeStrategies(&config, stash); err != nil {
return nil, err
}
if err := loadCrossExchangeStrategies(&config, stash); err != nil {
return nil, err
}
if err := loadReportPnL(&config, stash); err != nil {
return nil, err
}
return &config, nil
}
func loadCrossExchangeStrategies(stash Stash) (strategies []bbgo.CrossExchangeStrategy, err error) {
func loadImports(config *Config, stash Stash) error {
importStash, ok := stash["imports"]
if !ok {
return nil
}
imports, err := reUnmarshal(importStash, &config.Imports)
if err != nil {
return err
}
config.Imports = *imports.(*[]string)
return nil
}
func loadReportPnL(config *Config, stash Stash) error {
reporterStash, ok := stash["reportPnL"]
if !ok {
return nil
}
reporters, err := reUnmarshal(reporterStash, &config.PnLReporters)
if err != nil {
return err
}
config.PnLReporters = *(reporters.(*[]PnLReporter))
return nil
}
func loadCrossExchangeStrategies(config *Config, stash Stash) (err error) {
exchangeStrategiesConf, ok := stash["crossExchangeStrategies"]
if !ok {
return strategies, nil
return nil
}
if len(bbgo.LoadedCrossExchangeStrategies) == 0 {
return errors.New("no cross exchange strategy is registered")
}
configList, ok := exchangeStrategiesConf.([]interface{})
if !ok {
return nil, errors.New("expecting list in crossExchangeStrategies")
return errors.New("expecting list in crossExchangeStrategies")
}
for _, entry := range configList {
configStash, ok := entry.(Stash)
if !ok {
return nil, errors.Errorf("strategy config should be a map, given: %T %+v", entry, entry)
return errors.Errorf("strategy config should be a map, given: %T %+v", entry, entry)
}
for id, conf := range configStash {
@ -84,34 +165,36 @@ func loadCrossExchangeStrategies(stash Stash) (strategies []bbgo.CrossExchangeSt
if st, ok := bbgo.LoadedExchangeStrategies[id]; ok {
val, err := reUnmarshal(conf, st)
if err != nil {
return nil, err
return err
}
strategies = append(strategies, val.(bbgo.CrossExchangeStrategy))
config.CrossExchangeStrategies = append(config.CrossExchangeStrategies, val.(bbgo.CrossExchangeStrategy))
}
}
}
return strategies, nil
return nil
}
func loadExchangeStrategies(stash Stash) (strategies []SingleExchangeStrategyConfig, err error) {
func loadExchangeStrategies(config *Config, stash Stash) (err error) {
exchangeStrategiesConf, ok := stash["exchangeStrategies"]
if !ok {
return strategies, nil
// return nil, errors.New("exchangeStrategies is not defined")
return nil
}
if len(bbgo.LoadedExchangeStrategies) == 0 {
return errors.New("no exchange strategy is registered")
}
configList, ok := exchangeStrategiesConf.([]interface{})
if !ok {
return nil, errors.New("expecting list in exchangeStrategies")
return errors.New("expecting list in exchangeStrategies")
}
for _, entry := range configList {
configStash, ok := entry.(Stash)
if !ok {
return nil, errors.Errorf("strategy config should be a map, given: %T %+v", entry, entry)
return errors.Errorf("strategy config should be a map, given: %T %+v", entry, entry)
}
var mounts []string
@ -128,10 +211,10 @@ func loadExchangeStrategies(stash Stash) (strategies []SingleExchangeStrategyCon
if st, ok := bbgo.LoadedExchangeStrategies[id]; ok {
val, err := reUnmarshal(conf, st)
if err != nil {
return nil, err
return err
}
strategies = append(strategies, SingleExchangeStrategyConfig{
config.ExchangeStrategies = append(config.ExchangeStrategies, SingleExchangeStrategyConfig{
Mounts: mounts,
Strategy: val.(bbgo.SingleExchangeStrategy),
})
@ -139,7 +222,7 @@ func loadExchangeStrategies(stash Stash) (strategies []SingleExchangeStrategyCon
}
}
return strategies, nil
return nil
}
func reUnmarshal(conf interface{}, tpe interface{}) (interface{}, error) {

View File

@ -9,7 +9,7 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/buyandhold"
)
func TestLoadStrategies(t *testing.T) {
func TestLoadConfig(t *testing.T) {
type args struct {
configFile string
}

View File

@ -1,4 +1,6 @@
---
imports:
- github.com/c9s/bbgo/pkg/strategy/buyandhold
sessions:
max:
exchange: max
@ -9,9 +11,18 @@ sessions:
keyVar: BINANCE_API_KEY
secretVar: BINANCE_API_SECRET
exchangeStrategies:
- on: binance
buyandhold:
symbol: "BTCUSDT"
interval: "1m"
baseQuantity: 0.1
minDropPercentage: -0.05
- on: binance
buyandhold:
symbol: "BTCUSDT"
interval: "1m"
baseQuantity: 0.1
minDropPercentage: -0.05
reportPnL:
- averageCostBySymbols:
- "BTCUSDT"
- "BNBUSDT"
of: binance
when:
- "@daily"
- "@hourly"