diff --git a/config/support.yaml b/config/support.yaml index 6efbfa107..3e00cf978 100644 --- a/config/support.yaml +++ b/config/support.yaml @@ -59,8 +59,12 @@ exchangeStrategies: support: symbol: LINKUSDT interval: 1m - minVolume: 200000 - quantity: 2.0 + minVolume: 1_000 + scaleQuantity: + byVolume: + exp: + domain: [ 1_000, 200_000 ] + range: [ 0.5, 1.0 ] targets: - profitPercentage: 0.02 quantityPercentage: 0.5 diff --git a/pkg/bbgo/scale.go b/pkg/bbgo/scale.go index 542459399..13af5b7f1 100644 --- a/pkg/bbgo/scale.go +++ b/pkg/bbgo/scale.go @@ -3,8 +3,24 @@ package bbgo import ( "fmt" "math" + + "github.com/pkg/errors" ) +type Scale interface { + Solve() error + Formula() string + FormulaOf(x float64) string + Call(x float64) (y float64) +} + +func init() { + _ = Scale(&ExponentialScale{}) + _ = Scale(&LogarithmicScale{}) + _ = Scale(&LinearScale{}) + _ = Scale(&QuadraticScale{}) +} + // y := ab^x // shift xs[0] to 0 (x - h) // a = y1 @@ -14,7 +30,7 @@ import ( // y2/y1 = b^(x2-h) // // also posted at https://play.golang.org/p/JlWlwZjoebE -type ExpScale struct { +type ExponentialScale struct { Domain [2]float64 `json:"domain"` Range [2]float64 `json:"range"` @@ -23,26 +39,26 @@ type ExpScale struct { h float64 } -func (s *ExpScale) Solve() error { +func (s *ExponentialScale) Solve() error { s.h = s.Domain[0] s.a = s.Range[0] s.b = math.Pow(s.Range[1]/s.Range[0], 1/(s.Domain[1]-s.h)) return nil } -func (s *ExpScale) String() string { +func (s *ExponentialScale) String() string { return s.Formula() } -func (s *ExpScale) Formula() string { +func (s *ExponentialScale) Formula() string { return fmt.Sprintf("f(x) = %f * %f ^ (x - %f)", s.a, s.b, s.h) } -func (s *ExpScale) FormulaOf(x float64) string { +func (s *ExponentialScale) FormulaOf(x float64) string { return fmt.Sprintf("f(%f) = %f * %f ^ (%f - %f)", x, s.a, s.b, x, s.h) } -func (s *ExpScale) Call(x float64) (y float64) { +func (s *ExponentialScale) Call(x float64) (y float64) { if x < s.Domain[0] { x = s.Domain[0] } else if x > s.Domain[1] { @@ -53,7 +69,7 @@ func (s *ExpScale) Call(x float64) (y float64) { return y } -type LogScale struct { +type LogarithmicScale struct { Domain [2]float64 `json:"domain"` Range [2]float64 `json:"range"` @@ -62,7 +78,7 @@ type LogScale struct { a float64 } -func (s *LogScale) Call(x float64) (y float64) { +func (s *LogarithmicScale) Call(x float64) (y float64) { if x < s.Domain[0] { x = s.Domain[0] } else if x > s.Domain[1] { @@ -74,19 +90,19 @@ func (s *LogScale) Call(x float64) (y float64) { return y } -func (s *LogScale) String() string { +func (s *LogarithmicScale) String() string { return s.Formula() } -func (s *LogScale) Formula() string { +func (s *LogarithmicScale) Formula() string { return fmt.Sprintf("f(x) = %f * log(x - %f) + %f", s.a, s.h, s.s) } -func (s *LogScale) FormulaOf(x float64) string { +func (s *LogarithmicScale) FormulaOf(x float64) string { return fmt.Sprintf("f(%f) = %f * log(%f - %f) + %f", x, s.a, x, s.h, s.s) } -func (s *LogScale) Solve() error { +func (s *LogarithmicScale) Solve() error { // f(x) = a * log2(x - h) + s // // log2(1) = 0 @@ -134,7 +150,7 @@ func (s *LinearScale) Call(x float64) (y float64) { x = s.Domain[1] } - y = s.a * x + s.b + y = s.a*x + s.b return y } @@ -150,8 +166,6 @@ func (s *LinearScale) FormulaOf(x float64) string { return fmt.Sprintf("f(%f) = %f * %f + %f", x, s.a, x, s.b) } - - // see also: http://www.vb-helper.com/howto_find_quadratic_curve.html type QuadraticScale struct { Domain [3]float64 `json:"domain"` @@ -179,7 +193,7 @@ func (s *QuadraticScale) Call(x float64) (y float64) { } // y = a * log(x - h) + s - y = s.a * math.Pow(x, 2) + s.b * x + s.c + y = s.a*math.Pow(x, 2) + s.b*x + s.c return y } @@ -194,3 +208,100 @@ func (s *QuadraticScale) Formula() string { func (s *QuadraticScale) FormulaOf(x float64) string { return fmt.Sprintf("f(%f) = %f * %f ^ 2 + %f * %f + %f", x, s.a, x, s.b, x, s.c) } + +type SlideRule struct { + // Scale type could be one of "log", "exp", "linear", "quadratic" + // this is similar to the d3.scale + LinearScale *LinearScale `json:"linear"` + LogScale *LogarithmicScale `json:"log"` + ExpScale *ExponentialScale `json:"exp"` + QuadraticScale *QuadraticScale `json:"quadratic"` +} + +func (rule *SlideRule) Scale() (Scale, error) { + if rule.LogScale != nil { + return rule.LogScale, nil + } + + if rule.ExpScale != nil { + return rule.ExpScale, nil + } + + if rule.LinearScale != nil { + return rule.LinearScale, nil + } + + if rule.QuadraticScale != nil { + return rule.QuadraticScale, nil + } + + return nil, errors.New("no any scale is defined") +} + +// ScaleQuantity defines the scale DSL for strategy, e.g., +// +// scaleQuantity: +// byPrice: +// exp: +// domain: [10_000, 50_000] +// range: [0.01, 1.0] +// +// and +// +// scaleQuantity: +// byVolume: +// linear: +// domain: [10_000, 50_000] +// range: [0.01, 1.0] +type ScaleQuantity struct { + ByPrice *SlideRule `json:"byPrice"` + ByVolume *SlideRule `json:"byVolume"` +} + +func (q *ScaleQuantity) Scale(price float64, volume float64) (quantity float64, err error) { + if q.ByPrice != nil { + quantity, err = q.ScaleByPrice(price) + return + } else if q.ByVolume != nil { + quantity, err = q.ScaleByVolume(volume) + } else { + err = errors.New("either price or volume scale is not defined") + } + return +} + +// ScaleByPrice scale quantity by the given price +func (q *ScaleQuantity) ScaleByPrice(price float64) (float64, error) { + if q.ByPrice == nil { + return 0, errors.New("byPrice scale is not defined") + } + + scale, err := q.ByPrice.Scale() + if err != nil { + return 0, err + } + + if err := scale.Solve() ; err != nil { + return 0, err + } + + return scale.Call(price), nil +} + +// ScaleByVolume scale quantity by the given volume +func (q *ScaleQuantity) ScaleByVolume(volume float64) (float64, error) { + if q.ByVolume == nil { + return 0, errors.New("byVolume scale is not defined") + } + + scale, err := q.ByVolume.Scale() + if err != nil { + return 0, err + } + + if err := scale.Solve() ; err != nil { + return 0, err + } + + return scale.Call(volume), nil +} diff --git a/pkg/bbgo/scale_test.go b/pkg/bbgo/scale_test.go index 6847b7ecc..c616b1592 100644 --- a/pkg/bbgo/scale_test.go +++ b/pkg/bbgo/scale_test.go @@ -10,7 +10,7 @@ import ( func TestExpScale(t *testing.T) { // graph see: https://www.desmos.com/calculator/ip0ijbcbbf - scale := ExpScale{ + scale := ExponentialScale{ Domain: [2]float64{1000, 2000}, Range: [2]float64{0.001, 0.01}, } @@ -30,7 +30,7 @@ func TestExpScale(t *testing.T) { func TestLogScale(t *testing.T) { // see https://www.desmos.com/calculator/q1ufxx5gry - scale := LogScale{ + scale := LogarithmicScale{ Domain: [2]float64{1000, 2000}, Range: [2]float64{0.001, 0.01}, } diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index 7c8a878ef..87d84d8c8 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -56,7 +56,8 @@ type Strategy struct { LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"` // Quantity is the quantity you want to submit for each order. - Quantity fixedpoint.Value `json:"quantity,omitempty"` + Quantity fixedpoint.Value `json:"quantity,omitempty"` + ScaleQuantity *bbgo.ScaleQuantity `json:"scaleQuantity,omitempty"` // FixedAmount is used for fixed amount (dynamic quantity) if you don't want to use fixed quantity. FixedAmount fixedpoint.Value `json:"amount,omitempty" yaml:"amount"` @@ -120,14 +121,24 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type var orders []types.SubmitOrder for price := startPrice; s.LowerPrice <= price && price <= s.UpperPrice; price += gridSpread { - quantity := s.Quantity - if s.FixedAmount > 0 { + var quantity fixedpoint.Value + if s.Quantity > 0 { + quantity = s.Quantity + } else if s.ScaleQuantity != nil { + qf, err := s.ScaleQuantity.Scale(price.Float64(), 0) + if err != nil { + return nil, err + } + quantity = fixedpoint.NewFromFloat(qf) + } else if s.FixedAmount > 0 { quantity = s.FixedAmount.Div(price) } // quoteQuantity := price.Mul(quantity) if baseBalance.Available < quantity { - return orders, fmt.Errorf("base balance %f is not enough, stop generating orders", baseBalance.Available.Float64()) + return orders, fmt.Errorf("base balance %s %f is not enough, stop generating sell orders", + baseBalance.Currency, + baseBalance.Available.Float64()) } orders = append(orders, types.SubmitOrder{ @@ -185,14 +196,23 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types var orders []types.SubmitOrder for price := startPrice; s.LowerPrice <= price && price <= s.UpperPrice; price -= gridSpread { - quantity := s.Quantity - if s.FixedAmount > 0 { + var quantity fixedpoint.Value + if s.Quantity > 0 { + quantity = s.Quantity + } else if s.ScaleQuantity != nil { + qf, err := s.ScaleQuantity.Scale(price.Float64(), 0) + if err != nil { + return nil, err + } + quantity = fixedpoint.NewFromFloat(qf) + } else if s.FixedAmount > 0 { quantity = s.FixedAmount.Div(price) } quoteQuantity := price.Mul(quantity) if balance.Available < quoteQuantity { - return orders, fmt.Errorf("quote balance %f is not enough for %f, stop generating orders", + return orders, fmt.Errorf("quote balance %s %f is not enough for %f, stop generating buy orders", + balance.Currency, balance.Available.Float64(), quoteQuantity.Float64()) } @@ -324,7 +344,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Infof("position: %+v", position) - instanceID := fmt.Sprintf("grid-%s-%d", s.Symbol, s.GridNum) s.groupID = generateGroupID(instanceID) log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index 4dc0165b6..6680042fa 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -36,6 +36,8 @@ type Strategy struct { MinVolume fixedpoint.Value `json:"minVolume"` MarginOrderSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"` Targets []Target `json:"targets"` + + ScaleQuantity *bbgo.ScaleQuantity `json:"scaleQuantity"` } func (s *Strategy) ID() string { @@ -56,8 +58,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.MovingAverageWindow = 99 } - if s.Quantity == 0 { - return fmt.Errorf("quantity can not be zero") + if s.Quantity == 0 && s.ScaleQuantity == nil { + return fmt.Errorf("quantity or scaleQuantity can not be zero") } if s.MinVolume == 0 { @@ -95,7 +97,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Notify("found support: close price %f is under EMA %f, volume %f > minimum volume %f", closePrice, ema.Last(), kline.Volume, s.MinVolume.Float64()) - quantity := s.Quantity.Float64() + var quantity float64 + if s.Quantity > 0 { + quantity = s.Quantity.Float64() + } else if s.ScaleQuantity != nil { + var err error + quantity, err = s.ScaleQuantity.Scale(closePrice, kline.Volume) + if err != nil { + log.WithError(err).Error(err.Error()) + return + } + } orderForm := types.SubmitOrder{ Symbol: s.Symbol,