diff --git a/pkg/bbgo/exit_roi_stop_loss.go b/pkg/bbgo/exit_roi_stop_loss.go index 1cfb386a6..875934f41 100644 --- a/pkg/bbgo/exit_roi_stop_loss.go +++ b/pkg/bbgo/exit_roi_stop_loss.go @@ -8,24 +8,26 @@ import ( ) type RoiStopLoss struct { + Symbol string Percentage fixedpoint.Value `json:"percentage"` session *ExchangeSession orderExecutor *GeneralOrderExecutor } +func (s *RoiStopLoss) Subscribe(session *ExchangeSession) { + // use 1m kline to handle roi stop + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) +} + func (s *RoiStopLoss) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { s.session = session s.orderExecutor = orderExecutor position := orderExecutor.Position() - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { - return - } - + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { s.checkStopPrice(kline.Close, position) - }) + })) if !IsBackTesting { session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { diff --git a/pkg/bbgo/injection_test.go b/pkg/bbgo/injection_test.go deleted file mode 100644 index 69a8b5f72..000000000 --- a/pkg/bbgo/injection_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package bbgo - -import ( - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/dynamic" - "github.com/c9s/bbgo/pkg/service" - "github.com/c9s/bbgo/pkg/types" -) - -func Test_injectField(t *testing.T) { - type TT struct { - TradeService *service.TradeService - } - - // only pointer object can be set. - var tt = &TT{} - - // get the value of the pointer, or it can not be set. - var rv = reflect.ValueOf(tt).Elem() - - _, ret := dynamic.HasField(rv, "TradeService") - assert.True(t, ret) - - ts := &service.TradeService{} - - err := injectField(rv, "TradeService", ts, true) - assert.NoError(t, err) -} - -func Test_parseStructAndInject(t *testing.T) { - t.Run("skip nil", func(t *testing.T) { - ss := struct { - a int - Env *Environment - }{ - a: 1, - Env: nil, - } - err := parseStructAndInject(&ss, nil) - assert.NoError(t, err) - assert.Nil(t, ss.Env) - }) - t.Run("pointer", func(t *testing.T) { - ss := struct { - a int - Env *Environment - }{ - a: 1, - Env: nil, - } - err := parseStructAndInject(&ss, &Environment{}) - assert.NoError(t, err) - assert.NotNil(t, ss.Env) - }) - - t.Run("composition", func(t *testing.T) { - type TT struct { - *service.TradeService - } - ss := TT{} - err := parseStructAndInject(&ss, &service.TradeService{}) - assert.NoError(t, err) - assert.NotNil(t, ss.TradeService) - }) - - t.Run("struct", func(t *testing.T) { - ss := struct { - a int - Env Environment - }{ - a: 1, - } - err := parseStructAndInject(&ss, Environment{ - startTime: time.Now(), - }) - assert.NoError(t, err) - assert.NotEqual(t, time.Time{}, ss.Env.startTime) - }) - t.Run("interface/any", func(t *testing.T) { - ss := struct { - Any interface{} // anything - }{ - Any: nil, - } - err := parseStructAndInject(&ss, &Environment{ - startTime: time.Now(), - }) - assert.NoError(t, err) - assert.NotNil(t, ss.Any) - }) - t.Run("interface/stringer", func(t *testing.T) { - ss := struct { - Stringer types.Stringer // stringer interface - }{ - Stringer: nil, - } - err := parseStructAndInject(&ss, &types.Trade{}) - assert.NoError(t, err) - assert.NotNil(t, ss.Stringer) - }) -} diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 86371a53b..e8cbc13ed 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -196,7 +196,7 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si return err } - if err := injectField(rs, "OrderExecutor", orderExecutor, false); err != nil { + if err := dynamic.InjectField(rs, "OrderExecutor", orderExecutor, false); err != nil { return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy) } @@ -218,7 +218,7 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si return fmt.Errorf("marketDataStore of symbol %s not found", symbol) } - if err := parseStructAndInject(strategy, + if err := dynamic.ParseStructAndInject(strategy, market, indicatorSet, store, @@ -401,19 +401,19 @@ func (trader *Trader) injectCommonServices(s interface{}) error { return fmt.Errorf("field Persistence is not a struct element, %s given", field) } - if err := injectField(elem, "Facade", PersistenceServiceFacade, true); err != nil { + if err := dynamic.InjectField(elem, "Facade", PersistenceServiceFacade, true); err != nil { return err } /* - if err := parseStructAndInject(field.Interface(), persistenceFacade); err != nil { + if err := ParseStructAndInject(field.Interface(), persistenceFacade); err != nil { return err } */ } } - return parseStructAndInject(s, + return dynamic.ParseStructAndInject(s, &trader.logger, Notification, trader.environment.TradeService, diff --git a/pkg/dynamic/call.go b/pkg/dynamic/call.go index a3122faad..4ba59484d 100644 --- a/pkg/dynamic/call.go +++ b/pkg/dynamic/call.go @@ -51,3 +51,105 @@ func CallStructFieldsMethod(m interface{}, method string, args ...interface{}) e return nil } + +// CallMatch calls the function with the matched argument automatically +// you can define multiple parameter factory function to inject the return value as the function argument. +// e.g., +// CallMatch(targetFunction, 1, 10, true, func() *ParamType { .... }) +// +func CallMatch(f interface{}, objects ...interface{}) ([]reflect.Value, error) { + fv := reflect.ValueOf(f) + ft := reflect.TypeOf(f) + + var startIndex = 0 + var fArgs []reflect.Value + + var factoryParams = findFactoryParams(objects...) + +nextDynamicInputArg: + for i := 0; i < ft.NumIn(); i++ { + at := ft.In(i) + + // uat == underlying argument type + uat := at + if at.Kind() == reflect.Ptr { + uat = at.Elem() + } + + for oi := startIndex; oi < len(objects); oi++ { + var obj = objects[oi] + var objT = reflect.TypeOf(obj) + if objT == at { + fArgs = append(fArgs, reflect.ValueOf(obj)) + startIndex = oi + 1 + continue nextDynamicInputArg + } + + // get the kind of argument + switch k := uat.Kind(); k { + + case reflect.Interface: + if objT.Implements(at) { + fArgs = append(fArgs, reflect.ValueOf(obj)) + startIndex = oi + 1 + continue nextDynamicInputArg + } + } + } + + // factory param can be reused + for _, fp := range factoryParams { + fpt := fp.Type() + outType := fpt.Out(0) + if outType == at { + fOut := fp.Call(nil) + fArgs = append(fArgs, fOut[0]) + continue nextDynamicInputArg + } + } + + fArgs = append(fArgs, reflect.Zero(at)) + } + + out := fv.Call(fArgs) + if ft.NumOut() == 0 { + return out, nil + } + + // try to get the error object from the return value (if any) + var err error + for i := 0; i < ft.NumOut(); i++ { + outType := ft.Out(i) + switch outType.Kind() { + case reflect.Interface: + o := out[i].Interface() + switch ov := o.(type) { + case error: + err = ov + + } + + } + } + return out, err +} + +func findFactoryParams(objs ...interface{}) (fs []reflect.Value) { + for i := range objs { + obj := objs[i] + + objT := reflect.TypeOf(obj) + + if objT.Kind() != reflect.Func { + continue + } + + if objT.NumOut() == 0 || objT.NumIn() > 0 { + continue + } + + fs = append(fs, reflect.ValueOf(obj)) + } + + return fs +} diff --git a/pkg/dynamic/call_test.go b/pkg/dynamic/call_test.go index 5324218f9..b65029ded 100644 --- a/pkg/dynamic/call_test.go +++ b/pkg/dynamic/call_test.go @@ -27,3 +27,89 @@ func TestCallStructFieldsMethod(t *testing.T) { err := CallStructFieldsMethod(c, "Subscribe", 10) assert.NoError(t, err) } + +type S struct { + ID string +} + +func (s *S) String() string { return s.ID } + +func TestCallMatch(t *testing.T) { + t.Run("simple", func(t *testing.T) { + f := func(a int, b int) { + assert.Equal(t, 1, a) + assert.Equal(t, 2, b) + } + _, err := CallMatch(f, 1, 2) + assert.NoError(t, err) + }) + + t.Run("interface", func(t *testing.T) { + type A interface { + String() string + } + f := func(foo int, a A) { + assert.Equal(t, "foo", a.String()) + } + _, err := CallMatch(f, 10, &S{ID: "foo"}) + assert.NoError(t, err) + }) + + t.Run("nil interface", func(t *testing.T) { + type A interface { + String() string + } + f := func(foo int, a A) { + assert.Equal(t, 10, foo) + assert.Nil(t, a) + } + _, err := CallMatch(f, 10) + assert.NoError(t, err) + }) + + t.Run("struct pointer", func(t *testing.T) { + f := func(foo int, s *S) { + assert.Equal(t, 10, foo) + assert.NotNil(t, s) + } + _, err := CallMatch(f, 10, &S{}) + assert.NoError(t, err) + }) + + t.Run("struct pointer x 2", func(t *testing.T) { + f := func(foo int, s1, s2 *S) { + assert.Equal(t, 10, foo) + assert.Equal(t, "s1", s1.String()) + assert.Equal(t, "s2", s2.String()) + } + _, err := CallMatch(f, 10, &S{ID: "s1"}, &S{ID: "s2"}) + assert.NoError(t, err) + }) + + t.Run("func factory", func(t *testing.T) { + f := func(s *S) { + assert.Equal(t, "factory", s.String()) + } + _, err := CallMatch(f, func() *S { + return &S{ID: "factory"} + }) + assert.NoError(t, err) + }) + + t.Run("nil", func(t *testing.T) { + f := func(s *S) { + assert.Nil(t, s) + } + _, err := CallMatch(f) + assert.NoError(t, err) + }) + + t.Run("zero struct", func(t *testing.T) { + f := func(s S) { + assert.Equal(t, S{}, s) + } + _, err := CallMatch(f) + assert.NoError(t, err) + }) + +} diff --git a/pkg/bbgo/injection.go b/pkg/dynamic/inject.go similarity index 54% rename from pkg/bbgo/injection.go rename to pkg/dynamic/inject.go index 0db8cf228..04a48599b 100644 --- a/pkg/bbgo/injection.go +++ b/pkg/dynamic/inject.go @@ -1,13 +1,23 @@ -package bbgo +package dynamic import ( "fmt" "reflect" + "testing" + "time" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/types" ) -func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error { +type testEnvironment struct { + startTime time.Time +} + +func InjectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error { field := rs.FieldByName(fieldName) if !field.IsValid() { return nil @@ -38,10 +48,10 @@ func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnl return nil } -// parseStructAndInject parses the struct fields and injects the objects into the corresponding fields by its type. +// ParseStructAndInject parses the struct fields and injects the objects into the corresponding fields by its type. // if the given object is a reference of an object, the type of the target field MUST BE a pointer field. // if the given object is a struct value, the type of the target field CAN BE a pointer field or a struct value field. -func parseStructAndInject(f interface{}, objects ...interface{}) error { +func ParseStructAndInject(f interface{}, objects ...interface{}) error { sv := reflect.ValueOf(f) st := reflect.TypeOf(f) @@ -121,3 +131,96 @@ func parseStructAndInject(f interface{}, objects ...interface{}) error { return nil } + +func Test_injectField(t *testing.T) { + type TT struct { + TradeService *service.TradeService + } + + // only pointer object can be set. + var tt = &TT{} + + // get the value of the pointer, or it can not be set. + var rv = reflect.ValueOf(tt).Elem() + + _, ret := HasField(rv, "TradeService") + assert.True(t, ret) + + ts := &service.TradeService{} + + err := InjectField(rv, "TradeService", ts, true) + assert.NoError(t, err) +} + +func Test_parseStructAndInject(t *testing.T) { + t.Run("skip nil", func(t *testing.T) { + ss := struct { + a int + Env *testEnvironment + }{ + a: 1, + Env: nil, + } + err := ParseStructAndInject(&ss, nil) + assert.NoError(t, err) + assert.Nil(t, ss.Env) + }) + t.Run("pointer", func(t *testing.T) { + ss := struct { + a int + Env *testEnvironment + }{ + a: 1, + Env: nil, + } + err := ParseStructAndInject(&ss, &testEnvironment{}) + assert.NoError(t, err) + assert.NotNil(t, ss.Env) + }) + + t.Run("composition", func(t *testing.T) { + type TT struct { + *service.TradeService + } + ss := TT{} + err := ParseStructAndInject(&ss, &service.TradeService{}) + assert.NoError(t, err) + assert.NotNil(t, ss.TradeService) + }) + + t.Run("struct", func(t *testing.T) { + ss := struct { + a int + Env testEnvironment + }{ + a: 1, + } + err := ParseStructAndInject(&ss, testEnvironment{ + startTime: time.Now(), + }) + assert.NoError(t, err) + assert.NotEqual(t, time.Time{}, ss.Env.startTime) + }) + t.Run("interface/any", func(t *testing.T) { + ss := struct { + Any interface{} // anything + }{ + Any: nil, + } + err := ParseStructAndInject(&ss, &testEnvironment{ + startTime: time.Now(), + }) + assert.NoError(t, err) + assert.NotNil(t, ss.Any) + }) + t.Run("interface/stringer", func(t *testing.T) { + ss := struct { + Stringer types.Stringer // stringer interface + }{ + Stringer: nil, + } + err := ParseStructAndInject(&ss, &types.Trade{}) + assert.NoError(t, err) + assert.NotNil(t, ss.Stringer) + }) +} diff --git a/pkg/interact/interact.go b/pkg/interact/interact.go index a19b56210..820979cfe 100644 --- a/pkg/interact/interact.go +++ b/pkg/interact/interact.go @@ -112,7 +112,7 @@ func (it *Interact) handleResponse(session Session, text string, ctxObjects ...i } ctxObjects = append(ctxObjects, session) - _, err := parseFuncArgsAndCall(f, args, ctxObjects...) + _, err := ParseFuncArgsAndCall(f, args, ctxObjects...) if err != nil { return err } @@ -154,7 +154,7 @@ func (it *Interact) runCommand(session Session, command string, args []string, c ctxObjects = append(ctxObjects, session) session.SetState(cmd.initState) - if _, err := parseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil { + if _, err := ParseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil { return err } diff --git a/pkg/interact/interact_test.go b/pkg/interact/interact_test.go index bd0828240..8402ba1c8 100644 --- a/pkg/interact/interact_test.go +++ b/pkg/interact/interact_test.go @@ -18,7 +18,7 @@ func Test_parseFuncArgsAndCall_NoErrorFunction(t *testing.T) { return nil } - _, err := parseFuncArgsAndCall(noErrorFunc, []string{"BTCUSDT", "0.123", "true"}) + _, err := ParseFuncArgsAndCall(noErrorFunc, []string{"BTCUSDT", "0.123", "true"}) assert.NoError(t, err) } @@ -27,7 +27,7 @@ func Test_parseFuncArgsAndCall_ErrorFunction(t *testing.T) { return errors.New("error") } - _, err := parseFuncArgsAndCall(errorFunc, []string{"BTCUSDT", "0.123"}) + _, err := ParseFuncArgsAndCall(errorFunc, []string{"BTCUSDT", "0.123"}) assert.Error(t, err) } @@ -38,7 +38,7 @@ func Test_parseFuncArgsAndCall_InterfaceInjection(t *testing.T) { } buf := bytes.NewBuffer(nil) - _, err := parseFuncArgsAndCall(f, []string{"BTCUSDT", "0.123"}, buf) + _, err := ParseFuncArgsAndCall(f, []string{"BTCUSDT", "0.123"}, buf) assert.NoError(t, err) assert.Equal(t, "123", buf.String()) } diff --git a/pkg/interact/parse.go b/pkg/interact/parse.go index db4f3d1fd..64f55871b 100644 --- a/pkg/interact/parse.go +++ b/pkg/interact/parse.go @@ -10,21 +10,20 @@ import ( log "github.com/sirupsen/logrus" ) -func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) (State, error) { +func ParseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) (State, error) { fv := reflect.ValueOf(f) ft := reflect.TypeOf(f) - argIndex := 0 var rArgs []reflect.Value for i := 0; i < ft.NumIn(); i++ { at := ft.In(i) + // get the kind of argument switch k := at.Kind(); k { case reflect.Interface: found := false - for oi := 0; oi < len(objects); oi++ { obj := objects[oi] objT := reflect.TypeOf(obj) @@ -90,8 +89,8 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) } // try to get the error object from the return value - var state State var err error + var state State for i := 0; i < ft.NumOut(); i++ { outType := ft.Out(i) switch outType.Kind() { @@ -107,7 +106,6 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) err = ov } - } } return state, err diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go new file mode 100644 index 000000000..63f8a7492 --- /dev/null +++ b/pkg/strategy/pivotshort/resistance.go @@ -0,0 +1,146 @@ +package pivotshort + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type ResistanceShort struct { + Enabled bool `json:"enabled"` + Symbol string `json:"-"` + Market types.Market `json:"-"` + + types.IntervalWindow + + MinDistance fixedpoint.Value `json:"minDistance"` + NumLayers int `json:"numLayers"` + LayerSpread fixedpoint.Value `json:"layerSpread"` + Quantity fixedpoint.Value `json:"quantity"` + Ratio fixedpoint.Value `json:"ratio"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + resistancePivot *indicator.Pivot + resistancePrices []float64 + nextResistancePrice fixedpoint.Value + + activeOrders *bbgo.ActiveOrderBook +} + +func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeOrders.BindStream(session.UserDataStream) + + store, _ := session.MarketDataStore(s.Symbol) + + s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} + s.resistancePivot.Bind(store) + + // preload history kline data to the resistance pivot indicator + // we use the last kline to find the higher lows + lastKLine := preloadPivot(s.resistancePivot, store) + + // use the last kline from the history before we get the next closed kline + if lastKLine != nil { + s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) + } + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + position := s.orderExecutor.Position() + if position.IsOpened(kline.Close) { + return + } + + s.findNextResistancePriceAndPlaceOrders(kline.Close) + })) +} + +func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { + + minDistance := s.MinDistance.Float64() + lows := s.resistancePivot.Lows + resistancePrices := findPossibleResistancePrices(closePrice.Float64(), minDistance, lows) + + log.Infof("last price: %f, possible resistance prices: %+v", closePrice.Float64(), resistancePrices) + + ctx := context.Background() + if len(resistancePrices) > 0 { + nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) + if nextResistancePrice.Compare(s.nextResistancePrice) != 0 { + bbgo.Notify("Found next resistance price: %f", nextResistancePrice.Float64()) + s.nextResistancePrice = nextResistancePrice + s.placeResistanceOrders(ctx, nextResistancePrice) + } + } +} + +func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistancePrice fixedpoint.Value) { + futuresMode := s.session.Futures || s.session.IsolatedFutures + _ = futuresMode + + totalQuantity := s.Quantity + numLayers := s.NumLayers + if numLayers == 0 { + numLayers = 1 + } + + numLayersF := fixedpoint.NewFromInt(int64(numLayers)) + layerSpread := s.LayerSpread + quantity := totalQuantity.Div(numLayersF) + + if s.activeOrders.NumOfOrders() > 0 { + if err := s.orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil { + log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.activeOrders.Orders()) + } + } + + log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) + + var orderForms []types.SubmitOrder + for i := 0; i < numLayers; i++ { + balances := s.session.GetAccount().Balances() + quoteBalance := balances[s.Market.QuoteCurrency] + baseBalance := balances[s.Market.BaseCurrency] + _ = quoteBalance + _ = baseBalance + + // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) + price := resistancePrice.Mul(fixedpoint.One.Add(s.Ratio)) + spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) + price = price.Add(spread) + log.Infof("price = %f", price.Float64()) + + log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) + + orderForms = append(orderForms, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Price: price, + Quantity: quantity, + Tag: "resistanceShort", + MarginSideEffect: types.SideEffectTypeMarginBuy, + }) + + // TODO: fix futures mode later + /* + if futuresMode { + if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 { + } + } + */ + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orderForms...) + if err != nil { + log.WithError(err).Errorf("can not place resistance order") + } + s.activeOrders.Add(createdOrders...) +} diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index f99562e68..34b5ae038 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -48,143 +48,6 @@ type BreakLow struct { StopEMA *types.IntervalWindow `json:"stopEMA"` } -type ResistanceShort struct { - Enabled bool `json:"enabled"` - Symbol string `json:"-"` - Market types.Market `json:"-"` - - types.IntervalWindow - - MinDistance fixedpoint.Value `json:"minDistance"` - NumLayers int `json:"numLayers"` - LayerSpread fixedpoint.Value `json:"layerSpread"` - Quantity fixedpoint.Value `json:"quantity"` - Ratio fixedpoint.Value `json:"ratio"` - - session *bbgo.ExchangeSession - orderExecutor *bbgo.GeneralOrderExecutor - - resistancePivot *indicator.Pivot - resistancePrices []float64 - nextResistancePrice fixedpoint.Value - - activeOrders *bbgo.ActiveOrderBook -} - -func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { - s.session = session - s.orderExecutor = orderExecutor - s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - s.activeOrders.BindStream(session.UserDataStream) - - store, _ := session.MarketDataStore(s.Symbol) - - s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} - s.resistancePivot.Bind(store) - - // preload history kline data to the resistance pivot indicator - // we use the last kline to find the higher lows - lastKLine := preloadPivot(s.resistancePivot, store) - - // use the last kline from the history before we get the next closed kline - if lastKLine != nil { - s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) - } - - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != s.Symbol || kline.Interval != s.Interval { - return - } - - position := s.orderExecutor.Position() - if position.IsOpened(kline.Close) { - return - } - - s.findNextResistancePriceAndPlaceOrders(kline.Close) - }) -} - -func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { - - minDistance := s.MinDistance.Float64() - lows := s.resistancePivot.Lows - resistancePrices := findPossibleResistancePrices(closePrice.Float64(), minDistance, lows) - - log.Infof("last price: %f, possible resistance prices: %+v", closePrice.Float64(), resistancePrices) - - ctx := context.Background() - if len(resistancePrices) > 0 { - nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) - if nextResistancePrice.Compare(s.nextResistancePrice) != 0 { - bbgo.Notify("Found next resistance price: %f", nextResistancePrice.Float64()) - s.nextResistancePrice = nextResistancePrice - s.placeResistanceOrders(ctx, nextResistancePrice) - } - } -} - -func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistancePrice fixedpoint.Value) { - futuresMode := s.session.Futures || s.session.IsolatedFutures - _ = futuresMode - - totalQuantity := s.Quantity - numLayers := s.NumLayers - if numLayers == 0 { - numLayers = 1 - } - - numLayersF := fixedpoint.NewFromInt(int64(numLayers)) - layerSpread := s.LayerSpread - quantity := totalQuantity.Div(numLayersF) - - if err := s.orderExecutor.CancelOrders(ctx, s.activeOrders.Orders()...); err != nil { - log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.activeOrders.Orders()) - } - - log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) - - var orderForms []types.SubmitOrder - for i := 0; i < numLayers; i++ { - balances := s.session.GetAccount().Balances() - quoteBalance := balances[s.Market.QuoteCurrency] - baseBalance := balances[s.Market.BaseCurrency] - _ = quoteBalance - _ = baseBalance - - // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) - price := resistancePrice.Mul(fixedpoint.One.Add(s.Ratio)) - spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) - price = price.Add(spread) - log.Infof("price = %f", price.Float64()) - - log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) - - orderForms = append(orderForms, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Price: price, - Quantity: quantity, - Tag: "resistanceShort", - }) - - // TODO: fix futures mode later - /* - if futuresMode { - if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 { - } - } - */ - } - - createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orderForms...) - if err != nil { - log.WithError(err).Errorf("can not place resistance order") - } - s.activeOrders.Add(createdOrders...) -} - type Strategy struct { Environment *bbgo.Environment Symbol string `json:"symbol"` @@ -302,11 +165,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se preloadPivot(s.pivot, store) // update pivot low data - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != s.Symbol || kline.Interval != s.Interval { - return - } - + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow()) if lastLow.IsZero() { return @@ -318,7 +177,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.lastLow = lastLow s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) - }) + })) if s.BreakLow.StopEMA != nil { s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA) @@ -332,17 +191,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ResistanceShort.Bind(session, s.orderExecutor) } - // Always check whether you can open a short position or not - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { if s.Status != types.StrategyStatusRunning { return } - if kline.Symbol != s.Symbol || kline.Interval != types.Interval1m { - return - } - - if !s.Position.IsClosed() && !s.Position.IsDust(kline.Close) { + if s.Position.IsOpened(kline.Close) { return } @@ -368,8 +222,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - // we need the price cross the break line - // or we do nothing + // we need the price cross the break line or we do nothing if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) { return } @@ -390,6 +243,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } + // graceful cancel all active orders _ = s.orderExecutor.GracefulCancel(ctx) quantity := s.useQuantityOrBaseBalance(s.BreakLow.Quantity) @@ -402,7 +256,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", s.Symbol, kline.Close.Float64(), previousLow.Float64(), s.BreakLow.Ratio.Float64(), sellPrice.Float64()) s.placeLimitSell(ctx, sellPrice, quantity, "breakLowLimit") } - }) + })) if !bbgo.IsBackTesting { // use market trade to submit short order @@ -440,6 +294,10 @@ func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quant } func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { + if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { + return quantity + } + balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) if hasBalance { diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 6a92ffdf3..529d30d95 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -604,3 +604,14 @@ func (k *KLineSeries) Length() int { } var _ Series = &KLineSeries{} + +type KLineCallBack func(k KLine) + +func KLineWith(symbol string, interval Interval, callback KLineCallBack) KLineCallBack { + return func(k KLine) { + if k.Symbol != symbol || k.Interval != interval { + return + } + callback(k) + } +}