integrate quantity scale into support strategy and grid strategy

This commit is contained in:
c9s 2021-02-28 11:57:25 +08:00
parent bf87fbbf55
commit 99f236d2e0
5 changed files with 177 additions and 31 deletions

View File

@ -59,8 +59,12 @@ exchangeStrategies:
support: support:
symbol: LINKUSDT symbol: LINKUSDT
interval: 1m interval: 1m
minVolume: 200000 minVolume: 1_000
quantity: 2.0 scaleQuantity:
byVolume:
exp:
domain: [ 1_000, 200_000 ]
range: [ 0.5, 1.0 ]
targets: targets:
- profitPercentage: 0.02 - profitPercentage: 0.02
quantityPercentage: 0.5 quantityPercentage: 0.5

View File

@ -3,8 +3,24 @@ package bbgo
import ( import (
"fmt" "fmt"
"math" "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 // y := ab^x
// shift xs[0] to 0 (x - h) // shift xs[0] to 0 (x - h)
// a = y1 // a = y1
@ -14,7 +30,7 @@ import (
// y2/y1 = b^(x2-h) // y2/y1 = b^(x2-h)
// //
// also posted at https://play.golang.org/p/JlWlwZjoebE // also posted at https://play.golang.org/p/JlWlwZjoebE
type ExpScale struct { type ExponentialScale struct {
Domain [2]float64 `json:"domain"` Domain [2]float64 `json:"domain"`
Range [2]float64 `json:"range"` Range [2]float64 `json:"range"`
@ -23,26 +39,26 @@ type ExpScale struct {
h float64 h float64
} }
func (s *ExpScale) Solve() error { func (s *ExponentialScale) Solve() error {
s.h = s.Domain[0] s.h = s.Domain[0]
s.a = s.Range[0] s.a = s.Range[0]
s.b = math.Pow(s.Range[1]/s.Range[0], 1/(s.Domain[1]-s.h)) s.b = math.Pow(s.Range[1]/s.Range[0], 1/(s.Domain[1]-s.h))
return nil return nil
} }
func (s *ExpScale) String() string { func (s *ExponentialScale) String() string {
return s.Formula() 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) 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) 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] { if x < s.Domain[0] {
x = s.Domain[0] x = s.Domain[0]
} else if x > s.Domain[1] { } else if x > s.Domain[1] {
@ -53,7 +69,7 @@ func (s *ExpScale) Call(x float64) (y float64) {
return y return y
} }
type LogScale struct { type LogarithmicScale struct {
Domain [2]float64 `json:"domain"` Domain [2]float64 `json:"domain"`
Range [2]float64 `json:"range"` Range [2]float64 `json:"range"`
@ -62,7 +78,7 @@ type LogScale struct {
a float64 a float64
} }
func (s *LogScale) Call(x float64) (y float64) { func (s *LogarithmicScale) Call(x float64) (y float64) {
if x < s.Domain[0] { if x < s.Domain[0] {
x = s.Domain[0] x = s.Domain[0]
} else if x > s.Domain[1] { } else if x > s.Domain[1] {
@ -74,19 +90,19 @@ func (s *LogScale) Call(x float64) (y float64) {
return y return y
} }
func (s *LogScale) String() string { func (s *LogarithmicScale) String() string {
return s.Formula() 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) 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) 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 // f(x) = a * log2(x - h) + s
// //
// log2(1) = 0 // log2(1) = 0
@ -134,7 +150,7 @@ func (s *LinearScale) Call(x float64) (y float64) {
x = s.Domain[1] x = s.Domain[1]
} }
y = s.a * x + s.b y = s.a*x + s.b
return y 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) 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 // see also: http://www.vb-helper.com/howto_find_quadratic_curve.html
type QuadraticScale struct { type QuadraticScale struct {
Domain [3]float64 `json:"domain"` Domain [3]float64 `json:"domain"`
@ -179,7 +193,7 @@ func (s *QuadraticScale) Call(x float64) (y float64) {
} }
// y = a * log(x - h) + s // 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 return y
} }
@ -194,3 +208,100 @@ func (s *QuadraticScale) Formula() string {
func (s *QuadraticScale) FormulaOf(x float64) 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) 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
}

View File

@ -10,7 +10,7 @@ import (
func TestExpScale(t *testing.T) { func TestExpScale(t *testing.T) {
// graph see: https://www.desmos.com/calculator/ip0ijbcbbf // graph see: https://www.desmos.com/calculator/ip0ijbcbbf
scale := ExpScale{ scale := ExponentialScale{
Domain: [2]float64{1000, 2000}, Domain: [2]float64{1000, 2000},
Range: [2]float64{0.001, 0.01}, Range: [2]float64{0.001, 0.01},
} }
@ -30,7 +30,7 @@ func TestExpScale(t *testing.T) {
func TestLogScale(t *testing.T) { func TestLogScale(t *testing.T) {
// see https://www.desmos.com/calculator/q1ufxx5gry // see https://www.desmos.com/calculator/q1ufxx5gry
scale := LogScale{ scale := LogarithmicScale{
Domain: [2]float64{1000, 2000}, Domain: [2]float64{1000, 2000},
Range: [2]float64{0.001, 0.01}, Range: [2]float64{0.001, 0.01},
} }

View File

@ -56,7 +56,8 @@ type Strategy struct {
LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"` LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"`
// Quantity is the quantity you want to submit for each order. // 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 is used for fixed amount (dynamic quantity) if you don't want to use fixed quantity.
FixedAmount fixedpoint.Value `json:"amount,omitempty" yaml:"amount"` FixedAmount fixedpoint.Value `json:"amount,omitempty" yaml:"amount"`
@ -120,14 +121,24 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type
var orders []types.SubmitOrder var orders []types.SubmitOrder
for price := startPrice; s.LowerPrice <= price && price <= s.UpperPrice; price += gridSpread { for price := startPrice; s.LowerPrice <= price && price <= s.UpperPrice; price += gridSpread {
quantity := s.Quantity var quantity fixedpoint.Value
if s.FixedAmount > 0 { 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) quantity = s.FixedAmount.Div(price)
} }
// quoteQuantity := price.Mul(quantity) // quoteQuantity := price.Mul(quantity)
if baseBalance.Available < 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{ orders = append(orders, types.SubmitOrder{
@ -185,14 +196,23 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types
var orders []types.SubmitOrder var orders []types.SubmitOrder
for price := startPrice; s.LowerPrice <= price && price <= s.UpperPrice; price -= gridSpread { for price := startPrice; s.LowerPrice <= price && price <= s.UpperPrice; price -= gridSpread {
quantity := s.Quantity var quantity fixedpoint.Value
if s.FixedAmount > 0 { 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) quantity = s.FixedAmount.Div(price)
} }
quoteQuantity := price.Mul(quantity) quoteQuantity := price.Mul(quantity)
if balance.Available < quoteQuantity { 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(), balance.Available.Float64(),
quoteQuantity.Float64()) quoteQuantity.Float64())
} }
@ -324,7 +344,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
log.Infof("position: %+v", position) log.Infof("position: %+v", position)
instanceID := fmt.Sprintf("grid-%s-%d", s.Symbol, s.GridNum) instanceID := fmt.Sprintf("grid-%s-%d", s.Symbol, s.GridNum)
s.groupID = generateGroupID(instanceID) s.groupID = generateGroupID(instanceID)
log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID)

View File

@ -36,6 +36,8 @@ type Strategy struct {
MinVolume fixedpoint.Value `json:"minVolume"` MinVolume fixedpoint.Value `json:"minVolume"`
MarginOrderSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"` MarginOrderSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
Targets []Target `json:"targets"` Targets []Target `json:"targets"`
ScaleQuantity *bbgo.ScaleQuantity `json:"scaleQuantity"`
} }
func (s *Strategy) ID() string { func (s *Strategy) ID() string {
@ -56,8 +58,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.MovingAverageWindow = 99 s.MovingAverageWindow = 99
} }
if s.Quantity == 0 { if s.Quantity == 0 && s.ScaleQuantity == nil {
return fmt.Errorf("quantity can not be zero") return fmt.Errorf("quantity or scaleQuantity can not be zero")
} }
if s.MinVolume == 0 { 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()) 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{ orderForm := types.SubmitOrder{
Symbol: s.Symbol, Symbol: s.Symbol,