mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +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:
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue
Block a user