diff --git a/config/etf.yaml b/config/etf.yaml new file mode 100644 index 000000000..9677edc0e --- /dev/null +++ b/config/etf.yaml @@ -0,0 +1,11 @@ +exchangeStrategies: +- on: max + etf: + duration: 24h + totalAmount: 200.0 + index: + BTCUSDT: 5% + LTCUSDT: 15% + ETHUSDT: 30% + LINKUSDT: 20% + DOTUSDT: 30% diff --git a/pkg/bbgo/moving_average_settings.go b/pkg/bbgo/moving_average_settings.go new file mode 100644 index 000000000..2b1f209bb --- /dev/null +++ b/pkg/bbgo/moving_average_settings.go @@ -0,0 +1,47 @@ +package bbgo + +import ( + "fmt" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/strategy/schedule" + "github.com/c9s/bbgo/pkg/types" +) + +type MovingAverageSettings struct { + Type string `json:"type"` + Interval types.Interval `json:"interval"` + Window int `json:"window"` + + Side *types.SideType `json:"side"` + Quantity *fixedpoint.Value `json:"quantity"` + Amount *fixedpoint.Value `json:"amount"` +} + +func (settings MovingAverageSettings) IntervalWindow() types.IntervalWindow { + var window = 99 + if settings.Window > 0 { + window = settings.Window + } + + return types.IntervalWindow{ + Interval: settings.Interval, + Window: window, + } +} + +func (settings *MovingAverageSettings) Indicator(indicatorSet *StandardIndicatorSet) (inc schedule.Float64Indicator, err error) { + var iw = settings.IntervalWindow() + + switch settings.Type { + case "SMA": + inc = indicatorSet.SMA(iw) + + case "EWMA", "EMA": + inc = indicatorSet.EWMA(iw) + + default: + return nil, fmt.Errorf("unsupported moving average type: %s", settings.Type) + } + + return inc, nil +} diff --git a/pkg/cmd/builtin.go b/pkg/cmd/builtin.go index 092bfc705..74784717f 100644 --- a/pkg/cmd/builtin.go +++ b/pkg/cmd/builtin.go @@ -4,6 +4,7 @@ package cmd import ( _ "github.com/c9s/bbgo/pkg/strategy/bollgrid" _ "github.com/c9s/bbgo/pkg/strategy/buyandhold" + _ "github.com/c9s/bbgo/pkg/strategy/etf" _ "github.com/c9s/bbgo/pkg/strategy/flashcrash" _ "github.com/c9s/bbgo/pkg/strategy/gap" _ "github.com/c9s/bbgo/pkg/strategy/grid" diff --git a/pkg/strategy/etf/strategy.go b/pkg/strategy/etf/strategy.go new file mode 100644 index 000000000..bb159777e --- /dev/null +++ b/pkg/strategy/etf/strategy.go @@ -0,0 +1,106 @@ +package etf + +import ( + "context" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "time" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "etf" + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + Market types.Market + + Notifiability *bbgo.Notifiability + + + TotalAmount fixedpoint.Value `json:"totalAmount,omitempty"` + + // Interval is the period that you want to submit order + Duration types.Duration `json:"duration"` + + Index map[string]fixedpoint.Value `json:"index"` +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { +} + +func (s *Strategy) Validate() error { + if s.TotalAmount == 0 { + return errors.New("amount can not be empty") + } + + return nil +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + go func() { + ticker := time.NewTicker(s.Duration.Duration()) + defer ticker.Stop() + + s.Notifiability.Notify("ETF orders will be executed every %s", s.Duration.Duration().String()) + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + totalAmount := s.TotalAmount + for symbol, ratio := range s.Index { + amount := totalAmount.Mul(ratio) + + ticker, err := session.Exchange.QueryTicker(ctx, symbol) + if err != nil { + log.WithError(err).Error("query ticker error") + } + + askPrice := fixedpoint.NewFromFloat(ticker.Sell) + quantity := askPrice.Div(amount) + + // execute orders + quoteBalance, ok := session.Account.Balance(s.Market.QuoteCurrency) + if !ok { + return + } + if quoteBalance.Available < amount { + s.Notifiability.Notify("Quote balance %s is not enough: %f < %f", s.Market.QuoteCurrency, quoteBalance.Available.Float64(), amount.Float64()) + return + } + + s.Notifiability.Notify("Submitting etf order %s quantity %f at price %f (index ratio %f %%)", + symbol, + quantity.Float64(), + askPrice.Float64(), + ratio.Float64()*100.0) + _, err = orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeMarket, + Quantity: quantity.Float64(), + }) + + if err != nil { + log.WithError(err).Error("submit order error") + } + + } + } + } + }() + + return nil +} diff --git a/pkg/strategy/schedule/strategy.go b/pkg/strategy/schedule/strategy.go index 95c11c9b4..0ba01f1c8 100644 --- a/pkg/strategy/schedule/strategy.go +++ b/pkg/strategy/schedule/strategy.go @@ -2,8 +2,6 @@ package schedule import ( "context" - "fmt" - "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -23,45 +21,6 @@ type Float64Indicator interface { Last() float64 } -type MovingAverageSettings struct { - Type string `json:"type"` - Interval types.Interval `json:"interval"` - Window int `json:"window"` - - Side *types.SideType `json:"side"` - Quantity *fixedpoint.Value `json:"quantity"` - Amount *fixedpoint.Value `json:"amount"` -} - -func (settings MovingAverageSettings) IntervalWindow() types.IntervalWindow { - var window = 99 - if settings.Window > 0 { - window = settings.Window - } - - return types.IntervalWindow{ - Interval: settings.Interval, - Window: window, - } -} - -func (settings *MovingAverageSettings) Indicator(indicatorSet *bbgo.StandardIndicatorSet) (inc Float64Indicator, err error) { - var iw = settings.IntervalWindow() - - switch settings.Type { - case "SMA": - inc = indicatorSet.SMA(iw) - - case "EWMA", "EMA": - inc = indicatorSet.EWMA(iw) - - default: - return nil, fmt.Errorf("unsupported moving average type: %s", settings.Type) - } - - return inc, nil -} - type Strategy struct { Market types.Market @@ -85,9 +44,9 @@ type Strategy struct { Amount fixedpoint.Value `json:"amount,omitempty"` - BelowMovingAverage *MovingAverageSettings `json:"belowMovingAverage,omitempty"` + BelowMovingAverage *bbgo.MovingAverageSettings `json:"belowMovingAverage,omitempty"` - AboveMovingAverage *MovingAverageSettings `json:"aboveMovingAverage,omitempty"` + AboveMovingAverage *bbgo.MovingAverageSettings `json:"aboveMovingAverage,omitempty"` } func (s *Strategy) ID() string { @@ -218,7 +177,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } - s.Notifiability.Notify("Submitting scheduled order %s quantity %f", s.Symbol, quantity.Float64()) + s.Notifiability.Notify("Submitting scheduled order %s quantity %f at price %f", s.Symbol, quantity.Float64(), closePrice.Float64()) _, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, Side: side,