diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index 473df2cbc..ea5aa6f61 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -39,18 +39,21 @@ exchangeStrategies: # stopEMARange is the price range we allow short. # Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange]) - stopEMARange: 0% + # Higher the stopEMARange than higher the chance to open a short + stopEMARange: 2% stopEMA: interval: 1h window: 99 - bounceShort: - enabled: false + resistanceShort: + enabled: true interval: 1h - window: 10 + window: 8 + quantity: 10.0 + + # minDistance is used to ignore the place that is too near to the current price minDistance: 3% - # stopLossPercentage: 1% # ratio is the ratio of the resistance price, # higher the ratio, lower the price @@ -86,12 +89,15 @@ exchangeStrategies: # you can grab a simple stats by the following SQL: # SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20; - lowerShadowTakeProfit: + interval: 30m + window: 99 ratio: 3% # (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold - cumulatedVolumeTakeProfit: - minQuoteVolume: 100_000_000 + interval: 5m window: 2 + minQuoteVolume: 200_000_000 backtest: sessions: diff --git a/pkg/bbgo/config.go b/pkg/bbgo/config.go index 8a2eeb3e6..8c34c6232 100644 --- a/pkg/bbgo/config.go +++ b/pkg/bbgo/config.go @@ -13,6 +13,7 @@ import ( "gopkg.in/yaml.v3" "github.com/c9s/bbgo/pkg/datatype" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" @@ -387,7 +388,7 @@ func (c *Config) GetSignature() string { id := strategy.ID() ps = append(ps, id) - if symbol, ok := isSymbolBasedStrategy(reflect.ValueOf(strategy)); ok { + if symbol, ok := dynamic.LookupSymbolField(reflect.ValueOf(strategy)); ok { ps = append(ps, symbol) } } diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index 98404ddbf..2701fdb94 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -3,9 +3,23 @@ package bbgo import ( "reflect" - "github.com/c9s/bbgo/pkg/types" + "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/dynamic" ) +type ExitMethodSet []ExitMethod + +func (s *ExitMethodSet) SetAndSubscribe(session *ExchangeSession, parent interface{}) { + for i := range *s { + m := (*s)[i] + + // manually inherit configuration from strategy + m.Inherit(parent) + m.Subscribe(session) + } +} + type ExitMethod struct { RoiStopLoss *RoiStopLoss `json:"roiStopLoss"` ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"` @@ -14,35 +28,50 @@ type ExitMethod struct { CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"` } -func (m *ExitMethod) Subscribe(session *ExchangeSession) { - // TODO: pull out this implementation as a simple function to reflect.go - rv := reflect.ValueOf(m) - rt := reflect.TypeOf(m) - - rv = rv.Elem() - rt = rt.Elem() - infType := reflect.TypeOf((*types.Subscriber)(nil)).Elem() - - argValues := toReflectValues(session) - for i := 0; i < rt.NumField(); i++ { - fieldType := rt.Field(i) - if fieldType.Type.Implements(infType) { - method := rv.Field(i).MethodByName("Subscribe") - method.Call(argValues) +// Inherit is used for inheriting properties from the given strategy struct +// for example, some exit method requires the default interval and symbol name from the strategy param object +func (m *ExitMethod) Inherit(parent interface{}) { + // we need to pass some information from the strategy configuration to the exit methods, like symbol, interval and window + rt := reflect.TypeOf(m).Elem() + rv := reflect.ValueOf(m).Elem() + for j := 0; j < rv.NumField(); j++ { + if !rt.Field(j).IsExported() { + continue } + + fieldValue := rv.Field(j) + if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { + continue + } + + dynamic.InheritStructValues(fieldValue.Interface(), parent) + } +} + +func (m *ExitMethod) Subscribe(session *ExchangeSession) { + if err := dynamic.CallStructFieldsMethod(m, "Subscribe", session); err != nil { + panic(errors.Wrap(err, "dynamic Subscribe call failed")) } } func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { if m.ProtectiveStopLoss != nil { m.ProtectiveStopLoss.Bind(session, orderExecutor) - } else if m.RoiStopLoss != nil { + } + + if m.RoiStopLoss != nil { m.RoiStopLoss.Bind(session, orderExecutor) - } else if m.RoiTakeProfit != nil { + } + + if m.RoiTakeProfit != nil { m.RoiTakeProfit.Bind(session, orderExecutor) - } else if m.LowerShadowTakeProfit != nil { + } + + if m.LowerShadowTakeProfit != nil { m.LowerShadowTakeProfit.Bind(session, orderExecutor) - } else if m.CumulatedVolumeTakeProfit != nil { + } + + if m.CumulatedVolumeTakeProfit != nil { m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor) } } diff --git a/pkg/bbgo/exit_cumulated_volume_take_profit.go b/pkg/bbgo/exit_cumulated_volume_take_profit.go index 77c855c4b..864e949ab 100644 --- a/pkg/bbgo/exit_cumulated_volume_take_profit.go +++ b/pkg/bbgo/exit_cumulated_volume_take_profit.go @@ -3,6 +3,8 @@ package bbgo import ( "context" + log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -15,7 +17,10 @@ import ( // > SELECT start_time, `interval`, quote_volume, open, close FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' ORDER BY quote_volume DESC LIMIT 20; // type CumulatedVolumeTakeProfit struct { + Symbol string `json:"symbol"` + types.IntervalWindow + Ratio fixedpoint.Value `json:"ratio"` MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` @@ -32,7 +37,7 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor store, _ := session.MarketDataStore(position.Symbol) session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { + if kline.Symbol != position.Symbol || kline.Interval != s.Interval { return } @@ -46,25 +51,33 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor return } - if klines, ok := store.KLinesOfInterval(s.Interval); ok { - var cbv = fixedpoint.Zero - var cqv = fixedpoint.Zero - for i := 0; i < s.Window; i++ { - last := (*klines)[len(*klines)-1-i] - cqv = cqv.Add(last.QuoteVolume) - cbv = cbv.Add(last.Volume) - } + klines, ok := store.KLinesOfInterval(s.Interval) + if !ok { + log.Warnf("history kline not found") + return + } - if cqv.Compare(s.MinQuoteVolume) > 0 { - Notify("%s TakeProfit triggered by cumulated volume (window: %d) %f > %f, price = %f", - position.Symbol, - s.Window, - cqv.Float64(), - s.MinQuoteVolume.Float64(), kline.Close.Float64()) + if len(*klines) < s.Window { + return + } - _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit") - return - } + var cbv = fixedpoint.Zero + var cqv = fixedpoint.Zero + for i := 0; i < s.Window; i++ { + last := (*klines)[len(*klines)-1-i] + cqv = cqv.Add(last.QuoteVolume) + cbv = cbv.Add(last.Volume) + } + + if cqv.Compare(s.MinQuoteVolume) > 0 { + Notify("%s TakeProfit triggered by cumulated volume (window: %d) %f > %f, price = %f", + position.Symbol, + s.Window, + cqv.Float64(), + s.MinQuoteVolume.Float64(), kline.Close.Float64()) + + _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit") + return } }) } diff --git a/pkg/bbgo/exit_lower_shadow_take_profit.go b/pkg/bbgo/exit_lower_shadow_take_profit.go index a6008223e..ba598a69e 100644 --- a/pkg/bbgo/exit_lower_shadow_take_profit.go +++ b/pkg/bbgo/exit_lower_shadow_take_profit.go @@ -8,16 +8,29 @@ import ( ) type LowerShadowTakeProfit struct { - Ratio fixedpoint.Value `json:"ratio"` + // inherit from the strategy + types.IntervalWindow + // inherit from the strategy + Symbol string `json:"symbol"` + + Ratio fixedpoint.Value `json:"ratio"` session *ExchangeSession orderExecutor *GeneralOrderExecutor } +func (s *LowerShadowTakeProfit) Subscribe(session *ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { s.session = session s.orderExecutor = orderExecutor + stdIndicatorSet, _ := session.StandardIndicatorSet(s.Symbol) + ewma := stdIndicatorSet.EWMA(s.IntervalWindow) + + position := orderExecutor.Position() session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { @@ -38,6 +51,11 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge return } + // skip close price higher than the ewma + if closePrice.Float64() > ewma.Last() { + return + } + if kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Ratio) > 0 { Notify("%s TakeProfit triggered by shadow ratio %f, price = %f", position.Symbol, diff --git a/pkg/bbgo/graceful_shutdown.go b/pkg/bbgo/graceful_shutdown.go index b35482ce2..c3248b0c8 100644 --- a/pkg/bbgo/graceful_shutdown.go +++ b/pkg/bbgo/graceful_shutdown.go @@ -3,18 +3,40 @@ package bbgo import ( "context" "sync" + "time" + + "github.com/sirupsen/logrus" ) +var graceful = &Graceful{} + //go:generate callbackgen -type Graceful type Graceful struct { shutdownCallbacks []func(ctx context.Context, wg *sync.WaitGroup) } +// Shutdown is a blocking call to emit all shutdown callbacks at the same time. func (g *Graceful) Shutdown(ctx context.Context) { var wg sync.WaitGroup wg.Add(len(g.shutdownCallbacks)) - go g.EmitShutdown(ctx, &wg) + // for each shutdown callback, we give them 10 second + shtCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + + go g.EmitShutdown(shtCtx, &wg) wg.Wait() + cancel() +} + +func OnShutdown(f func(ctx context.Context, wg *sync.WaitGroup)) { + graceful.OnShutdown(f) +} + +func Shutdown() { + logrus.Infof("shutting down...") + + ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) + graceful.Shutdown(ctx) + cancel() } diff --git a/pkg/bbgo/injection_test.go b/pkg/bbgo/injection_test.go index dd6370320..69a8b5f72 100644 --- a/pkg/bbgo/injection_test.go +++ b/pkg/bbgo/injection_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" ) @@ -22,7 +23,7 @@ func Test_injectField(t *testing.T) { // get the value of the pointer, or it can not be set. var rv = reflect.ValueOf(tt).Elem() - _, ret := hasField(rv, "TradeService") + _, ret := dynamic.HasField(rv, "TradeService") assert.True(t, ret) ts := &service.TradeService{} diff --git a/pkg/bbgo/persistence.go b/pkg/bbgo/persistence.go index b435c8f07..89b4179df 100644 --- a/pkg/bbgo/persistence.go +++ b/pkg/bbgo/persistence.go @@ -6,6 +6,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/service" ) @@ -106,10 +107,10 @@ func Sync(obj interface{}) { } func loadPersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error { - return iterateFieldsByTag(obj, "persistence", func(tag string, field reflect.StructField, value reflect.Value) error { + return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, field reflect.StructField, value reflect.Value) error { log.Debugf("[loadPersistenceFields] loading value into field %v, tag = %s, original value = %v", field, tag, value) - newValueInf := newTypeValueInterface(value.Type()) + newValueInf := dynamic.NewTypeValueInterface(value.Type()) // inf := value.Interface() store := persistence.NewStore("state", id, tag) if err := store.Load(&newValueInf); err != nil { @@ -134,7 +135,7 @@ func loadPersistenceFields(obj interface{}, id string, persistence service.Persi } func storePersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error { - return iterateFieldsByTag(obj, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { + return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { log.Debugf("[storePersistenceFields] storing value from field %v, tag = %s, original value = %v", ft, tag, fv) inf := fv.Interface() diff --git a/pkg/bbgo/persistence_test.go b/pkg/bbgo/persistence_test.go index ebc5314f0..0eea57ed5 100644 --- a/pkg/bbgo/persistence_test.go +++ b/pkg/bbgo/persistence_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" @@ -23,7 +24,6 @@ func (s *TestStructWithoutInstanceID) ID() string { type TestStruct struct { *Environment - *Graceful Position *types.Position `persistence:"position"` Integer int64 `persistence:"integer"` @@ -83,7 +83,7 @@ func Test_loadPersistenceFields(t *testing.T) { t.Run(psName+"/nil", func(t *testing.T) { var b *TestStruct = nil err := loadPersistenceFields(b, "test-nil", ps) - assert.Equal(t, errCanNotIterateNilPointer, err) + assert.Equal(t, dynamic.ErrCanNotIterateNilPointer, err) }) t.Run(psName+"/pointer-field", func(t *testing.T) { diff --git a/pkg/bbgo/reflect.go b/pkg/bbgo/reflect.go index f83da274d..263c2cc87 100644 --- a/pkg/bbgo/reflect.go +++ b/pkg/bbgo/reflect.go @@ -1,9 +1,9 @@ package bbgo import ( - "errors" - "fmt" "reflect" + + "github.com/c9s/bbgo/pkg/dynamic" ) type InstanceIDProvider interface { @@ -19,7 +19,7 @@ func callID(obj interface{}) string { return ret[0].String() } - if symbol, ok := isSymbolBasedStrategy(sv); ok { + if symbol, ok := dynamic.LookupSymbolField(sv); ok { m := sv.MethodByName("ID") ret := m.Call(nil) return ret[0].String() + ":" + symbol @@ -31,90 +31,3 @@ func callID(obj interface{}) string { return ret[0].String() + ":" } -func isSymbolBasedStrategy(rs reflect.Value) (string, bool) { - if rs.Kind() == reflect.Ptr { - rs = rs.Elem() - } - - field := rs.FieldByName("Symbol") - if !field.IsValid() { - return "", false - } - - if field.Kind() != reflect.String { - return "", false - } - - return field.String(), true -} - -func hasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) { - field = rs.FieldByName(fieldName) - return field, field.IsValid() -} - -type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Value) error - -var errCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer") - -func iterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error { - sv := reflect.ValueOf(obj) - st := reflect.TypeOf(obj) - - if st.Kind() != reflect.Ptr { - return fmt.Errorf("f should be a pointer of a struct, %s given", st) - } - - // for pointer, check if it's nil - if sv.IsNil() { - return errCanNotIterateNilPointer - } - - // solve the reference - st = st.Elem() - sv = sv.Elem() - - if st.Kind() != reflect.Struct { - return fmt.Errorf("f should be a struct, %s given", st) - } - - for i := 0; i < sv.NumField(); i++ { - fv := sv.Field(i) - ft := st.Field(i) - - // skip unexported fields - if !st.Field(i).IsExported() { - continue - } - - tag, ok := ft.Tag.Lookup(tagName) - if !ok { - continue - } - - if err := cb(tag, ft, fv); err != nil { - return err - } - } - - return nil -} - -// https://github.com/xiaojun207/go-base-utils/blob/master/utils/Clone.go -func newTypeValueInterface(typ reflect.Type) interface{} { - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - dst := reflect.New(typ).Elem() - return dst.Addr().Interface() - } - dst := reflect.New(typ) - return dst.Interface() -} - -func toReflectValues(args ...interface{}) (values []reflect.Value) { - for _, arg := range args { - values = append(values, reflect.ValueOf(arg)) - } - - return values -} diff --git a/pkg/bbgo/reflect_test.go b/pkg/bbgo/reflect_test.go new file mode 100644 index 000000000..920078f66 --- /dev/null +++ b/pkg/bbgo/reflect_test.go @@ -0,0 +1,2 @@ +package bbgo + diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 4fc35e08e..86371a53b 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -10,6 +10,7 @@ import ( _ "github.com/go-sql-driver/mysql" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/interact" ) @@ -72,8 +73,6 @@ type Trader struct { exchangeStrategies map[string][]SingleExchangeStrategy logger Logger - - Graceful Graceful } func NewTrader(environ *Environment) *Trader { @@ -201,7 +200,7 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy) } - if symbol, ok := isSymbolBasedStrategy(rs); ok { + if symbol, ok := dynamic.LookupSymbolField(rs); ok { log.Infof("found symbol based strategy from %s", rs.Type()) market, ok := session.Market(symbol) @@ -394,7 +393,7 @@ func (trader *Trader) injectCommonServices(s interface{}) error { // a special injection for persistence selector: // if user defined the selector, the facade pointer will be nil, hence we need to update the persistence facade pointer sv := reflect.ValueOf(s).Elem() - if field, ok := hasField(sv, "Persistence"); ok { + if field, ok := dynamic.HasField(sv, "Persistence"); ok { // the selector is set, but we need to update the facade pointer if !field.IsNil() { elem := field.Elem() @@ -415,7 +414,6 @@ func (trader *Trader) injectCommonServices(s interface{}) error { } return parseStructAndInject(s, - &trader.Graceful, &trader.logger, Notification, trader.environment.TradeService, diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 363d2d282..1c1ac11a4 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -443,9 +443,7 @@ var BacktestCmd = &cobra.Command{ cmdutil.WaitForSignal(runCtx, syscall.SIGINT, syscall.SIGTERM) log.Infof("shutting down trader...") - shutdownCtx, cancelShutdown := context.WithDeadline(runCtx, time.Now().Add(10*time.Second)) - trader.Graceful.Shutdown(shutdownCtx) - cancelShutdown() + bbgo.Shutdown() // put the logger back to print the pnl log.SetLevel(log.InfoLevel) diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index ca634c0a5..c6f1841d6 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -8,7 +8,6 @@ import ( "path/filepath" "runtime/pprof" "syscall" - "time" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -78,12 +77,7 @@ func runSetup(baseCtx context.Context, userConfig *bbgo.Config, enableApiServer cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cancelTrading() - // graceful period = 15 second - shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(15*time.Second)) - - log.Infof("shutting down...") - trader.Graceful.Shutdown(shutdownCtx) - cancelShutdown() + bbgo.Shutdown() return nil } @@ -216,10 +210,7 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cancelTrading() - log.Infof("shutting down...") - shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(30*time.Second)) - trader.Graceful.Shutdown(shutdownCtx) - cancelShutdown() + bbgo.Shutdown() if err := trader.SaveState(); err != nil { log.WithError(err).Errorf("can not save strategy states") diff --git a/pkg/dynamic/call.go b/pkg/dynamic/call.go new file mode 100644 index 000000000..a3122faad --- /dev/null +++ b/pkg/dynamic/call.go @@ -0,0 +1,53 @@ +package dynamic + +import ( + "errors" + "reflect" +) + +// CallStructFieldsMethod iterates field from the given struct object +// check if the field object implements the interface, if it's implemented, then we call a specific method +func CallStructFieldsMethod(m interface{}, method string, args ...interface{}) error { + rv := reflect.ValueOf(m) + rt := reflect.TypeOf(m) + + if rt.Kind() != reflect.Ptr { + return errors.New("the given object needs to be a pointer") + } + + rv = rv.Elem() + rt = rt.Elem() + + if rt.Kind() != reflect.Struct { + return errors.New("the given object needs to be struct") + } + + argValues := ToReflectValues(args...) + for i := 0; i < rt.NumField(); i++ { + fieldType := rt.Field(i) + fieldValue := rv.Field(i) + + // skip non-exported fields + if !fieldType.IsExported() { + continue + } + + if fieldType.Type.Kind() == reflect.Ptr && fieldValue.IsNil() { + continue + } + + methodType, ok := fieldType.Type.MethodByName(method) + if !ok { + continue + } + + if len(argValues) < methodType.Type.NumIn() { + // return fmt.Errorf("method %v require %d args, %d given", methodType, methodType.Type.NumIn(), len(argValues)) + } + + refMethod := fieldValue.MethodByName(method) + refMethod.Call(argValues) + } + + return nil +} diff --git a/pkg/dynamic/call_test.go b/pkg/dynamic/call_test.go new file mode 100644 index 000000000..5324218f9 --- /dev/null +++ b/pkg/dynamic/call_test.go @@ -0,0 +1,29 @@ +package dynamic + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type callTest struct { + ChildCall1 *childCall1 + ChildCall2 *childCall2 +} + +type childCall1 struct{} + +func (c *childCall1) Subscribe(a int) {} + +type childCall2 struct{} + +func (c *childCall2) Subscribe(a int) {} + +func TestCallStructFieldsMethod(t *testing.T) { + c := &callTest{ + ChildCall1: &childCall1{}, + ChildCall2: &childCall2{}, + } + err := CallStructFieldsMethod(c, "Subscribe", 10) + assert.NoError(t, err) +} diff --git a/pkg/dynamic/field.go b/pkg/dynamic/field.go new file mode 100644 index 000000000..5fc222be1 --- /dev/null +++ b/pkg/dynamic/field.go @@ -0,0 +1,26 @@ +package dynamic + +import "reflect" + +func HasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) { + field = rs.FieldByName(fieldName) + return field, field.IsValid() +} + +func LookupSymbolField(rs reflect.Value) (string, bool) { + if rs.Kind() == reflect.Ptr { + rs = rs.Elem() + } + + field := rs.FieldByName("Symbol") + if !field.IsValid() { + return "", false + } + + if field.Kind() != reflect.String { + return "", false + } + + return field.String(), true +} + diff --git a/pkg/dynamic/iterate.go b/pkg/dynamic/iterate.go new file mode 100644 index 000000000..12d6e2842 --- /dev/null +++ b/pkg/dynamic/iterate.go @@ -0,0 +1,54 @@ +package dynamic + +import ( + "errors" + "fmt" + "reflect" +) + +type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Value) error + +var ErrCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer") + +func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error { + sv := reflect.ValueOf(obj) + st := reflect.TypeOf(obj) + + if st.Kind() != reflect.Ptr { + return fmt.Errorf("f should be a pointer of a struct, %s given", st) + } + + // for pointer, check if it's nil + if sv.IsNil() { + return ErrCanNotIterateNilPointer + } + + // solve the reference + st = st.Elem() + sv = sv.Elem() + + if st.Kind() != reflect.Struct { + return fmt.Errorf("f should be a struct, %s given", st) + } + + for i := 0; i < sv.NumField(); i++ { + fv := sv.Field(i) + ft := st.Field(i) + + // skip unexported fields + if !st.Field(i).IsExported() { + continue + } + + tag, ok := ft.Tag.Lookup(tagName) + if !ok { + continue + } + + if err := cb(tag, ft, fv); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/dynamic/merge.go b/pkg/dynamic/merge.go new file mode 100644 index 000000000..8e44bd333 --- /dev/null +++ b/pkg/dynamic/merge.go @@ -0,0 +1,41 @@ +package dynamic + +import "reflect" + +// InheritStructValues merges the field value from the source struct to the dest struct. +// Only fields with the same type and the same name will be updated. +func InheritStructValues(dst, src interface{}) { + if dst == nil { + return + } + + rtA := reflect.TypeOf(dst) + srcStructType := reflect.TypeOf(src) + + rtA = rtA.Elem() + srcStructType = srcStructType.Elem() + + for i := 0; i < rtA.NumField(); i++ { + fieldType := rtA.Field(i) + fieldName := fieldType.Name + + if !fieldType.IsExported() { + continue + } + + // if there is a field with the same name + fieldSrcType, found := srcStructType.FieldByName(fieldName) + if !found { + continue + } + + // ensure that the type is the same + if fieldSrcType.Type == fieldType.Type { + srcValue := reflect.ValueOf(src).Elem().FieldByName(fieldName) + dstValue := reflect.ValueOf(dst).Elem().FieldByName(fieldName) + if (fieldType.Type.Kind() == reflect.Ptr && dstValue.IsNil()) || dstValue.IsZero() { + dstValue.Set(srcValue) + } + } + } +} diff --git a/pkg/dynamic/merge_test.go b/pkg/dynamic/merge_test.go new file mode 100644 index 000000000..ca61355b0 --- /dev/null +++ b/pkg/dynamic/merge_test.go @@ -0,0 +1,82 @@ +package dynamic + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type TestStrategy struct { + Symbol string `json:"symbol"` + Interval string `json:"interval"` + BaseQuantity fixedpoint.Value `json:"baseQuantity"` + MaxAssetQuantity fixedpoint.Value `json:"maxAssetQuantity"` + MinDropPercentage fixedpoint.Value `json:"minDropPercentage"` +} + +func Test_reflectMergeStructFields(t *testing.T) { + t.Run("zero value", func(t *testing.T) { + a := &TestStrategy{Symbol: "BTCUSDT"} + b := &struct{ Symbol string }{Symbol: ""} + InheritStructValues(b, a) + assert.Equal(t, "BTCUSDT", b.Symbol) + }) + + t.Run("non-zero value", func(t *testing.T) { + a := &TestStrategy{Symbol: "BTCUSDT"} + b := &struct{ Symbol string }{Symbol: "ETHUSDT"} + InheritStructValues(b, a) + assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value") + }) + + t.Run("zero embedded struct", func(t *testing.T) { + iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} + a := &struct { + types.IntervalWindow + Symbol string + }{ + IntervalWindow: iw, + Symbol: "BTCUSDT", + } + b := &struct { + Symbol string + types.IntervalWindow + }{} + InheritStructValues(b, a) + assert.Equal(t, iw, b.IntervalWindow) + assert.Equal(t, "BTCUSDT", b.Symbol) + }) + + t.Run("non-zero embedded struct", func(t *testing.T) { + iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} + a := &struct { + types.IntervalWindow + }{ + IntervalWindow: iw, + } + b := &struct { + types.IntervalWindow + }{ + IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9}, + } + InheritStructValues(b, a) + assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow) + }) + + t.Run("skip different type but the same name", func(t *testing.T) { + a := &struct { + A float64 + }{ + A: 1.99, + } + b := &struct { + A string + }{} + InheritStructValues(b, a) + assert.Equal(t, "", b.A) + assert.Equal(t, 1.99, a.A) + }) +} diff --git a/pkg/dynamic/typevalue.go b/pkg/dynamic/typevalue.go new file mode 100644 index 000000000..3ca3f1c83 --- /dev/null +++ b/pkg/dynamic/typevalue.go @@ -0,0 +1,24 @@ +package dynamic + +import "reflect" + +// https://github.com/xiaojun207/go-base-utils/blob/master/utils/Clone.go +func NewTypeValueInterface(typ reflect.Type) interface{} { + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + dst := reflect.New(typ).Elem() + return dst.Addr().Interface() + } + dst := reflect.New(typ) + return dst.Interface() +} + +// ToReflectValues convert the go objects into reflect.Value slice +func ToReflectValues(args ...interface{}) (values []reflect.Value) { + for i := range args { + arg := args[i] + values = append(values, reflect.ValueOf(arg)) + } + + return values +} diff --git a/pkg/strategy/bollgrid/strategy.go b/pkg/strategy/bollgrid/strategy.go index 8d689f7d4..676c8fea0 100644 --- a/pkg/strategy/bollgrid/strategy.go +++ b/pkg/strategy/bollgrid/strategy.go @@ -41,9 +41,6 @@ type Strategy struct { // This field will be injected automatically since we defined the Symbol field. *bbgo.StandardIndicatorSet - // Graceful let you define the graceful shutdown handler - *bbgo.Graceful - // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc // This field will be injected automatically since we defined the Symbol field. types.Market @@ -350,7 +347,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.profitOrders.BindStream(session.UserDataStream) // setup graceful shutting down handler - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { // call Done to notify the main process. defer wg.Done() log.Infof("canceling active orders...") diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 93b135b9d..2bcf6e2e9 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -49,7 +49,6 @@ type BollingerSetting struct { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Environment *bbgo.Environment @@ -616,7 +615,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // s.book = types.NewStreamBook(s.Symbol) // s.book.BindStreamForBackground(session.MarketDataStream) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() _ = s.orderExecutor.GracefulCancel(ctx) diff --git a/pkg/strategy/dca/strategy.go b/pkg/strategy/dca/strategy.go index f0e86aa53..38e6c9d14 100644 --- a/pkg/strategy/dca/strategy.go +++ b/pkg/strategy/dca/strategy.go @@ -47,7 +47,6 @@ func (b BudgetPeriod) Duration() time.Duration { // Strategy is the Dollar-Cost-Average strategy type Strategy struct { - *bbgo.Graceful Environment *bbgo.Environment Symbol string `json:"symbol"` diff --git a/pkg/strategy/emastop/strategy.go b/pkg/strategy/emastop/strategy.go index 89c837b37..7c9c4a190 100644 --- a/pkg/strategy/emastop/strategy.go +++ b/pkg/strategy/emastop/strategy.go @@ -25,7 +25,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful SourceExchangeName string `json:"sourceExchange"` @@ -217,7 +216,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.place(ctx, orderExecutor, session, indicator, closePrice) }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling trailingstop order...") s.clear(ctx, orderExecutor) @@ -261,7 +260,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.place(ctx, &orderExecutor, session, indicator, closePrice) }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling trailingstop order...") s.clear(ctx, &orderExecutor) diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index bce4cf07a..4a926c901 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -51,7 +51,6 @@ type Strategy struct { KLineEndTime types.Time *bbgo.Environment - *bbgo.Graceful bbgo.StrategyController activeMakerOrders *bbgo.ActiveOrderBook @@ -1221,7 +1220,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling active orders...") s.CancelAll(ctx) diff --git a/pkg/strategy/flashcrash/strategy.go b/pkg/strategy/flashcrash/strategy.go index b15fcbfb7..4b5c80577 100644 --- a/pkg/strategy/flashcrash/strategy.go +++ b/pkg/strategy/flashcrash/strategy.go @@ -49,10 +49,6 @@ type Strategy struct { // This field will be injected automatically since we defined the Symbol field. *bbgo.StandardIndicatorSet - // Graceful shutdown function - *bbgo.Graceful - // -------------------------- - // ewma is the exponential weighted moving average indicator ewma *indicator.EWMA } @@ -114,7 +110,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeOrders.BindStream(session.UserDataStream) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling active orders...") diff --git a/pkg/strategy/fmaker/strategy.go b/pkg/strategy/fmaker/strategy.go index d67367569..c6f7068be 100644 --- a/pkg/strategy/fmaker/strategy.go +++ b/pkg/strategy/fmaker/strategy.go @@ -31,7 +31,6 @@ type IntervalWindowSetting struct { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Environment *bbgo.Environment diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index 75e781783..7fc899576 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -45,8 +45,6 @@ type State struct { } type Strategy struct { - *bbgo.Graceful `json:"-" yaml:"-"` - *bbgo.Persistence // OrderExecutor is an interface for submitting order. @@ -621,7 +619,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.tradeCollector.BindStream(session.UserDataStream) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() if err := s.SaveState(); err != nil { diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 3fd239cb6..a55094833 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -10,6 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" @@ -47,8 +48,10 @@ type BreakLow struct { StopEMA *types.IntervalWindow `json:"stopEMA"` } -type BounceShort struct { - Enabled bool `json:"enabled"` +type ResistanceShort struct { + Enabled bool `json:"enabled"` + Symbol string `json:"-"` + Market types.Market `json:"-"` types.IntervalWindow @@ -57,20 +60,129 @@ type BounceShort struct { 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 + + resistanceOrders []types.Order } -type Entry struct { - CatBounceRatio fixedpoint.Value `json:"catBounceRatio"` - NumLayers int `json:"numLayers"` - TotalQuantity fixedpoint.Value `json:"totalQuantity"` +func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor - Quantity fixedpoint.Value `json:"quantity"` - MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"` + position := orderExecutor.Position() + symbol := position.Symbol + store, _ := session.MarketDataStore(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 + s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) + + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if kline.Symbol != s.Symbol || kline.Interval != s.Interval { + return + } + + s.findNextResistancePriceAndPlaceOrders(kline.Close) + }) +} + +func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { + position := s.orderExecutor.Position() + if position.IsOpened(closePrice) { + return + } + + 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 { + 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.resistanceOrders...); err != nil { + log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.resistanceOrders) + } + s.resistanceOrders = nil + + 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.resistanceOrders = createdOrders } type Strategy struct { - *bbgo.Graceful - Environment *bbgo.Environment Symbol string `json:"symbol"` Market types.Market @@ -83,23 +195,22 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + // BreakLow is one of the entry method BreakLow BreakLow `json:"breakLow"` - BounceShort *BounceShort `json:"bounceShort"` + // ResistanceShort is one of the entry method + ResistanceShort *ResistanceShort `json:"resistanceShort"` - Entry Entry `json:"entry"` - ExitMethods []bbgo.ExitMethod `json:"exits"` + ExitMethods bbgo.ExitMethodSet `json:"exits"` session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor - stopLossPrice fixedpoint.Value lastLow fixedpoint.Value pivot *indicator.Pivot resistancePivot *indicator.Pivot stopEWMA *indicator.EWMA pivotLowPrices []fixedpoint.Value - resistancePrices []float64 currentBounceShortPrice fixedpoint.Value // StrategyController @@ -114,55 +225,16 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) - if s.BounceShort != nil && s.BounceShort.Enabled { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.BounceShort.Interval}) + if s.ResistanceShort != nil && s.ResistanceShort.Enabled { + dynamic.InheritStructValues(s.ResistanceShort, s) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ResistanceShort.Interval}) } if !bbgo.IsBackTesting { session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) } -} -func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { - balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) - - if hasBalance { - if quantity.IsZero() { - bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String()) - quantity = balance.Available - } else { - quantity = fixedpoint.Min(quantity, balance.Available) - } - } - - if quantity.IsZero() { - log.Errorf("quantity is zero, can not submit sell order, please check settings") - } - - return quantity -} - -func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value, tag string) { - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Price: price, - Side: types.SideTypeSell, - Type: types.OrderTypeLimit, - Quantity: quantity, - MarginSideEffect: types.SideEffectTypeMarginBuy, - Tag: tag, - }) -} - -func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Value, tag string) { - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeMarket, - Quantity: quantity, - MarginSideEffect: types.SideEffectTypeMarginBuy, - Tag: tag, - }) + s.ExitMethods.SetAndSubscribe(session, s) } func (s *Strategy) InstanceID() string { @@ -225,8 +297,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} s.pivot.Bind(store) - - lastKLine := s.preloadPivot(s.pivot, store) + preloadPivot(s.pivot, store) // update pivot low data session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { @@ -247,11 +318,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) }) - if s.BounceShort != nil && s.BounceShort.Enabled { - s.resistancePivot = &indicator.Pivot{IntervalWindow: s.BounceShort.IntervalWindow} - s.resistancePivot.Bind(store) - } - if s.BreakLow.StopEMA != nil { s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA) } @@ -260,36 +326,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se method.Bind(session, s.orderExecutor) } - if s.BounceShort != nil && s.BounceShort.Enabled { - if s.resistancePivot != nil { - s.preloadPivot(s.resistancePivot, store) - } - - session.UserDataStream.OnStart(func() { - if lastKLine == nil { - return - } - - if s.resistancePivot != nil { - lows := s.resistancePivot.Lows - minDistance := s.BounceShort.MinDistance.Float64() - closePrice := lastKLine.Close.Float64() - s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows) - log.Infof("last price: %f, possible resistance prices: %+v", closePrice, s.resistancePrices) - - if len(s.resistancePrices) > 0 { - resistancePrice := fixedpoint.NewFromFloat(s.resistancePrices[0]) - if resistancePrice.Compare(s.currentBounceShortPrice) != 0 { - log.Infof("updating resistance price... possible resistance prices: %+v", s.resistancePrices) - - _ = s.orderExecutor.GracefulCancel(ctx) - - s.currentBounceShortPrice = resistancePrice - s.placeBounceSellOrders(ctx, s.currentBounceShortPrice) - } - } - } - }) + if s.ResistanceShort != nil && s.ResistanceShort.Enabled { + s.ResistanceShort.Bind(session, s.orderExecutor) } // Always check whether you can open a short position or not @@ -364,40 +402,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - // StrategyController - if s.Status != types.StrategyStatusRunning { - return - } - - if s.BounceShort == nil || !s.BounceShort.Enabled { - return - } - - if kline.Symbol != s.Symbol || kline.Interval != s.BounceShort.Interval { - return - } - - if s.resistancePivot != nil { - closePrice := kline.Close.Float64() - minDistance := s.BounceShort.MinDistance.Float64() - lows := s.resistancePivot.Lows - s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows) - - if len(s.resistancePrices) > 0 { - resistancePrice := fixedpoint.NewFromFloat(s.resistancePrices[0]) - if resistancePrice.Compare(s.currentBounceShortPrice) != 0 { - log.Infof("updating resistance price... possible resistance prices: %+v", s.resistancePrices) - - _ = s.orderExecutor.GracefulCancel(ctx) - - s.currentBounceShortPrice = resistancePrice - s.placeBounceSellOrders(ctx, s.currentBounceShortPrice) - } - } - } - }) - if !bbgo.IsBackTesting { // use market trade to submit short order session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { @@ -405,7 +409,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) wg.Done() }) @@ -423,46 +427,6 @@ func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value, return price, false } -func (s *Strategy) placeBounceSellOrders(ctx context.Context, resistancePrice fixedpoint.Value) { - futuresMode := s.session.Futures || s.session.IsolatedFutures - totalQuantity := s.BounceShort.Quantity - numLayers := s.BounceShort.NumLayers - if numLayers == 0 { - numLayers = 1 - } - - numLayersF := fixedpoint.NewFromInt(int64(numLayers)) - - layerSpread := s.BounceShort.LayerSpread - quantity := totalQuantity.Div(numLayersF) - - log.Infof("placing bounce short orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) - - for i := 0; i < numLayers; i++ { - balances := s.session.GetAccount().Balances() - quoteBalance := balances[s.Market.QuoteCurrency] - baseBalance := balances[s.Market.BaseCurrency] - - // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) - price := resistancePrice.Mul(fixedpoint.One.Add(s.BounceShort.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()) - - if futuresMode { - if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 { - s.placeOrder(ctx, price, quantity) - } - } else { - if quantity.Compare(baseBalance.Available) <= 0 { - s.placeOrder(ctx, price, quantity) - } - } - } -} - func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quantity fixedpoint.Value) { _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, @@ -473,26 +437,51 @@ func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quant }) } -func (s *Strategy) preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataStore) *types.KLine { - klines, ok := store.KLinesOfInterval(pivot.Interval) - if !ok { - return nil +func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { + balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) + + if hasBalance { + if quantity.IsZero() { + bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String()) + quantity = balance.Available + } else { + quantity = fixedpoint.Min(quantity, balance.Available) + } } - last := (*klines)[len(*klines)-1] - log.Debugf("updating pivot indicator: %d klines", len(*klines)) - - for i := pivot.Window; i < len(*klines); i++ { - pivot.Update((*klines)[0 : i+1]) + if quantity.IsZero() { + log.Errorf("quantity is zero, can not submit sell order, please check settings") } - log.Infof("found %s %v previous lows: %v", s.Symbol, pivot.IntervalWindow, pivot.Lows) - log.Infof("found %s %v previous highs: %v", s.Symbol, pivot.IntervalWindow, pivot.Highs) - return &last + return quantity +} + +func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value, tag string) { + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Price: price, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: quantity, + MarginSideEffect: types.SideEffectTypeMarginBuy, + Tag: tag, + }) +} + +func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Value, tag string) { + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeMarket, + Quantity: quantity, + MarginSideEffect: types.SideEffectTypeMarginBuy, + Tag: tag, + }) } func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { // sort float64 in increasing order + // lower to higher prices sort.Float64s(lows) var resistancePrices []float64 @@ -514,3 +503,21 @@ func findPossibleResistancePrices(closePrice float64, minDistance float64, lows return resistancePrices } + +func preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataStore) *types.KLine { + klines, ok := store.KLinesOfInterval(pivot.Interval) + if !ok { + return nil + } + + last := (*klines)[len(*klines)-1] + log.Debugf("updating pivot indicator: %d klines", len(*klines)) + + for i := pivot.Window; i < len(*klines); i++ { + pivot.Update((*klines)[0 : i+1]) + } + + log.Debugf("found %v previous lows: %v", pivot.IntervalWindow, pivot.Lows) + log.Debugf("found %v previous highs: %v", pivot.IntervalWindow, pivot.Highs) + return &last +} diff --git a/pkg/strategy/rsmaker/strategy.go b/pkg/strategy/rsmaker/strategy.go index 0d0c5a8fe..0704f8ae8 100644 --- a/pkg/strategy/rsmaker/strategy.go +++ b/pkg/strategy/rsmaker/strategy.go @@ -29,9 +29,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful - *bbgo.Notifiability - Environment *bbgo.Environment StandardIndicatorSet *bbgo.StandardIndicatorSet Market types.Market diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index e91313be3..2f456a7d2 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -30,7 +30,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Environment *bbgo.Environment @@ -391,7 +390,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) // Graceful shutdown - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() close(s.stopC) diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index 4a02e443a..d9a37b135 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -134,7 +134,6 @@ func (control *TrailingStopControl) GenerateStopOrder(quantity fixedpoint.Value) type Strategy struct { *bbgo.Persistence `json:"-"` *bbgo.Environment `json:"-"` - *bbgo.Graceful `json:"-"` session *bbgo.ExchangeSession @@ -582,7 +581,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() // Cancel trailing stop order diff --git a/pkg/strategy/wall/strategy.go b/pkg/strategy/wall/strategy.go index 824cc28cf..ffc9cef4a 100644 --- a/pkg/strategy/wall/strategy.go +++ b/pkg/strategy/wall/strategy.go @@ -30,7 +30,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Environment *bbgo.Environment @@ -377,7 +376,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }() - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() close(s.stopC) diff --git a/pkg/strategy/xbalance/strategy.go b/pkg/strategy/xbalance/strategy.go index 60f0ac6ae..ea4aa1e04 100644 --- a/pkg/strategy/xbalance/strategy.go +++ b/pkg/strategy/xbalance/strategy.go @@ -135,8 +135,6 @@ func (a *Address) UnmarshalJSON(body []byte) error { } type Strategy struct { - *bbgo.Graceful - Interval types.Duration `json:"interval"` Addresses map[string]Address `json:"addresses"` @@ -342,7 +340,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.State = s.newDefaultState() } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() }) diff --git a/pkg/strategy/xgap/strategy.go b/pkg/strategy/xgap/strategy.go index 102ea2a42..7406e5551 100644 --- a/pkg/strategy/xgap/strategy.go +++ b/pkg/strategy/xgap/strategy.go @@ -57,7 +57,6 @@ func (s *State) Reset() { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Symbol string `json:"symbol"` @@ -193,7 +192,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se } } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() close(s.stopC) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 289b94208..aca499413 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -33,7 +33,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Environment *bbgo.Environment @@ -879,7 +878,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } }() - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() close(s.stopC) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index f11582efb..ea9526f47 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -58,7 +58,6 @@ func (s *State) Reset() { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence *bbgo.Environment @@ -180,7 +179,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se return err } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() s.SaveState() diff --git a/pkg/types/position.go b/pkg/types/position.go index 185106572..a85d09057 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -273,6 +273,10 @@ func (p *Position) IsClosed() bool { return p.Base.Sign() == 0 } +func (p *Position) IsOpened(currentPrice fixedpoint.Value) bool { + return !p.IsClosed() && !p.IsDust(currentPrice) +} + func (p *Position) Type() PositionType { if p.Base.Sign() > 0 { return PositionLong diff --git a/pkg/types/subscribe.go b/pkg/types/subscribe.go deleted file mode 100644 index 324dd04cb..000000000 --- a/pkg/types/subscribe.go +++ /dev/null @@ -1,5 +0,0 @@ -package types - -type Subscriber interface { - Subscribe() -}