diff --git a/pkg/dynamic/print_strategy.go b/pkg/dynamic/print_strategy.go new file mode 100644 index 000000000..5e1ff7062 --- /dev/null +++ b/pkg/dynamic/print_strategy.go @@ -0,0 +1,96 @@ +package dynamic + +import ( + "fmt" + "io" + "reflect" + "unsafe" + + "github.com/c9s/bbgo/pkg/util" +) + +// @param s: strategy object +// @param f: io.Writer used for writing the strategy dump +// @param seriesLength: if exist, the first value will be chosen to be the length of data from series to be printed out +// default to 1 when not exist or the value is invalid +func ParamDump(s interface{}, f io.Writer, seriesLength ...int) { + length := 1 + if len(seriesLength) > 0 && seriesLength[0] > 0 { + length = seriesLength[0] + } + val := reflect.ValueOf(s) + if val.Type().Kind() == util.Pointer { + val = val.Elem() + } + for i := 0; i < val.Type().NumField(); i++ { + t := val.Type().Field(i) + if ig := t.Tag.Get("ignore"); ig == "true" { + continue + } + field := val.Field(i) + if t.IsExported() || t.Anonymous || t.Type.Kind() == reflect.Func || t.Type.Kind() == reflect.Chan { + continue + } + fieldName := t.Name + typeName := field.Type().String() + value := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() + isSeries := true + lastFunc := value.MethodByName("Last") + isSeries = isSeries && lastFunc.IsValid() + lengthFunc := value.MethodByName("Length") + isSeries = isSeries && lengthFunc.IsValid() + indexFunc := value.MethodByName("Index") + isSeries = isSeries && indexFunc.IsValid() + + stringFunc := value.MethodByName("String") + canString := stringFunc.IsValid() + + if isSeries { + l := int(lengthFunc.Call(nil)[0].Int()) + if l >= length { + fmt.Fprintf(f, "%s: Series[..., %.4f", fieldName, indexFunc.Call([]reflect.Value{reflect.ValueOf(length - 1)})[0].Float()) + for j := length - 2; j >= 0; j-- { + fmt.Fprintf(f, ", %.4f", indexFunc.Call([]reflect.Value{reflect.ValueOf(j)})[0].Float()) + } + fmt.Fprintf(f, "]\n") + } else if l > 0 { + fmt.Fprintf(f, "%s: Series[%.4f", fieldName, indexFunc.Call([]reflect.Value{reflect.ValueOf(l - 1)})[0].Float()) + for j := l - 2; j >= 0; j-- { + fmt.Fprintf(f, ", %.4f", indexFunc.Call([]reflect.Value{reflect.ValueOf(j)})[0].Float()) + } + fmt.Fprintf(f, "]\n") + } else { + fmt.Fprintf(f, "%s: Series[]\n", fieldName) + } + } else if canString { + fmt.Fprintf(f, "%s: %s\n", fieldName, stringFunc.Call(nil)[0].String()) + } else if CanInt(field) { + fmt.Fprintf(f, "%s: %d\n", fieldName, field.Int()) + } else if field.CanConvert(reflect.TypeOf(float64(0))) { + fmt.Fprintf(f, "%s: %.4f\n", fieldName, field.Float()) + } else if field.CanInterface() { + fmt.Fprintf(f, "%s: %v", fieldName, field.Interface()) + } else if field.Type().Kind() == reflect.Map { + fmt.Fprintf(f, "%s: {", fieldName) + iter := value.MapRange() + for iter.Next() { + k := iter.Key().Interface() + v := iter.Value().Interface() + fmt.Fprintf(f, "%v: %v, ", k, v) + } + fmt.Fprintf(f, "}\n") + } else if field.Type().Kind() == reflect.Slice { + fmt.Fprintf(f, "%s: [", fieldName) + l := field.Len() + if l > 0 { + fmt.Fprintf(f, "%v", field.Index(0)) + } + for j := 1; j < field.Len(); j++ { + fmt.Fprintf(f, ", %v", field.Index(j)) + } + fmt.Fprintf(f, "]\n") + } else { + fmt.Fprintf(f, "%s(%s): %s\n", fieldName, typeName, field.String()) + } + } +} diff --git a/pkg/strategy/drift/output.go b/pkg/strategy/drift/output.go index e3f2d21f9..e078a1579 100644 --- a/pkg/strategy/drift/output.go +++ b/pkg/strategy/drift/output.go @@ -1,101 +1,18 @@ package drift import ( - "fmt" "io" - "reflect" - "unsafe" "github.com/jedib0t/go-pretty/v6/table" "github.com/c9s/bbgo/pkg/dynamic" - style2 "github.com/c9s/bbgo/pkg/style" + "github.com/c9s/bbgo/pkg/style" ) -func (s *Strategy) ParamDump(f io.Writer, seriesLength ...int) { - length := 1 - if len(seriesLength) > 0 && seriesLength[0] > 0 { - length = seriesLength[0] - } - val := reflect.ValueOf(s).Elem() - for i := 0; i < val.Type().NumField(); i++ { - t := val.Type().Field(i) - if ig := t.Tag.Get("ignore"); ig == "true" { - continue - } - field := val.Field(i) - if t.IsExported() || t.Anonymous || t.Type.Kind() == reflect.Func || t.Type.Kind() == reflect.Chan { - continue - } - fieldName := t.Name - typeName := field.Type().String() - log.Infof("fieldName %s typeName %s", fieldName, typeName) - value := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() - isSeries := true - lastFunc := value.MethodByName("Last") - isSeries = isSeries && lastFunc.IsValid() - lengthFunc := value.MethodByName("Length") - isSeries = isSeries && lengthFunc.IsValid() - indexFunc := value.MethodByName("Index") - isSeries = isSeries && indexFunc.IsValid() - - stringFunc := value.MethodByName("String") - canString := stringFunc.IsValid() - - if isSeries { - l := int(lengthFunc.Call(nil)[0].Int()) - if l >= length { - fmt.Fprintf(f, "%s: Series[..., %.4f", fieldName, indexFunc.Call([]reflect.Value{reflect.ValueOf(length - 1)})[0].Float()) - for j := length - 2; j >= 0; j-- { - fmt.Fprintf(f, ", %.4f", indexFunc.Call([]reflect.Value{reflect.ValueOf(j)})[0].Float()) - } - fmt.Fprintf(f, "]\n") - } else if l > 0 { - fmt.Fprintf(f, "%s: Series[%.4f", fieldName, indexFunc.Call([]reflect.Value{reflect.ValueOf(l - 1)})[0].Float()) - for j := l - 2; j >= 0; j-- { - fmt.Fprintf(f, ", %.4f", indexFunc.Call([]reflect.Value{reflect.ValueOf(j)})[0].Float()) - } - fmt.Fprintf(f, "]\n") - } else { - fmt.Fprintf(f, "%s: Series[]\n", fieldName) - } - } else if canString { - fmt.Fprintf(f, "%s: %s\n", fieldName, stringFunc.Call(nil)[0].String()) - } else if dynamic.CanInt(field) { - fmt.Fprintf(f, "%s: %d\n", fieldName, field.Int()) - } else if field.CanConvert(reflect.TypeOf(float64(0))) { - fmt.Fprintf(f, "%s: %.4f\n", fieldName, field.Float()) - } else if field.CanInterface() { - fmt.Fprintf(f, "%s: %v", fieldName, field.Interface()) - } else if field.Type().Kind() == reflect.Map { - fmt.Fprintf(f, "%s: {", fieldName) - iter := value.MapRange() - for iter.Next() { - k := iter.Key().Interface() - v := iter.Value().Interface() - fmt.Fprintf(f, "%v: %v, ", k, v) - } - fmt.Fprintf(f, "}\n") - } else if field.Type().Kind() == reflect.Slice { - fmt.Fprintf(f, "%s: [", fieldName) - l := field.Len() - if l > 0 { - fmt.Fprintf(f, "%v", field.Index(0)) - } - for j := 1; j < field.Len(); j++ { - fmt.Fprintf(f, ", %v", field.Index(j)) - } - fmt.Fprintf(f, "]\n") - } else { - fmt.Fprintf(f, "%s(%s): %s\n", fieldName, typeName, field.String()) - } - } -} - func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) { - var style *table.Style + var tableStyle *table.Style if pretty { - style = style2.NewDefaultTableStyle() + tableStyle = style.NewDefaultTableStyle() } - dynamic.PrintConfig(s, f, style, len(withColor) > 0 && withColor[0], dynamic.DefaultWhiteList()...) + dynamic.PrintConfig(s, f, tableStyle, len(withColor) > 0 && withColor[0], dynamic.DefaultWhiteList()...) } diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index fb49f689b..345f3f778 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -16,6 +16,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/interact" @@ -148,11 +149,11 @@ func (s *Strategy) CurrentPosition() *types.Position { return s.Position } -func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) bool { +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { order := s.p.NewMarketCloseOrder(percentage) if order == nil { s.positionLock.Unlock() - return false + return nil } order.Tag = "close" order.TimeInForce = "" @@ -171,14 +172,14 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu s.positionLock.Unlock() for { if s.Market.IsDustQuantity(order.Quantity, price) { - return false + return nil } _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order) if err != nil { order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) continue } - return true + return nil } } @@ -383,9 +384,8 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= pricef || s.trailingCheck(pricef, "long")) if exitShortCondition || exitLongCondition { - if s.ClosePosition(ctx, fixedpoint.One) { - log.Infof("close position by orderbook changes") - } + s.ClosePosition(ctx, fixedpoint.One) + log.Infof("close position by orderbook changes") } else { s.positionLock.Unlock() } @@ -993,9 +993,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se var buffer bytes.Buffer l, err := strconv.Atoi(length) if err != nil { - s.ParamDump(&buffer) + dynamic.ParamDump(s, &buffer) } else { - s.ParamDump(&buffer, l) + dynamic.ParamDump(s, &buffer, l) } reply.Message(buffer.String()) }) diff --git a/pkg/strategy/elliottwave/output.go b/pkg/strategy/elliottwave/output.go new file mode 100644 index 000000000..ebfac2ae4 --- /dev/null +++ b/pkg/strategy/elliottwave/output.go @@ -0,0 +1,43 @@ +package elliottwave + +import ( + "bytes" + "io" + "strconv" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/dynamic" + "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/style" + "github.com/jedib0t/go-pretty/v6/table" +) + +func (s *Strategy) initOutputCommands() { + bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) { + var buffer bytes.Buffer + s.Print(&buffer, false) + reply.Message(buffer.String()) + }) + bbgo.RegisterCommand("/dump", "Dump internal params", func(reply interact.Reply) { + reply.Message("Please enter series output length:") + }).Next(func(length string, reply interact.Reply) { + var buffer bytes.Buffer + l, err := strconv.Atoi(length) + if err != nil { + dynamic.ParamDump(s, &buffer) + } else { + dynamic.ParamDump(s, &buffer, l) + } + reply.Message(buffer.String()) + }) + + bbgo.RegisterModifier(s) +} + +func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) { + var tableStyle *table.Style + if pretty { + tableStyle = style.NewDefaultTableStyle() + } + dynamic.PrintConfig(s, f, tableStyle, len(withColor) > 0 && withColor[0], dynamic.DefaultWhiteList()...) +} diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go index 2a8f98cc3..bf9172e68 100644 --- a/pkg/strategy/elliottwave/strategy.go +++ b/pkg/strategy/elliottwave/strategy.go @@ -36,17 +36,18 @@ type SourceFunc func(*types.KLine) fixedpoint.Value type Strategy struct { Symbol string `json:"symbol"` + bbgo.OpenPositionOptions bbgo.StrategyController bbgo.SourceSelector types.Market Session *bbgo.ExchangeSession Interval types.Interval `json:"interval"` - Stoploss fixedpoint.Value `json:"stoploss"` + Stoploss fixedpoint.Value `json:"stoploss" modifiable:"true"` WindowATR int `json:"windowATR"` WindowQuick int `json:"windowQuick"` WindowSlow int `json:"windowSlow"` - PendingMinutes int `json:"pendingMinutes"` + PendingMinutes int `json:"pendingMinutes" modifiable:"true"` UseHeikinAshi bool `json:"useHeikinAshi"` // whether to draw graph or not by the end of backtest @@ -80,8 +81,8 @@ type Strategy struct { highestPrice float64 `persistence:"highest_price"` lowestPrice float64 `persistence:"lowest_price"` - TrailingCallbackRate []float64 `json:"trailingCallbackRate"` - TrailingActivationRatio []float64 `json:"trailingActivationRatio"` + TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"` + TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"` ExitMethods bbgo.ExitMethodSet `json:"exits"` midPrice fixedpoint.Value @@ -131,6 +132,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 { order.Quantity = baseBalance } + order.MarginSideEffect = types.SideEffectTypeAutoRepay for { if s.Market.IsDustQuantity(order.Quantity, price) { return nil @@ -355,6 +357,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime)) s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime)) + s.initOutputCommands() + // event trigger order: s.Interval => Interval1m store, ok := session.SerialMarketDataStore(s.Symbol, []types.Interval{s.Interval, types.Interval1m}) if !ok { @@ -380,6 +384,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se fmt.Fprintf(&buffer, "%s\n", daypnl) } fmt.Fprintln(&buffer, s.TradeStats.BriefString()) + s.Print(&buffer, true, true) os.Stdout.Write(buffer.Bytes()) if s.DrawGraph { s.Draw(store, &profit, &cumProfit) @@ -491,33 +496,23 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { } if source.Compare(price) > 0 { source = price - sourcef = source.Float64() } - balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() - quoteBalance, ok := balances[s.Market.QuoteCurrency] - if !ok { - log.Errorf("unable to get quoteCurrency") - return - } - if s.Market.IsDustQuantity( - quoteBalance.Available.Div(source), source) { - return - } - quantity := quoteBalance.Available.Div(source) - createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Price: source, - Quantity: quantity, - Tag: "long", - }) + opt := s.OpenPositionOptions + opt.Long = true + opt.Price = source + opt.Tags = []string{"long"} + log.Infof("source in long %v %v", source, price) + createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) if err != nil { - log.WithError(err).Errorf("cannot place buy order") - log.Errorf("%v %v %v", quoteBalance, source, kline) + if _, ok := err.(types.ZeroAssetError); ok { + return + } + log.WithError(err).Errorf("cannot place buy order: %v %v", source, kline) return } - s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + if createdOrders != nil { + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + } return } if shortCondition && !bull { @@ -527,31 +522,23 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { } if source.Compare(price) < 0 { source = price - sourcef = price.Float64() } - balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() - baseBalance, ok := balances[s.Market.BaseCurrency] - if !ok { - log.Errorf("unable to get baseCurrency") - return - } - if s.Market.IsDustQuantity(baseBalance.Available, source) { - return - } - quantity := baseBalance.Available - createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimit, - Price: source, - Quantity: quantity, - Tag: "short", - }) + opt := s.OpenPositionOptions + opt.Short = true + opt.Price = source + opt.Tags = []string{"short"} + log.Infof("source in short %v %v", source, price) + createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) if err != nil { - log.WithError(err).Errorf("cannot place sell order") + if _, ok := err.(types.ZeroAssetError); ok { + return + } + log.WithError(err).Errorf("cannot place sell order: %v %v", source, kline) return } - s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + if createdOrders != nil { + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + } return } }