feature: add config dump / param dump / param modify for elliottwave, refactor param dump

This commit is contained in:
zenix 2022-09-27 20:26:59 +09:00
parent 8d92d43710
commit b2875eedc5
5 changed files with 187 additions and 144 deletions

View File

@ -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())
}
}
}

View File

@ -1,101 +1,18 @@
package drift package drift
import ( import (
"fmt"
"io" "io"
"reflect"
"unsafe"
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
"github.com/c9s/bbgo/pkg/dynamic" "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) { func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) {
var style *table.Style var tableStyle *table.Style
if pretty { 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()...)
} }

View File

@ -16,6 +16,7 @@ import (
"github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/datatype/floats"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/interact" "github.com/c9s/bbgo/pkg/interact"
@ -148,11 +149,11 @@ func (s *Strategy) CurrentPosition() *types.Position {
return s.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) order := s.p.NewMarketCloseOrder(percentage)
if order == nil { if order == nil {
s.positionLock.Unlock() s.positionLock.Unlock()
return false return nil
} }
order.Tag = "close" order.Tag = "close"
order.TimeInForce = "" order.TimeInForce = ""
@ -171,14 +172,14 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
s.positionLock.Unlock() s.positionLock.Unlock()
for { for {
if s.Market.IsDustQuantity(order.Quantity, price) { if s.Market.IsDustQuantity(order.Quantity, price) {
return false return nil
} }
_, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order) _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order)
if err != nil { if err != nil {
order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta))
continue 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 || exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= pricef ||
s.trailingCheck(pricef, "long")) s.trailingCheck(pricef, "long"))
if exitShortCondition || exitLongCondition { if exitShortCondition || exitLongCondition {
if s.ClosePosition(ctx, fixedpoint.One) { s.ClosePosition(ctx, fixedpoint.One)
log.Infof("close position by orderbook changes") log.Infof("close position by orderbook changes")
}
} else { } else {
s.positionLock.Unlock() s.positionLock.Unlock()
} }
@ -993,9 +993,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
var buffer bytes.Buffer var buffer bytes.Buffer
l, err := strconv.Atoi(length) l, err := strconv.Atoi(length)
if err != nil { if err != nil {
s.ParamDump(&buffer) dynamic.ParamDump(s, &buffer)
} else { } else {
s.ParamDump(&buffer, l) dynamic.ParamDump(s, &buffer, l)
} }
reply.Message(buffer.String()) reply.Message(buffer.String())
}) })

View File

@ -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()...)
}

View File

@ -36,17 +36,18 @@ type SourceFunc func(*types.KLine) fixedpoint.Value
type Strategy struct { type Strategy struct {
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
bbgo.OpenPositionOptions
bbgo.StrategyController bbgo.StrategyController
bbgo.SourceSelector bbgo.SourceSelector
types.Market types.Market
Session *bbgo.ExchangeSession Session *bbgo.ExchangeSession
Interval types.Interval `json:"interval"` Interval types.Interval `json:"interval"`
Stoploss fixedpoint.Value `json:"stoploss"` Stoploss fixedpoint.Value `json:"stoploss" modifiable:"true"`
WindowATR int `json:"windowATR"` WindowATR int `json:"windowATR"`
WindowQuick int `json:"windowQuick"` WindowQuick int `json:"windowQuick"`
WindowSlow int `json:"windowSlow"` WindowSlow int `json:"windowSlow"`
PendingMinutes int `json:"pendingMinutes"` PendingMinutes int `json:"pendingMinutes" modifiable:"true"`
UseHeikinAshi bool `json:"useHeikinAshi"` UseHeikinAshi bool `json:"useHeikinAshi"`
// whether to draw graph or not by the end of backtest // whether to draw graph or not by the end of backtest
@ -80,8 +81,8 @@ type Strategy struct {
highestPrice float64 `persistence:"highest_price"` highestPrice float64 `persistence:"highest_price"`
lowestPrice float64 `persistence:"lowest_price"` lowestPrice float64 `persistence:"lowest_price"`
TrailingCallbackRate []float64 `json:"trailingCallbackRate"` TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"`
TrailingActivationRatio []float64 `json:"trailingActivationRatio"` TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"`
ExitMethods bbgo.ExitMethodSet `json:"exits"` ExitMethods bbgo.ExitMethodSet `json:"exits"`
midPrice fixedpoint.Value 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 { } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 {
order.Quantity = baseBalance order.Quantity = baseBalance
} }
order.MarginSideEffect = types.SideEffectTypeAutoRepay
for { for {
if s.Market.IsDustQuantity(order.Quantity, price) { if s.Market.IsDustQuantity(order.Quantity, price) {
return nil 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.Interval1d, startTime))
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime)) s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime))
s.initOutputCommands()
// event trigger order: s.Interval => Interval1m // event trigger order: s.Interval => Interval1m
store, ok := session.SerialMarketDataStore(s.Symbol, []types.Interval{s.Interval, types.Interval1m}) store, ok := session.SerialMarketDataStore(s.Symbol, []types.Interval{s.Interval, types.Interval1m})
if !ok { if !ok {
@ -380,6 +384,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
fmt.Fprintf(&buffer, "%s\n", daypnl) fmt.Fprintf(&buffer, "%s\n", daypnl)
} }
fmt.Fprintln(&buffer, s.TradeStats.BriefString()) fmt.Fprintln(&buffer, s.TradeStats.BriefString())
s.Print(&buffer, true, true)
os.Stdout.Write(buffer.Bytes()) os.Stdout.Write(buffer.Bytes())
if s.DrawGraph { if s.DrawGraph {
s.Draw(store, &profit, &cumProfit) s.Draw(store, &profit, &cumProfit)
@ -491,33 +496,23 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
} }
if source.Compare(price) > 0 { if source.Compare(price) > 0 {
source = price source = price
sourcef = source.Float64()
} }
balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() opt := s.OpenPositionOptions
quoteBalance, ok := balances[s.Market.QuoteCurrency] opt.Long = true
if !ok { opt.Price = source
log.Errorf("unable to get quoteCurrency") opt.Tags = []string{"long"}
return log.Infof("source in long %v %v", source, price)
} createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt)
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",
})
if err != nil { if err != nil {
log.WithError(err).Errorf("cannot place buy order") if _, ok := err.(types.ZeroAssetError); ok {
log.Errorf("%v %v %v", quoteBalance, source, kline)
return return
} }
log.WithError(err).Errorf("cannot place buy order: %v %v", source, kline)
return
}
if createdOrders != nil {
s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter
}
return return
} }
if shortCondition && !bull { if shortCondition && !bull {
@ -527,31 +522,23 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
} }
if source.Compare(price) < 0 { if source.Compare(price) < 0 {
source = price source = price
sourcef = price.Float64()
} }
balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() opt := s.OpenPositionOptions
baseBalance, ok := balances[s.Market.BaseCurrency] opt.Short = true
if !ok { opt.Price = source
log.Errorf("unable to get baseCurrency") opt.Tags = []string{"short"}
return log.Infof("source in short %v %v", source, price)
} createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt)
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",
})
if err != nil { if err != nil {
log.WithError(err).Errorf("cannot place sell order") if _, ok := err.(types.ZeroAssetError); ok {
return return
} }
log.WithError(err).Errorf("cannot place sell order: %v %v", source, kline)
return
}
if createdOrders != nil {
s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter
}
return return
} }
} }