mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
integrate quantity scale into support strategy and grid strategy
This commit is contained in:
parent
bf87fbbf55
commit
99f236d2e0
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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},
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ type Strategy struct {
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user