mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-21 22:43:52 +00:00
Merge pull request #24 from c9s/feature/pnl-config
feature: PnL API and wrapper binary compiler
This commit is contained in:
commit
870bae19d3
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -20,3 +20,6 @@
|
|||
|
||||
/.env.local
|
||||
/.env.*.local
|
||||
|
||||
/build
|
||||
/bbgow
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
102
cmd/run.go
102
cmd/run.go
|
@ -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
|
||||
},
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
192
pkg/cmd/run.go
Normal 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
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
23
pkg/config/testdata/strategy.yaml
vendored
23
pkg/config/testdata/strategy.yaml
vendored
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user