diff --git a/config/binance-margin.yaml b/config/binance-margin.yaml index 169f2e58f..61cb4aea5 100644 --- a/config/binance-margin.yaml +++ b/config/binance-margin.yaml @@ -1,11 +1,23 @@ --- sessions: + # cross margin + binance_margin: + exchange: binance + margin: true + + # isolated margin binance_margin_linkusdt: exchange: binance margin: true isolatedMargin: true isolatedMarginSymbol: LINKUSDT + binance_margin_dotusdt: + exchange: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: DOTUSDT + exchangeStrategies: - on: binance_margin_linkusdt diff --git a/pkg/cmd/margin.go b/pkg/cmd/margin.go new file mode 100644 index 000000000..74d4b8735 --- /dev/null +++ b/pkg/cmd/margin.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "time" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +var selectedSession *bbgo.ExchangeSession + +func init() { + marginLoansCmd.Flags().String("asset", "", "asset") + marginLoansCmd.Flags().String("session", "", "exchange session name") + marginCmd.AddCommand(marginLoansCmd) + + RootCmd.AddCommand(marginCmd) +} + +// go run ./cmd/bbgo margin --session=binance +var marginCmd = &cobra.Command{ + Use: "margin", + Short: "margin related history", + SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := cobraLoadDotenv(cmd, args); err != nil { + return err + } + + if err := cobraLoadConfig(cmd, args); err != nil { + return err + } + + // ctx := context.Background() + environ := bbgo.NewEnvironment() + + if userConfig == nil { + return errors.New("user config is not loaded") + } + + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { + return err + } + + sessionName, err := cmd.Flags().GetString("session") + if err != nil { + return err + } + + session, ok := environ.Session(sessionName) + if !ok { + return fmt.Errorf("session %s not found", sessionName) + } + + selectedSession = session + return nil + }, +} + +// go run ./cmd/bbgo margin loans --session=binance +var marginLoansCmd = &cobra.Command{ + Use: "loans --session=SESSION_NAME --asset=ASSET", + Short: "query loans history", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + asset, err := cmd.Flags().GetString("asset") + if err != nil { + return fmt.Errorf("can't get the symbol from flags: %w", err) + } + + if selectedSession == nil { + return errors.New("session is not set") + } + + marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistory) + if !ok { + return fmt.Errorf("exchange %s does not support MarginHistory service", selectedSession.ExchangeName) + } + + now := time.Now() + startTime := now.AddDate(0, -5, 0) + endTime := now + loans, err := marginHistoryService.QueryLoanHistory(ctx, asset, &startTime, &endTime) + if err != nil { + return err + } + + log.Infof("%d loans", len(loans)) + for _, loan := range loans { + log.Infof("LOAN %+v", loan) + } + + return nil + }, +} diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 687f9a4bf..694440e71 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -32,24 +32,10 @@ var RootCmd = &cobra.Command{ SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - disableDotEnv, err := cmd.Flags().GetBool("no-dotenv") - if err != nil { + if err := cobraLoadDotenv(cmd, args) ; 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) @@ -67,39 +53,63 @@ var RootCmd = &cobra.Command{ }() } - configFile, err := cmd.Flags().GetString("config") - if err != nil { - return errors.Wrapf(err, "failed to get the config flag") - } - - // load config file nicely - if len(configFile) > 0 { - // if config file exists, use the config loaded from the config file. - // otherwise, use a empty config object - if _, err := os.Stat(configFile); err == nil { - // load successfully - 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) { - // config file doesn't exist, we should use the empty config - userConfig = &bbgo.Config{} - } else { - // other error - return errors.Wrapf(err, "config file load error: %s", configFile) - } - } - - return nil + return cobraLoadConfig(cmd, args) }, - RunE: func(cmd *cobra.Command, args []string) error { return nil }, } +func cobraLoadDotenv(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 +} + +func cobraLoadConfig(cmd *cobra.Command, args []string) error { + configFile, err := cmd.Flags().GetString("config") + if err != nil { + return errors.Wrapf(err, "failed to get the config flag") + } + + // load config file nicely + if len(configFile) > 0 { + // if config file exists, use the config loaded from the config file. + // otherwise, use an empty config object + if _, err := os.Stat(configFile); err == nil { + // load successfully + 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) { + // config file doesn't exist, we should use the empty config + userConfig = &bbgo.Config{} + } else { + // other error + return errors.Wrapf(err, "config file load error: %s", configFile) + } + } + + return nil +} + func init() { RootCmd.PersistentFlags().Bool("debug", false, "debug mode") RootCmd.PersistentFlags().Bool("metrics", false, "enable prometheus metrics") diff --git a/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go index e6c481cfe..6d9a13448 100644 --- a/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go +++ b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go @@ -4,6 +4,9 @@ import ( "time" "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) // RepayStatus one of PENDING (pending execution), CONFIRMED (successfully loaned), FAILED (execution failed, nothing happened to your account); @@ -16,14 +19,14 @@ const ( ) type MarginRepayRecord struct { - IsolatedSymbol string `json:"isolatedSymbol"` - Amount string `json:"amount"` - Asset string `json:"asset"` - Interest string `json:"interest"` - Principal string `json:"principal"` - Status string `json:"status"` - Timestamp int64 `json:"timestamp"` - TxId int64 `json:"txId"` + IsolatedSymbol string `json:"isolatedSymbol"` + Amount fixedpoint.Value `json:"amount"` + Asset string `json:"asset"` + Interest fixedpoint.Value `json:"interest"` + Principal fixedpoint.Value `json:"principal"` + Status string `json:"status"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` + TxId uint64 `json:"txId"` } //go:generate requestgen -method GET -url "/sapi/v1/margin/repay" -type GetMarginRepayHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginRepayRecord diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 5a1db26fa..bd7632ddd 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -106,6 +106,8 @@ func New(key, secret string) *Exchange { var err error if len(key) > 0 && len(secret) > 0 { + client2.Auth(key, secret) + timeSetter.Do(func() { _, err = client.NewSetServerTimeService().Do(context.Background()) if err != nil { diff --git a/pkg/exchange/binance/margin_history.go b/pkg/exchange/binance/margin_history.go index 6eb679369..544bfda20 100644 --- a/pkg/exchange/binance/margin_history.go +++ b/pkg/exchange/binance/margin_history.go @@ -4,16 +4,32 @@ import ( "context" "time" + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" "github.com/c9s/bbgo/pkg/types" ) func (e *Exchange) QueryLoanHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginLoanRecord, error) { req := e.client2.NewGetMarginLoanHistoryRequest() req.Asset(asset) + req.Size(100) if startTime != nil { req.StartTime(*startTime) + + // 6 months + if time.Since(*startTime) > time.Hour*24*30*6 { + req.Archived(true) + } } + + if startTime != nil && endTime != nil { + duration := endTime.Sub(*startTime) + if duration > time.Hour*24*30 { + t := startTime.Add(time.Hour * 24 * 30) + endTime = &t + } + } + if endTime != nil { req.EndTime(*endTime) } @@ -22,18 +38,51 @@ func (e *Exchange) QueryLoanHistory(ctx context.Context, asset string, startTime req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) } - loans, err := req.Do(ctx) - _ = loans - return nil, err + records, err := req.Do(ctx) + if err != nil { + return nil, err + } + + var loans []types.MarginLoanRecord + for _, record := range records { + loans = append(loans, toGlobalLoan(record)) + } + + return loans, err +} + +func toGlobalLoan(record binanceapi.MarginLoanRecord) types.MarginLoanRecord { + return types.MarginLoanRecord{ + TransactionID: uint64(record.TxId), + Asset: record.Asset, + Principle: record.Principal, + Time: types.Time(record.Timestamp), + IsolatedSymbol: record.IsolatedSymbol, + } } func (e *Exchange) QueryRepayHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginRepayRecord, error) { req := e.client2.NewGetMarginRepayHistoryRequest() req.Asset(asset) + req.Size(100) if startTime != nil { req.StartTime(*startTime) + + // 6 months + if time.Since(*startTime) > time.Hour*24*30*6 { + req.Archived(true) + } } + + if startTime != nil && endTime != nil { + duration := endTime.Sub(*startTime) + if duration > time.Hour*24*30 { + t := startTime.Add(time.Hour * 24 * 30) + endTime = &t + } + } + if endTime != nil { req.EndTime(*endTime) } @@ -42,8 +91,24 @@ func (e *Exchange) QueryRepayHistory(ctx context.Context, asset string, startTim req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) } - _, err := req.Do(ctx) - return nil, err + records, err := req.Do(ctx) + + var repays []types.MarginRepayRecord + for _, record := range records { + repays = append(repays, toGlobalRepay(record)) + } + + return repays, err +} + +func toGlobalRepay(record binanceapi.MarginRepayRecord) types.MarginRepayRecord { + return types.MarginRepayRecord{ + TransactionID: record.TxId, + Asset: record.Asset, + Principle: record.Principal, + Time: types.Time(record.Timestamp), + IsolatedSymbol: record.IsolatedSymbol, + } } func (e *Exchange) QueryLiquidationHistory(ctx context.Context, startTime, endTime *time.Time) ([]types.MarginLiquidationRecord, error) {