From cb612a22b1adb361dab76039879859349dfee814 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 2 Nov 2022 18:26:30 +0800 Subject: [PATCH 01/59] add grid2 strategy --- pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/grid2/strategy.go | 164 +++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 pkg/strategy/grid2/strategy.go diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 1c29ab684..93334d9f0 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -17,6 +17,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/fmaker" _ "github.com/c9s/bbgo/pkg/strategy/funding" _ "github.com/c9s/bbgo/pkg/strategy/grid" + _ "github.com/c9s/bbgo/pkg/strategy/grid2" _ "github.com/c9s/bbgo/pkg/strategy/harmonic" _ "github.com/c9s/bbgo/pkg/strategy/irr" _ "github.com/c9s/bbgo/pkg/strategy/kline" diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go new file mode 100644 index 000000000..66613b53b --- /dev/null +++ b/pkg/strategy/grid2/strategy.go @@ -0,0 +1,164 @@ +package grid2 + +import ( + "context" + "fmt" + "sync" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +const ID = "grid2" + +var log = logrus.WithField("strategy", ID) + +var notionalModifier = fixedpoint.NewFromFloat(1.0001) + +func init() { + // Register the pointer of the strategy struct, + // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) + // Note: built-in strategies need to imported manually in the bbgo cmd package. + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc + // This field will be injected automatically since we defined the Symbol field. + types.Market `json:"-" yaml:"-"` + + // These fields will be filled from the config file (it translates YAML to JSON) + Symbol string `json:"symbol" yaml:"symbol"` + + // ProfitSpread is the fixed profit spread you want to submit the sell order + ProfitSpread fixedpoint.Value `json:"profitSpread" yaml:"profitSpread"` + + // GridNum is the grid number, how many orders you want to post on the orderbook. + GridNum int64 `json:"gridNumber" yaml:"gridNumber"` + + UpperPrice fixedpoint.Value `json:"upperPrice" yaml:"upperPrice"` + + LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"` + + bbgo.QuantityOrAmount + + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + Position *types.Position `persistence:"position"` + + // orderStore is used to store all the created orders, so that we can filter the trades. + orderStore *bbgo.OrderStore + + // activeOrders is the locally maintained active order book of the maker orders. + activeOrders *bbgo.ActiveOrderBook + + tradeCollector *bbgo.TradeCollector + + // groupID is the group ID used for the strategy instance for canceling orders + groupID uint32 +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Validate() error { + if s.UpperPrice.IsZero() { + return errors.New("upperPrice can not be zero, you forgot to set?") + } + + if s.LowerPrice.IsZero() { + return errors.New("lowerPrice can not be zero, you forgot to set?") + } + + if s.UpperPrice.Compare(s.LowerPrice) <= 0 { + return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) + } + + if s.ProfitSpread.Sign() <= 0 { + // If profitSpread is empty or its value is negative + return fmt.Errorf("profit spread should bigger than 0") + } + + if s.GridNum == 0 { + return fmt.Errorf("gridNum can not be zero") + } + + if err := s.QuantityOrAmount.Validate(); err != nil { + return err + } + + return nil +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) +} + +// InstanceID returns the instance identifier from the current grid configuration parameters +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s-%s-%d-%d-%d", ID, s.Symbol, s.GridNum, s.UpperPrice.Int(), s.LowerPrice.Int()) +} + +func (s *Strategy) handleOrderFilled(o types.Order) { + +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + + s.groupID = util.FNV32(instanceID) + + log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + s.orderStore = bbgo.NewOrderStore(s.Symbol) + s.orderStore.BindStream(session.UserDataStream) + + // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeOrders.OnFilled(s.handleOrderFilled) + s.activeOrders.BindStream(session.UserDataStream) + + s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) + + s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { + bbgo.Notify(trade) + s.ProfitStats.AddTrade(trade) + }) + + s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + bbgo.Notify(position) + }) + + s.tradeCollector.BindStream(session.UserDataStream) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + bbgo.Sync(ctx, s) + + // now we can cancel the open orders + log.Infof("canceling active orders...") + if err := session.Exchange.CancelOrders(context.Background(), s.activeOrders.Orders()...); err != nil { + log.WithError(err).Errorf("cancel order error") + } + }) + + session.UserDataStream.OnStart(func() { + + }) + + return nil +} From 21a1d550e3f9315d1315e5aaddf74b7bfa120bba Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 3 Nov 2022 13:41:47 +0800 Subject: [PATCH 02/59] grid2: add grid struct --- pkg/strategy/grid2/grid.go | 102 ++++++++++++++++++++++++++++++++ pkg/strategy/grid2/grid_test.go | 81 +++++++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 pkg/strategy/grid2/grid.go create mode 100644 pkg/strategy/grid2/grid_test.go diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go new file mode 100644 index 000000000..73c142e25 --- /dev/null +++ b/pkg/strategy/grid2/grid.go @@ -0,0 +1,102 @@ +package grid2 + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type Grid struct { + UpperPrice fixedpoint.Value `json:"upperPrice"` + LowerPrice fixedpoint.Value `json:"lowerPrice"` + + // Spread is the spread of each grid + Spread fixedpoint.Value `json:"spread"` + + // Size is the number of total grids + Size fixedpoint.Value `json:"size"` + + // Pins are the pinned grid prices, from low to high + Pins []fixedpoint.Value `json:"pins"` + + pinsCache map[fixedpoint.Value]struct{} `json:"-"` +} + +func NewGrid(lower, upper, density fixedpoint.Value) *Grid { + var height = upper - lower + var size = height.Div(density) + var pins []fixedpoint.Value + + for p := lower; p <= upper; p += size { + pins = append(pins, p) + } + + grid := &Grid{ + UpperPrice: upper, + LowerPrice: lower, + Size: density, + Spread: size, + Pins: pins, + pinsCache: make(map[fixedpoint.Value]struct{}, len(pins)), + } + grid.updatePinsCache() + return grid +} + +func (g *Grid) Above(price fixedpoint.Value) bool { + return price > g.UpperPrice +} + +func (g *Grid) Below(price fixedpoint.Value) bool { + return price < g.LowerPrice +} + +func (g *Grid) OutOfRange(price fixedpoint.Value) bool { + return price < g.LowerPrice || price > g.UpperPrice +} + +func (g *Grid) updatePinsCache() { + for _, pin := range g.Pins { + g.pinsCache[pin] = struct{}{} + } +} + +func (g *Grid) HasPin(pin fixedpoint.Value) (ok bool) { + _, ok = g.pinsCache[pin] + return ok +} + +func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []fixedpoint.Value) { + g.UpperPrice = upper + + // since the grid is extended, the size should be updated as well + g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() + + lastPin := g.Pins[len(g.Pins)-1] + for p := lastPin + g.Spread; p <= g.UpperPrice; p += g.Spread { + newPins = append(newPins, p) + } + + g.Pins = append(g.Pins, newPins...) + g.updatePinsCache() + return newPins +} + +func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []fixedpoint.Value) { + g.LowerPrice = lower + + // since the grid is extended, the size should be updated as well + g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() + + firstPin := g.Pins[0] + numToAdd := (firstPin - g.LowerPrice).Div(g.Spread).Floor() + if numToAdd == 0 { + return newPins + } + + for p := firstPin - g.Spread.Mul(numToAdd); p < firstPin; p += g.Spread { + newPins = append(newPins, p) + } + + g.Pins = append(newPins, g.Pins...) + g.updatePinsCache() + return newPins +} diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go new file mode 100644 index 000000000..3ed9d0e4e --- /dev/null +++ b/pkg/strategy/grid2/grid_test.go @@ -0,0 +1,81 @@ +package grid2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +func TestNewGrid(t *testing.T) { + upper := fixedpoint.NewFromFloat(500.0) + lower := fixedpoint.NewFromFloat(100.0) + size := fixedpoint.NewFromFloat(100.0) + grid := NewGrid(lower, upper, size) + assert.Equal(t, upper, grid.UpperPrice) + assert.Equal(t, lower, grid.LowerPrice) + assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) + if assert.Len(t, grid.Pins, 101) { + assert.Equal(t, fixedpoint.NewFromFloat(100.0), grid.Pins[0]) + assert.Equal(t, fixedpoint.NewFromFloat(500.0), grid.Pins[100]) + } +} + +func TestGrid_HasPin(t *testing.T) { + upper := fixedpoint.NewFromFloat(500.0) + lower := fixedpoint.NewFromFloat(100.0) + size := fixedpoint.NewFromFloat(100.0) + grid := NewGrid(lower, upper, size) + + assert.True(t, grid.HasPin(fixedpoint.NewFromFloat(100.0))) + assert.True(t, grid.HasPin(fixedpoint.NewFromFloat(500.0))) + assert.False(t, grid.HasPin(fixedpoint.NewFromFloat(101.0))) +} + +func TestGrid_ExtendUpperPrice(t *testing.T) { + upper := fixedpoint.NewFromFloat(500.0) + lower := fixedpoint.NewFromFloat(100.0) + size := fixedpoint.NewFromFloat(100.0) + grid := NewGrid(lower, upper, size) + + originalSpread := grid.Spread + newPins := grid.ExtendUpperPrice(fixedpoint.NewFromFloat(1000.0)) + assert.Equal(t, originalSpread, grid.Spread) + assert.Len(t, newPins, 125) // (1000-500) / 4 + assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) + if assert.Len(t, grid.Pins, 226) { + assert.Equal(t, fixedpoint.NewFromFloat(100.0), grid.Pins[0]) + assert.Equal(t, fixedpoint.NewFromFloat(1000.0), grid.Pins[225]) + } +} + +func TestGrid_ExtendLowerPrice(t *testing.T) { + upper := fixedpoint.NewFromFloat(3000.0) + lower := fixedpoint.NewFromFloat(2000.0) + size := fixedpoint.NewFromFloat(100.0) + grid := NewGrid(lower, upper, size) + + // spread = (3000 - 2000) / 100.0 + expectedSpread := fixedpoint.NewFromFloat(10.0) + assert.Equal(t, expectedSpread, grid.Spread) + + originalSpread := grid.Spread + newPins := grid.ExtendLowerPrice(fixedpoint.NewFromFloat(1000.0)) + assert.Equal(t, originalSpread, grid.Spread) + + // 100 = (2000-1000) / 10 + if assert.Len(t, newPins, 100) { + assert.Equal(t, fixedpoint.NewFromFloat(2000.0)-expectedSpread, newPins[99]) + } + + assert.Equal(t, expectedSpread, grid.Spread) + if assert.Len(t, grid.Pins, 201) { + assert.Equal(t, fixedpoint.NewFromFloat(1000.0), grid.Pins[0]) + assert.Equal(t, fixedpoint.NewFromFloat(3000.0), grid.Pins[200]) + } + + newPins2 := grid.ExtendLowerPrice( + fixedpoint.NewFromFloat(1000.0 - 1.0)) + assert.Len(t, newPins2, 0) // should have no new pin generated +} From 2761cff2bf126866ff53830f89c56f59dc991a5b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 4 Nov 2022 00:26:28 +0800 Subject: [PATCH 03/59] grid2: add pin tests --- pkg/strategy/grid2/grid.go | 77 +++++++++++++++++---------- pkg/strategy/grid2/grid_test.go | 93 +++++++++++++++++++++++++++++---- pkg/types/market.go | 4 +- pkg/types/market_test.go | 10 ++-- 4 files changed, 138 insertions(+), 46 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 73c142e25..054e8f17c 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -1,6 +1,8 @@ package grid2 import ( + "math" + "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -15,64 +17,83 @@ type Grid struct { Size fixedpoint.Value `json:"size"` // Pins are the pinned grid prices, from low to high - Pins []fixedpoint.Value `json:"pins"` + Pins []Pin `json:"pins"` - pinsCache map[fixedpoint.Value]struct{} `json:"-"` + pinsCache map[Pin]struct{} `json:"-"` } -func NewGrid(lower, upper, density fixedpoint.Value) *Grid { - var height = upper - lower - var size = height.Div(density) - var pins []fixedpoint.Value +type Pin fixedpoint.Value - for p := lower; p <= upper; p += size { - pins = append(pins, p) +func calculateArithmeticPins(lower, upper, size, tickSize fixedpoint.Value) []Pin { + var height = upper.Sub(lower) + var spread = height.Div(size) + + var pins []Pin + for p := lower; p.Compare(upper) <= 0; p = p.Add(spread) { + // tickSize here = 0.01 + pp := math.Trunc(p.Float64()/tickSize.Float64()) * tickSize.Float64() + pins = append(pins, Pin(fixedpoint.NewFromFloat(pp))) } + return pins +} + +func buildPinCache(pins []Pin) map[Pin]struct{} { + cache := make(map[Pin]struct{}, len(pins)) + for _, pin := range pins { + cache[pin] = struct{}{} + } + + return cache +} + +func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { + var height = upper.Sub(lower) + var spread = height.Div(size) + var pins = calculateArithmeticPins(lower, upper, size, tickSize) + grid := &Grid{ UpperPrice: upper, LowerPrice: lower, - Size: density, - Spread: size, + Size: size, + Spread: spread, Pins: pins, - pinsCache: make(map[fixedpoint.Value]struct{}, len(pins)), + pinsCache: buildPinCache(pins), } - grid.updatePinsCache() + return grid } func (g *Grid) Above(price fixedpoint.Value) bool { - return price > g.UpperPrice + return price.Compare(g.UpperPrice) > 0 } func (g *Grid) Below(price fixedpoint.Value) bool { - return price < g.LowerPrice + return price.Compare(g.LowerPrice) < 0 } func (g *Grid) OutOfRange(price fixedpoint.Value) bool { - return price < g.LowerPrice || price > g.UpperPrice + return price.Compare(g.LowerPrice) < 0 || price.Compare(g.UpperPrice) > 0 } func (g *Grid) updatePinsCache() { - for _, pin := range g.Pins { - g.pinsCache[pin] = struct{}{} - } + g.pinsCache = buildPinCache(g.Pins) } -func (g *Grid) HasPin(pin fixedpoint.Value) (ok bool) { +func (g *Grid) HasPin(pin Pin) (ok bool) { _, ok = g.pinsCache[pin] return ok } -func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []fixedpoint.Value) { +func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { g.UpperPrice = upper // since the grid is extended, the size should be updated as well g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() - lastPin := g.Pins[len(g.Pins)-1] - for p := lastPin + g.Spread; p <= g.UpperPrice; p += g.Spread { - newPins = append(newPins, p) + lastPinPrice := fixedpoint.Value(g.Pins[len(g.Pins)-1]) + for p := lastPinPrice.Add(g.Spread); p <= g.UpperPrice; p += g.Spread { + newPins = append(newPins, Pin(p)) } g.Pins = append(g.Pins, newPins...) @@ -80,20 +101,20 @@ func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []fixedpoint.Va return newPins } -func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []fixedpoint.Value) { +func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) { g.LowerPrice = lower // since the grid is extended, the size should be updated as well g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() - firstPin := g.Pins[0] - numToAdd := (firstPin - g.LowerPrice).Div(g.Spread).Floor() + firstPinPrice := fixedpoint.Value(g.Pins[0]) + numToAdd := (firstPinPrice.Sub(g.LowerPrice)).Div(g.Spread).Floor() if numToAdd == 0 { return newPins } - for p := firstPin - g.Spread.Mul(numToAdd); p < firstPin; p += g.Spread { - newPins = append(newPins, p) + for p := firstPinPrice.Sub(g.Spread.Mul(numToAdd)); p.Compare(firstPinPrice) < 0; p = p.Add(g.Spread) { + newPins = append(newPins, Pin(p)) } g.Pins = append(newPins, g.Pins...) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 3ed9d0e4e..12992ea8a 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -8,11 +8,20 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) +func number(a interface{}) fixedpoint.Value { + if s, ok := a.(string); ok { + return fixedpoint.MustNewFromString(s) + } + + f := a.(float64) + return fixedpoint.NewFromFloat(f) +} + func TestNewGrid(t *testing.T) { upper := fixedpoint.NewFromFloat(500.0) lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size) + grid := NewGrid(lower, upper, size, number(2.0)) assert.Equal(t, upper, grid.UpperPrice) assert.Equal(t, lower, grid.LowerPrice) assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) @@ -26,21 +35,21 @@ func TestGrid_HasPin(t *testing.T) { upper := fixedpoint.NewFromFloat(500.0) lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size) + grid := NewGrid(lower, upper, size, number(2)) - assert.True(t, grid.HasPin(fixedpoint.NewFromFloat(100.0))) - assert.True(t, grid.HasPin(fixedpoint.NewFromFloat(500.0))) - assert.False(t, grid.HasPin(fixedpoint.NewFromFloat(101.0))) + assert.True(t, grid.HasPin(Pin(number(100.0)))) + assert.True(t, grid.HasPin(Pin(number(500.0)))) + assert.False(t, grid.HasPin(Pin(number(101.0)))) } func TestGrid_ExtendUpperPrice(t *testing.T) { - upper := fixedpoint.NewFromFloat(500.0) - lower := fixedpoint.NewFromFloat(100.0) - size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size) + upper := number(500.0) + lower := number(100.0) + size := number(100.0) + grid := NewGrid(lower, upper, size, number(2.0)) originalSpread := grid.Spread - newPins := grid.ExtendUpperPrice(fixedpoint.NewFromFloat(1000.0)) + newPins := grid.ExtendUpperPrice(number(1000.0)) assert.Equal(t, originalSpread, grid.Spread) assert.Len(t, newPins, 125) // (1000-500) / 4 assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) @@ -54,7 +63,7 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { upper := fixedpoint.NewFromFloat(3000.0) lower := fixedpoint.NewFromFloat(2000.0) size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size) + grid := NewGrid(lower, upper, size, number(2.0)) // spread = (3000 - 2000) / 100.0 expectedSpread := fixedpoint.NewFromFloat(10.0) @@ -79,3 +88,65 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { fixedpoint.NewFromFloat(1000.0 - 1.0)) assert.Len(t, newPins2, 0) // should have no new pin generated } + +func Test_calculateArithmeticPins(t *testing.T) { + type args struct { + lower fixedpoint.Value + upper fixedpoint.Value + size fixedpoint.Value + tickSize fixedpoint.Value + } + tests := []struct { + name string + args args + want []Pin + }{ + { + name: "simple", + args: args{ + lower: number(1000.0), + upper: number(3000.0), + size: number(30.0), + tickSize: number(0.01), + }, + want: []Pin{ + Pin(number(1000.0)), + Pin(number(1066.660)), + Pin(number(1133.330)), + Pin(number(1199.990)), + Pin(number(1266.660)), + Pin(number(1333.330)), + Pin(number(1399.990)), + Pin(number(1466.660)), + Pin(number(1533.330)), + Pin(number(1599.990)), + Pin(number(1666.660)), + Pin(number(1733.330)), + Pin(number(1799.990)), + Pin(number(1866.660)), + Pin(number(1933.330)), + Pin(number(1999.990)), + Pin(number(2066.660)), + Pin(number(2133.330)), + Pin(number("2199.99")), + Pin(number(2266.660)), + Pin(number(2333.330)), + Pin(number("2399.99")), + Pin(number(2466.660)), + Pin(number(2533.330)), + Pin(number("2599.99")), + Pin(number(2666.660)), + Pin(number(2733.330)), + Pin(number(2799.990)), + Pin(number(2866.660)), + Pin(number(2933.330)), + Pin(number(2999.990)), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, calculateArithmeticPins(tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize), "calculateArithmeticPins(%v, %v, %v, %v)", tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize) + }) + } +} diff --git a/pkg/types/market.go b/pkg/types/market.go index 1092b441e..e2b08cd21 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -150,10 +150,10 @@ func (m Market) FormatPriceCurrency(val fixedpoint.Value) string { func (m Market) FormatPrice(val fixedpoint.Value) string { // p := math.Pow10(m.PricePrecision) - return formatPrice(val, m.TickSize) + return FormatPrice(val, m.TickSize) } -func formatPrice(price fixedpoint.Value, tickSize fixedpoint.Value) string { +func FormatPrice(price fixedpoint.Value, tickSize fixedpoint.Value) string { prec := int(math.Round(math.Abs(math.Log10(tickSize.Float64())))) return price.FormatString(prec) } diff --git a/pkg/types/market_test.go b/pkg/types/market_test.go index d0544e9ba..809e60b0d 100644 --- a/pkg/types/market_test.go +++ b/pkg/types/market_test.go @@ -26,12 +26,12 @@ func TestFormatQuantity(t *testing.T) { } func TestFormatPrice(t *testing.T) { - price := formatPrice( + price := FormatPrice( s("26.288256"), s("0.0001")) assert.Equal(t, "26.2882", price) - price = formatPrice(s("26.288656"), s("0.001")) + price = FormatPrice(s("26.288656"), s("0.001")) assert.Equal(t, "26.288", price) } @@ -78,7 +78,7 @@ func TestDurationParse(t *testing.T) { } } -func Test_formatPrice(t *testing.T) { +func Test_FormatPrice(t *testing.T) { type args struct { price fixedpoint.Value tickSize fixedpoint.Value @@ -125,9 +125,9 @@ func Test_formatPrice(t *testing.T) { binanceFormatRE := regexp.MustCompile("^([0-9]{1,20})(.[0-9]{1,20})?$") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := formatPrice(tt.args.price, tt.args.tickSize) + got := FormatPrice(tt.args.price, tt.args.tickSize) if got != tt.want { - t.Errorf("formatPrice() = %v, want %v", got, tt.want) + t.Errorf("FormatPrice() = %v, want %v", got, tt.want) } assert.Regexp(t, binanceFormatRE, got) From e675a084e2c1ddc61ee55e903a64977b2429e486 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Nov 2022 10:06:57 +0800 Subject: [PATCH 04/59] grid2: refactor spread, height methods --- pkg/strategy/grid2/grid.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 054e8f17c..555223fab 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -10,9 +10,6 @@ type Grid struct { UpperPrice fixedpoint.Value `json:"upperPrice"` LowerPrice fixedpoint.Value `json:"lowerPrice"` - // Spread is the spread of each grid - Spread fixedpoint.Value `json:"spread"` - // Size is the number of total grids Size fixedpoint.Value `json:"size"` @@ -48,15 +45,12 @@ func buildPinCache(pins []Pin) map[Pin]struct{} { } func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { - var height = upper.Sub(lower) - var spread = height.Div(size) var pins = calculateArithmeticPins(lower, upper, size, tickSize) grid := &Grid{ UpperPrice: upper, LowerPrice: lower, Size: size, - Spread: spread, Pins: pins, pinsCache: buildPinCache(pins), } @@ -64,6 +58,15 @@ func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { return grid } +func (g *Grid) Height() fixedpoint.Value { + return g.UpperPrice.Sub(g.LowerPrice) +} + +// Spread returns the spread of each grid +func (g *Grid) Spread() fixedpoint.Value { + return g.Height().Div(g.Size) +} + func (g *Grid) Above(price fixedpoint.Value) bool { return price.Compare(g.UpperPrice) > 0 } @@ -89,10 +92,11 @@ func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { g.UpperPrice = upper // since the grid is extended, the size should be updated as well - g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() + spread := g.Spread() + g.Size = (g.UpperPrice - g.LowerPrice).Div(spread).Floor() lastPinPrice := fixedpoint.Value(g.Pins[len(g.Pins)-1]) - for p := lastPinPrice.Add(g.Spread); p <= g.UpperPrice; p += g.Spread { + for p := lastPinPrice.Add(spread); p <= g.UpperPrice; p = p.Add(spread) { newPins = append(newPins, Pin(p)) } @@ -102,18 +106,22 @@ func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { } func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) { + spread := g.Spread() + g.LowerPrice = lower + height := g.Height() + // since the grid is extended, the size should be updated as well - g.Size = (g.UpperPrice - g.LowerPrice).Div(g.Spread).Floor() + g.Size = height.Div(spread).Floor() firstPinPrice := fixedpoint.Value(g.Pins[0]) - numToAdd := (firstPinPrice.Sub(g.LowerPrice)).Div(g.Spread).Floor() + numToAdd := (firstPinPrice.Sub(g.LowerPrice)).Div(spread).Floor() if numToAdd == 0 { return newPins } - for p := firstPinPrice.Sub(g.Spread.Mul(numToAdd)); p.Compare(firstPinPrice) < 0; p = p.Add(g.Spread) { + for p := firstPinPrice.Sub(spread.Mul(numToAdd)); p.Compare(firstPinPrice) < 0; p = p.Add(spread) { newPins = append(newPins, Pin(p)) } From d6f751c027b6eca0ab5d958fe1819424ff06caf3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Nov 2022 10:12:50 +0800 Subject: [PATCH 05/59] grid2: improve ExtendLowerPrice --- pkg/strategy/grid2/grid.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 555223fab..7e5d78aec 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -2,6 +2,7 @@ package grid2 import ( "math" + "sort" "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -108,24 +109,23 @@ func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) { spread := g.Spread() - g.LowerPrice = lower - - height := g.Height() - - // since the grid is extended, the size should be updated as well - g.Size = height.Div(spread).Floor() - - firstPinPrice := fixedpoint.Value(g.Pins[0]) - numToAdd := (firstPinPrice.Sub(g.LowerPrice)).Div(spread).Floor() - if numToAdd == 0 { - return newPins - } - - for p := firstPinPrice.Sub(spread.Mul(numToAdd)); p.Compare(firstPinPrice) < 0; p = p.Add(spread) { + startPrice := g.LowerPrice.Sub(spread) + for p := startPrice; p.Compare(lower) >= 0; p = p.Sub(spread) { newPins = append(newPins, Pin(p)) } - g.Pins = append(newPins, g.Pins...) - g.updatePinsCache() + g.addPins(newPins) return newPins } + +func (g *Grid) addPins(pins []Pin) { + g.Pins = append(g.Pins, pins...) + + sort.Slice(g.Pins, func(i, j int) bool { + a := fixedpoint.Value(g.Pins[i]) + b := fixedpoint.Value(g.Pins[j]) + return a.Compare(b) < 0 + }) + + g.updatePinsCache() +} From 533587ffd2091f1be77f640d9cecf51dd4cc391d Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Nov 2022 10:13:13 +0800 Subject: [PATCH 06/59] grid2: update lowerPrice --- pkg/strategy/grid2/grid.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 7e5d78aec..bc11d99b2 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -114,6 +114,7 @@ func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) { newPins = append(newPins, Pin(p)) } + g.LowerPrice = lower g.addPins(newPins) return newPins } From 725c624281d253c2272c904f64a3648176fd022b Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 6 Nov 2022 10:14:07 +0800 Subject: [PATCH 07/59] grid2: rewrite ExtendUpperPrice --- pkg/strategy/grid2/grid.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index bc11d99b2..c6670228d 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -90,19 +90,15 @@ func (g *Grid) HasPin(pin Pin) (ok bool) { } func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { - g.UpperPrice = upper - - // since the grid is extended, the size should be updated as well spread := g.Spread() - g.Size = (g.UpperPrice - g.LowerPrice).Div(spread).Floor() - lastPinPrice := fixedpoint.Value(g.Pins[len(g.Pins)-1]) - for p := lastPinPrice.Add(spread); p <= g.UpperPrice; p = p.Add(spread) { + startPrice := g.UpperPrice.Add(spread) + for p := startPrice; p.Compare(upper) <= 0; p = p.Add(spread) { newPins = append(newPins, Pin(p)) } - g.Pins = append(g.Pins, newPins...) - g.updatePinsCache() + g.UpperPrice = upper + g.addPins(newPins) return newPins } From 75c088eb9c1db4b370d209ed81742a8105a1ee1f Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 7 Nov 2022 13:51:44 +0800 Subject: [PATCH 08/59] refactor calculateArithmeticPins --- pkg/strategy/grid2/grid.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index c6670228d..baed325f2 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -14,6 +14,9 @@ type Grid struct { // Size is the number of total grids Size fixedpoint.Value `json:"size"` + // TickSize is the price tick size, this is used for truncating price + TickSize fixedpoint.Value `json:"tickSize"` + // Pins are the pinned grid prices, from low to high Pins []Pin `json:"pins"` @@ -22,10 +25,7 @@ type Grid struct { type Pin fixedpoint.Value -func calculateArithmeticPins(lower, upper, size, tickSize fixedpoint.Value) []Pin { - var height = upper.Sub(lower) - var spread = height.Div(size) - +func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) []Pin { var pins []Pin for p := lower; p.Compare(upper) <= 0; p = p.Add(spread) { // tickSize here = 0.01 @@ -46,16 +46,16 @@ func buildPinCache(pins []Pin) map[Pin]struct{} { } func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { - var pins = calculateArithmeticPins(lower, upper, size, tickSize) - grid := &Grid{ UpperPrice: upper, LowerPrice: lower, Size: size, - Pins: pins, - pinsCache: buildPinCache(pins), + TickSize: tickSize, } + var spread = grid.Spread() + var pins = calculateArithmeticPins(lower, upper, spread, tickSize) + grid.addPins(pins) return grid } @@ -80,10 +80,6 @@ func (g *Grid) OutOfRange(price fixedpoint.Value) bool { return price.Compare(g.LowerPrice) < 0 || price.Compare(g.UpperPrice) > 0 } -func (g *Grid) updatePinsCache() { - g.pinsCache = buildPinCache(g.Pins) -} - func (g *Grid) HasPin(pin Pin) (ok bool) { _, ok = g.pinsCache[pin] return ok @@ -126,3 +122,7 @@ func (g *Grid) addPins(pins []Pin) { g.updatePinsCache() } + +func (g *Grid) updatePinsCache() { + g.pinsCache = buildPinCache(g.Pins) +} From f98c00b7aa46deb30db00db165bd9d44ee0f0484 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Nov 2022 16:14:04 +0800 Subject: [PATCH 09/59] grid2: fix extendLowerPrice method and tests --- pkg/strategy/grid2/grid.go | 40 +++++++++++++++------------- pkg/strategy/grid2/grid_test.go | 46 +++++++++++++++++---------------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index baed325f2..d075ed2be 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -17,6 +17,9 @@ type Grid struct { // TickSize is the price tick size, this is used for truncating price TickSize fixedpoint.Value `json:"tickSize"` + // Spread is a immutable number + Spread fixedpoint.Value `json:"spread"` + // Pins are the pinned grid prices, from low to high Pins []Pin `json:"pins"` @@ -46,14 +49,17 @@ func buildPinCache(pins []Pin) map[Pin]struct{} { } func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { + height := upper.Sub(lower) + spread := height.Div(size) + grid := &Grid{ UpperPrice: upper, LowerPrice: lower, Size: size, TickSize: tickSize, + Spread: spread, } - var spread = grid.Spread() var pins = calculateArithmeticPins(lower, upper, spread, tickSize) grid.addPins(pins) return grid @@ -63,11 +69,6 @@ func (g *Grid) Height() fixedpoint.Value { return g.UpperPrice.Sub(g.LowerPrice) } -// Spread returns the spread of each grid -func (g *Grid) Spread() fixedpoint.Value { - return g.Height().Div(g.Size) -} - func (g *Grid) Above(price fixedpoint.Value) bool { return price.Compare(g.UpperPrice) > 0 } @@ -86,31 +87,34 @@ func (g *Grid) HasPin(pin Pin) (ok bool) { } func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { - spread := g.Spread() - - startPrice := g.UpperPrice.Add(spread) - for p := startPrice; p.Compare(upper) <= 0; p = p.Add(spread) { - newPins = append(newPins, Pin(p)) - } - + newPins = calculateArithmeticPins(g.UpperPrice, upper, g.Spread, g.TickSize) g.UpperPrice = upper g.addPins(newPins) return newPins } func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) { - spread := g.Spread() - - startPrice := g.LowerPrice.Sub(spread) - for p := startPrice; p.Compare(lower) >= 0; p = p.Sub(spread) { - newPins = append(newPins, Pin(p)) + if lower.Compare(g.LowerPrice) >= 0 { + return nil } + n := g.LowerPrice.Sub(lower).Div(g.Spread).Floor() + lower = g.LowerPrice.Sub(g.Spread.Mul(n)) + newPins = calculateArithmeticPins(lower, g.LowerPrice.Sub(g.Spread), g.Spread, g.TickSize) + g.LowerPrice = lower g.addPins(newPins) return newPins } +func (g *Grid) TopPin() Pin { + return g.Pins[len(g.Pins)-1] +} + +func (g *Grid) BottomPin() Pin { + return g.Pins[0] +} + func (g *Grid) addPins(pins []Pin) { g.Pins = append(g.Pins, pins...) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 12992ea8a..df89c6b53 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -45,48 +45,50 @@ func TestGrid_HasPin(t *testing.T) { func TestGrid_ExtendUpperPrice(t *testing.T) { upper := number(500.0) lower := number(100.0) - size := number(100.0) - grid := NewGrid(lower, upper, size, number(2.0)) - + size := number(40.0) + grid := NewGrid(lower, upper, size, number(0.01)) originalSpread := grid.Spread + + assert.Equal(t, number(10.0), originalSpread) + assert.Len(t, grid.Pins, 40) // (1000-500) / 4 + newPins := grid.ExtendUpperPrice(number(1000.0)) assert.Equal(t, originalSpread, grid.Spread) - assert.Len(t, newPins, 125) // (1000-500) / 4 - assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) - if assert.Len(t, grid.Pins, 226) { - assert.Equal(t, fixedpoint.NewFromFloat(100.0), grid.Pins[0]) - assert.Equal(t, fixedpoint.NewFromFloat(1000.0), grid.Pins[225]) - } + assert.Len(t, newPins, 51) // (1000-500) / 4 } func TestGrid_ExtendLowerPrice(t *testing.T) { upper := fixedpoint.NewFromFloat(3000.0) lower := fixedpoint.NewFromFloat(2000.0) - size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size, number(2.0)) + size := fixedpoint.NewFromFloat(10.0) + grid := NewGrid(lower, upper, size, number(0.01)) - // spread = (3000 - 2000) / 100.0 - expectedSpread := fixedpoint.NewFromFloat(10.0) + assert.Equal(t, Pin(number(2000.0)), grid.BottomPin(), "bottom pin should be 1000.0") + assert.Equal(t, Pin(number(3000.0)), grid.TopPin(), "top pin should be 3000.0") + assert.Len(t, grid.Pins, 11) + + // spread = (3000 - 2000) / 10.0 + expectedSpread := fixedpoint.NewFromFloat(100.0) assert.Equal(t, expectedSpread, grid.Spread) originalSpread := grid.Spread newPins := grid.ExtendLowerPrice(fixedpoint.NewFromFloat(1000.0)) assert.Equal(t, originalSpread, grid.Spread) + t.Logf("newPins: %+v", newPins) + // 100 = (2000-1000) / 10 - if assert.Len(t, newPins, 100) { - assert.Equal(t, fixedpoint.NewFromFloat(2000.0)-expectedSpread, newPins[99]) + if assert.Len(t, newPins, 10) { + assert.Equal(t, Pin(number(1000.0)), newPins[0]) + assert.Equal(t, Pin(number(1900.0)), newPins[len(newPins)-1]) } assert.Equal(t, expectedSpread, grid.Spread) - if assert.Len(t, grid.Pins, 201) { - assert.Equal(t, fixedpoint.NewFromFloat(1000.0), grid.Pins[0]) - assert.Equal(t, fixedpoint.NewFromFloat(3000.0), grid.Pins[200]) - } - newPins2 := grid.ExtendLowerPrice( - fixedpoint.NewFromFloat(1000.0 - 1.0)) - assert.Len(t, newPins2, 0) // should have no new pin generated + if assert.Len(t, grid.Pins, 21) { + assert.Equal(t, Pin(number(1000.0)), grid.BottomPin(), "bottom pin should be 1000.0") + assert.Equal(t, Pin(number(3000.0)), grid.TopPin(), "top pin should be 3000.0") + } } func Test_calculateArithmeticPins(t *testing.T) { From 4ddbeff7e43f0c7edd1bf721681fa3068bce1675 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Nov 2022 18:11:04 +0800 Subject: [PATCH 10/59] grid2: fix Test_calculateArithmeticPins --- pkg/strategy/grid2/grid_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index df89c6b53..c04d76d59 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -104,6 +104,7 @@ func Test_calculateArithmeticPins(t *testing.T) { want []Pin }{ { + // (3000-1000)/30 = 66.6666666 name: "simple", args: args{ lower: number(1000.0), @@ -148,7 +149,8 @@ func Test_calculateArithmeticPins(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, calculateArithmeticPins(tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize), "calculateArithmeticPins(%v, %v, %v, %v)", tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize) + spread := tt.args.upper.Sub(tt.args.lower).Div(tt.args.size) + assert.Equalf(t, tt.want, calculateArithmeticPins(tt.args.lower, tt.args.upper, spread, tt.args.tickSize), "calculateArithmeticPins(%v, %v, %v, %v)", tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize) }) } } From f46fc7ee804be793b920436a0c0e346998a64b3a Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Nov 2022 20:09:07 +0800 Subject: [PATCH 11/59] grid2: fix tests --- pkg/strategy/grid2/grid_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index c04d76d59..8a5ad4d03 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -26,8 +26,8 @@ func TestNewGrid(t *testing.T) { assert.Equal(t, lower, grid.LowerPrice) assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) if assert.Len(t, grid.Pins, 101) { - assert.Equal(t, fixedpoint.NewFromFloat(100.0), grid.Pins[0]) - assert.Equal(t, fixedpoint.NewFromFloat(500.0), grid.Pins[100]) + assert.Equal(t, Pin(number(100.0)), grid.Pins[0]) + assert.Equal(t, Pin(number(500.0)), grid.Pins[100]) } } From 4fb2230e5dd5744830ab5ba786863d52de1cb8df Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Nov 2022 20:23:25 +0800 Subject: [PATCH 12/59] grid2: improve number func --- pkg/strategy/grid2/grid_test.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 8a5ad4d03..6568bdcc7 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -9,19 +9,25 @@ import ( ) func number(a interface{}) fixedpoint.Value { - if s, ok := a.(string); ok { - return fixedpoint.MustNewFromString(s) + switch v := a.(type) { + case string: + return fixedpoint.MustNewFromString(v) + case int: + return fixedpoint.NewFromInt(int64(v)) + case int64: + return fixedpoint.NewFromInt(int64(v)) + case float64: + return fixedpoint.NewFromFloat(v) } - f := a.(float64) - return fixedpoint.NewFromFloat(f) + return fixedpoint.Zero } func TestNewGrid(t *testing.T) { upper := fixedpoint.NewFromFloat(500.0) lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size, number(2.0)) + grid := NewGrid(lower, upper, size, number(0.01)) assert.Equal(t, upper, grid.UpperPrice) assert.Equal(t, lower, grid.LowerPrice) assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) @@ -35,7 +41,7 @@ func TestGrid_HasPin(t *testing.T) { upper := fixedpoint.NewFromFloat(500.0) lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) - grid := NewGrid(lower, upper, size, number(2)) + grid := NewGrid(lower, upper, size, number(0.01)) assert.True(t, grid.HasPin(Pin(number(100.0)))) assert.True(t, grid.HasPin(Pin(number(500.0)))) From 629cea0f44b45eb5590aa661a4e4f54c41e01ce4 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 8 Nov 2022 20:27:55 +0800 Subject: [PATCH 13/59] grid2: fix ExtendUpperPrice and its tests --- pkg/strategy/grid2/grid.go | 6 +++++- pkg/strategy/grid2/grid_test.go | 11 +++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index d075ed2be..35633f633 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -87,7 +87,11 @@ func (g *Grid) HasPin(pin Pin) (ok bool) { } func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { - newPins = calculateArithmeticPins(g.UpperPrice, upper, g.Spread, g.TickSize) + if upper.Compare(g.UpperPrice) <= 0 { + return nil + } + + newPins = calculateArithmeticPins(g.UpperPrice.Add(g.Spread), upper, g.Spread, g.TickSize) g.UpperPrice = upper g.addPins(newPins) return newPins diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 6568bdcc7..18e5c8095 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -51,16 +51,19 @@ func TestGrid_HasPin(t *testing.T) { func TestGrid_ExtendUpperPrice(t *testing.T) { upper := number(500.0) lower := number(100.0) - size := number(40.0) + size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) originalSpread := grid.Spread - assert.Equal(t, number(10.0), originalSpread) - assert.Len(t, grid.Pins, 40) // (1000-500) / 4 + t.Logf("pins: %+v", grid.Pins) + assert.Equal(t, number(100.0), originalSpread) + assert.Len(t, grid.Pins, 5) // (1000-500) / 4 newPins := grid.ExtendUpperPrice(number(1000.0)) + assert.Len(t, grid.Pins, 10) + assert.Len(t, newPins, 5) assert.Equal(t, originalSpread, grid.Spread) - assert.Len(t, newPins, 51) // (1000-500) / 4 + t.Logf("pins: %+v", grid.Pins) } func TestGrid_ExtendLowerPrice(t *testing.T) { From 84c3d386ca48a117bdac78837647a3369e63c119 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Nov 2022 16:20:40 +0800 Subject: [PATCH 14/59] grid2: implement find next higher/lower pin --- pkg/strategy/grid2/grid.go | 28 +++++++++++++++++++++++++ pkg/strategy/grid2/grid_test.go | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 35633f633..20aa541b2 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -86,6 +86,34 @@ func (g *Grid) HasPin(pin Pin) (ok bool) { return ok } +// NextHigherPin finds the next higher pin +func (g *Grid) NextHigherPin(price fixedpoint.Value) (Pin, bool) { + i := g.SearchPin(price) + if i < len(g.Pins) && fixedpoint.Value(g.Pins[i]).Compare(price) == 0 && i+1 < len(g.Pins) { + return g.Pins[i+1], true + } + + return Pin(fixedpoint.Zero), false +} + +// NextLowerPin finds the next lower pin +func (g *Grid) NextLowerPin(price fixedpoint.Value) (Pin, bool) { + i := g.SearchPin(price) + if i < len(g.Pins) && fixedpoint.Value(g.Pins[i]).Compare(price) == 0 && i-1 >= 0 { + return g.Pins[i-1], true + } + + return Pin(fixedpoint.Zero), false +} + +func (g *Grid) SearchPin(price fixedpoint.Value) int { + i := sort.Search(len(g.Pins), func(i int) bool { + a := fixedpoint.Value(g.Pins[i]) + return a.Compare(price) >= 0 + }) + return i +} + func (g *Grid) ExtendUpperPrice(upper fixedpoint.Value) (newPins []Pin) { if upper.Compare(g.UpperPrice) <= 0 { return nil diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 18e5c8095..742c2116f 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -100,6 +100,42 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { } } +func TestGrid_NextLowerPin(t *testing.T) { + upper := number(500.0) + lower := number(100.0) + size := number(4.0) + grid := NewGrid(lower, upper, size, number(0.01)) + t.Logf("pins: %+v", grid.Pins) + + next, ok := grid.NextLowerPin(number(200.0)) + assert.True(t, ok) + assert.Equal(t, Pin(number(100.0)), next) + + next, ok = grid.NextLowerPin(number(150.0)) + assert.False(t, ok) + assert.Equal(t, Pin(fixedpoint.Zero), next) +} + +func TestGrid_NextHigherPin(t *testing.T) { + upper := number(500.0) + lower := number(100.0) + size := number(4.0) + grid := NewGrid(lower, upper, size, number(0.01)) + t.Logf("pins: %+v", grid.Pins) + + next, ok := grid.NextHigherPin(number(100.0)) + assert.True(t, ok) + assert.Equal(t, Pin(number(200.0)), next) + + next, ok = grid.NextHigherPin(number(400.0)) + assert.True(t, ok) + assert.Equal(t, Pin(number(500.0)), next) + + next, ok = grid.NextHigherPin(number(500.0)) + assert.False(t, ok) + assert.Equal(t, Pin(fixedpoint.Zero), next) +} + func Test_calculateArithmeticPins(t *testing.T) { type args struct { lower fixedpoint.Value From 1fa51860028a0a35046cb7ef1c81d5d9dc8d7761 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Nov 2022 16:25:00 +0800 Subject: [PATCH 15/59] grid2: allocate grid object --- pkg/strategy/grid2/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 66613b53b..68d9eec3f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -45,6 +45,8 @@ type Strategy struct { LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"` + grid *Grid + bbgo.QuantityOrAmount ProfitStats *types.ProfitStats `persistence:"profit_stats"` @@ -123,6 +125,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position = types.NewPositionFromMarket(s.Market) } + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) From 32b6299b93c3fe0af9c35acf67fb6ba0335f17fc Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Nov 2022 16:26:04 +0800 Subject: [PATCH 16/59] grid2: pull out CalculatePins --- pkg/strategy/grid2/grid.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 20aa541b2..539c134dd 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -60,11 +60,15 @@ func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { Spread: spread, } - var pins = calculateArithmeticPins(lower, upper, spread, tickSize) - grid.addPins(pins) + grid.CalculatePins() return grid } +func (g *Grid) CalculatePins() { + var pins = calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) + g.addPins(pins) +} + func (g *Grid) Height() fixedpoint.Value { return g.UpperPrice.Sub(g.LowerPrice) } From a8cbe0e488f1381570beed62984348dab1942653 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Nov 2022 18:56:33 +0800 Subject: [PATCH 17/59] grid2: pull out calculate pins call --- pkg/strategy/grid2/grid.go | 1 - pkg/strategy/grid2/grid_test.go | 9 +++++++++ pkg/strategy/grid2/strategy.go | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 539c134dd..802e7f103 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -60,7 +60,6 @@ func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { Spread: spread, } - grid.CalculatePins() return grid } diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 742c2116f..82ab446a7 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -28,6 +28,8 @@ func TestNewGrid(t *testing.T) { lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() + assert.Equal(t, upper, grid.UpperPrice) assert.Equal(t, lower, grid.LowerPrice) assert.Equal(t, fixedpoint.NewFromFloat(4), grid.Spread) @@ -42,6 +44,7 @@ func TestGrid_HasPin(t *testing.T) { lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() assert.True(t, grid.HasPin(Pin(number(100.0)))) assert.True(t, grid.HasPin(Pin(number(500.0)))) @@ -53,6 +56,8 @@ func TestGrid_ExtendUpperPrice(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() + originalSpread := grid.Spread t.Logf("pins: %+v", grid.Pins) @@ -71,6 +76,7 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { lower := fixedpoint.NewFromFloat(2000.0) size := fixedpoint.NewFromFloat(10.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() assert.Equal(t, Pin(number(2000.0)), grid.BottomPin(), "bottom pin should be 1000.0") assert.Equal(t, Pin(number(3000.0)), grid.TopPin(), "top pin should be 3000.0") @@ -105,6 +111,8 @@ func TestGrid_NextLowerPin(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() + t.Logf("pins: %+v", grid.Pins) next, ok := grid.NextLowerPin(number(200.0)) @@ -121,6 +129,7 @@ func TestGrid_NextHigherPin(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) + grid.CalculatePins() t.Logf("pins: %+v", grid.Pins) next, ok := grid.NextHigherPin(number(100.0)) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 68d9eec3f..42ff8449b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -126,6 +126,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculatePins() s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) From a42c1799e2eab35cb3d7658faf4fdccf6ce2cc6b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 9 Nov 2022 19:43:14 +0800 Subject: [PATCH 18/59] grid2: define PinCalculator type --- pkg/strategy/grid2/grid.go | 22 +++++++++++++++++++--- pkg/strategy/grid2/grid_test.go | 12 ++++++------ pkg/strategy/grid2/strategy.go | 2 +- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 802e7f103..f61b1cb2e 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -7,6 +7,8 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) +type PinCalculator func() []Pin + type Grid struct { UpperPrice fixedpoint.Value `json:"upperPrice"` LowerPrice fixedpoint.Value `json:"lowerPrice"` @@ -24,6 +26,8 @@ type Grid struct { Pins []Pin `json:"pins"` pinsCache map[Pin]struct{} `json:"-"` + + calculator PinCalculator } type Pin fixedpoint.Value @@ -63,9 +67,21 @@ func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { return grid } -func (g *Grid) CalculatePins() { - var pins = calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) - g.addPins(pins) +func (g *Grid) CalculateGeometricPins() { + g.calculator = func() []Pin { + // return calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) + return nil + } + + g.addPins(g.calculator()) +} + +func (g *Grid) CalculateArithmeticPins() { + g.calculator = func() []Pin { + return calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) + } + + g.addPins(g.calculator()) } func (g *Grid) Height() fixedpoint.Value { diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index 82ab446a7..ccd7245cf 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -28,7 +28,7 @@ func TestNewGrid(t *testing.T) { lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() assert.Equal(t, upper, grid.UpperPrice) assert.Equal(t, lower, grid.LowerPrice) @@ -44,7 +44,7 @@ func TestGrid_HasPin(t *testing.T) { lower := fixedpoint.NewFromFloat(100.0) size := fixedpoint.NewFromFloat(100.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() assert.True(t, grid.HasPin(Pin(number(100.0)))) assert.True(t, grid.HasPin(Pin(number(500.0)))) @@ -56,7 +56,7 @@ func TestGrid_ExtendUpperPrice(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() originalSpread := grid.Spread @@ -76,7 +76,7 @@ func TestGrid_ExtendLowerPrice(t *testing.T) { lower := fixedpoint.NewFromFloat(2000.0) size := fixedpoint.NewFromFloat(10.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() assert.Equal(t, Pin(number(2000.0)), grid.BottomPin(), "bottom pin should be 1000.0") assert.Equal(t, Pin(number(3000.0)), grid.TopPin(), "top pin should be 3000.0") @@ -111,7 +111,7 @@ func TestGrid_NextLowerPin(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() t.Logf("pins: %+v", grid.Pins) @@ -129,7 +129,7 @@ func TestGrid_NextHigherPin(t *testing.T) { lower := number(100.0) size := number(4.0) grid := NewGrid(lower, upper, size, number(0.01)) - grid.CalculatePins() + grid.CalculateArithmeticPins() t.Logf("pins: %+v", grid.Pins) next, ok := grid.NextHigherPin(number(100.0)) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 42ff8449b..710607d8e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -126,7 +126,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) - s.grid.CalculatePins() + s.grid.CalculateArithmeticPins() s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) From 68b1fce634df55ed4670e5c25eaff56f3cf021a1 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 10 Nov 2022 20:58:46 +0800 Subject: [PATCH 19/59] grid2: get the last trade price and apply generalOrderExecutor --- pkg/strategy/grid2/grid.go | 1 + pkg/strategy/grid2/strategy.go | 106 ++++++++++++++++++++++++++------- 2 files changed, 85 insertions(+), 22 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index f61b1cb2e..9121a7ea9 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -69,6 +69,7 @@ func NewGrid(lower, upper, size, tickSize fixedpoint.Value) *Grid { func (g *Grid) CalculateGeometricPins() { g.calculator = func() []Pin { + // TODO: implement geometric calculator // return calculateArithmeticPins(g.LowerPrice, g.UpperPrice, g.Spread, g.TickSize) return nil } diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 710607d8e..98672c29c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -28,6 +28,8 @@ func init() { } type Strategy struct { + Environment *bbgo.Environment + // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc // This field will be injected automatically since we defined the Symbol field. types.Market `json:"-" yaml:"-"` @@ -60,6 +62,8 @@ type Strategy struct { tradeCollector *bbgo.TradeCollector + orderExecutor *bbgo.GeneralOrderExecutor + // groupID is the group ID used for the strategy instance for canceling orders groupID uint32 } @@ -125,30 +129,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position = types.NewPositionFromMarket(s.Market) } + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.Bind() + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) s.grid.CalculateArithmeticPins() - s.orderStore = bbgo.NewOrderStore(s.Symbol) - s.orderStore.BindStream(session.UserDataStream) - - // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. - s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - s.activeOrders.OnFilled(s.handleOrderFilled) - s.activeOrders.BindStream(session.UserDataStream) - - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) - - s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { - bbgo.Notify(trade) - s.ProfitStats.AddTrade(trade) - }) - - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { - bbgo.Notify(position) - }) - - s.tradeCollector.BindStream(session.UserDataStream) - bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() @@ -162,8 +153,79 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) session.UserDataStream.OnStart(func() { - + if err := s.setupGridOrders(ctx, session); err != nil { + log.WithError(err).Errorf("failed to setup grid orders") + } }) return nil } + +func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { + lastPrice, err := s.getLastTradePrice(ctx, session) + if err != nil { + return errors.Wrap(err, "failed to get the last trade price") + } + + // shift 1 grid because we will start from the buy order + // if the buy order is filled, then we will submit another sell order at the higher grid. + for i := len(s.grid.Pins) - 2; i >= 0; i++ { + pin := s.grid.Pins[i] + price := fixedpoint.Value(pin) + + if price.Compare(lastPrice) >= 0 { + if s.QuantityOrAmount.Quantity.Sign() > 0 { + quantity := s.QuantityOrAmount.Quantity + + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", + }) + + if err2 != nil { + return err2 + } + + _ = createdOrders + + } else if s.QuantityOrAmount.Amount.Sign() > 0 { + + } + } + } + + return nil +} + +func (s *Strategy) getLastTradePrice(ctx context.Context, session *bbgo.ExchangeSession) (fixedpoint.Value, error) { + if bbgo.IsBackTesting { + price, ok := session.LastPrice(s.Symbol) + if !ok { + return fixedpoint.Zero, fmt.Errorf("last price of %s not found", s.Symbol) + } + + return price, nil + } + + tickers, err := session.Exchange.QueryTickers(ctx, s.Symbol) + if err != nil { + return fixedpoint.Zero, err + } + + if ticker, ok := tickers[s.Symbol]; ok { + if !ticker.Last.IsZero() { + return ticker.Last, nil + } + + // fallback to buy price + return ticker.Buy, nil + } + + return fixedpoint.Zero, fmt.Errorf("%s ticker price not found", s.Symbol) +} From 2c373959a8abf166e59ff3e193f1223766b71e34 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 10 Nov 2022 23:34:55 +0800 Subject: [PATCH 20/59] grid2: add investment check --- pkg/strategy/grid2/strategy.go | 110 +++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 26 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 98672c29c..803737a21 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -32,25 +32,33 @@ type Strategy struct { // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc // This field will be injected automatically since we defined the Symbol field. - types.Market `json:"-" yaml:"-"` + types.Market `json:"-"` // These fields will be filled from the config file (it translates YAML to JSON) - Symbol string `json:"symbol" yaml:"symbol"` + Symbol string `json:"symbol"` // ProfitSpread is the fixed profit spread you want to submit the sell order - ProfitSpread fixedpoint.Value `json:"profitSpread" yaml:"profitSpread"` + ProfitSpread fixedpoint.Value `json:"profitSpread"` // GridNum is the grid number, how many orders you want to post on the orderbook. - GridNum int64 `json:"gridNumber" yaml:"gridNumber"` + GridNum int64 `json:"gridNumber"` - UpperPrice fixedpoint.Value `json:"upperPrice" yaml:"upperPrice"` + UpperPrice fixedpoint.Value `json:"upperPrice"` - LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"` + LowerPrice fixedpoint.Value `json:"lowerPrice"` + + // QuantityOrAmount embeds the Quantity field and the Amount field + // If you set up the Quantity field or the Amount field, you don't need to set the QuoteInvestment and BaseInvestment + bbgo.QuantityOrAmount + + // If Quantity and Amount is not set, we can use the quote investment to calculate our quantity. + QuoteInvestment fixedpoint.Value `json:"quoteInvestment"` + + // BaseInvestment is the total base quantity you want to place as the sell order. + BaseInvestment fixedpoint.Value `json:"baseInvestment"` grid *Grid - bbgo.QuantityOrAmount - ProfitStats *types.ProfitStats `persistence:"profit_stats"` Position *types.Position `persistence:"position"` @@ -169,33 +177,83 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. + quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() + + // check if base and quote are enough + baseBalance, ok := session.Account.Balance(s.Market.BaseCurrency) + if !ok { + return fmt.Errorf("base %s balance not found", s.Market.BaseCurrency) + } + + quoteBalance, ok := session.Account.Balance(s.Market.QuoteCurrency) + if !ok { + return fmt.Errorf("quote %s balance not found", s.Market.QuoteCurrency) + } + + totalBase := baseBalance.Available + totalQuote := quoteBalance.Available + + if quantityOrAmountIsSet { + requiredBase := fixedpoint.Zero + requiredQuote := fixedpoint.Zero + for i := len(s.grid.Pins) - 1; i >= 0; i++ { + pin := s.grid.Pins[i] + price := fixedpoint.Value(pin) + + q := s.QuantityOrAmount.CalculateQuantity(price) + if price.Compare(lastPrice) >= 0 { + // sell + requiredBase = requiredBase.Add(q) + } else { + requiredQuote = requiredQuote.Add(q) + } + } + + if requiredBase.Compare(totalBase) < 0 && requiredQuote.Compare(totalQuote) < 0 { + return fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enought", + totalBase.Float64(), s.Market.BaseCurrency, + totalQuote.Float64(), s.Market.QuoteCurrency) + } + + if requiredBase.Compare(totalBase) < 0 { + // see if we can convert some quotes to base + } + } + for i := len(s.grid.Pins) - 2; i >= 0; i++ { pin := s.grid.Pins[i] price := fixedpoint.Value(pin) if price.Compare(lastPrice) >= 0 { - if s.QuantityOrAmount.Quantity.Sign() > 0 { - quantity := s.QuantityOrAmount.Quantity + // check sell order + if quantityOrAmountIsSet { + if s.QuantityOrAmount.Quantity.Sign() > 0 { + quantity := s.QuantityOrAmount.Quantity - createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Quantity: quantity, - Price: price, - Market: s.Market, - TimeInForce: types.TimeInForceGTC, - Tag: "grid", - }) + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", + }) + + if err2 != nil { + return err2 + } + + _ = createdOrders + + } else if s.QuantityOrAmount.Amount.Sign() > 0 { - if err2 != nil { - return err2 } + } else if s.BaseInvestment.Sign() > 0 { - _ = createdOrders - - } else if s.QuantityOrAmount.Amount.Sign() > 0 { - + } else { + // error: either quantity, amount, baseInvestment is not set. } } } From 4c8db08ccca4732fd5533e8421df6027952ca2b2 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 11 Nov 2022 02:24:52 +0800 Subject: [PATCH 21/59] grid2: fix require quote and require base calculation --- pkg/strategy/grid2/strategy.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 803737a21..c14326b6f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -200,12 +200,32 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe pin := s.grid.Pins[i] price := fixedpoint.Value(pin) - q := s.QuantityOrAmount.CalculateQuantity(price) if price.Compare(lastPrice) >= 0 { - // sell - requiredBase = requiredBase.Add(q) + // sell orders + if requiredBase.Compare(totalBase) < 0 { + if q := s.QuantityOrAmount.Quantity; !q.IsZero() { + requiredBase = requiredBase.Add(s.QuantityOrAmount.Quantity) + } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { + qq := s.QuantityOrAmount.CalculateQuantity(price) + requiredBase = requiredBase.Add(qq) + } + } else if i > 0 { + // convert buy quote to requiredQuote + nextLowerPin := s.grid.Pins[i-1] + nextLowerPrice := fixedpoint.Value(nextLowerPin) + if q := s.QuantityOrAmount.Quantity; !q.IsZero() { + requiredQuote = requiredQuote.Add(q.Mul(nextLowerPrice)) + } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { + requiredQuote = requiredQuote.Add(amount) + } + } } else { - requiredQuote = requiredQuote.Add(q) + // buy orders + if q := s.QuantityOrAmount.Quantity; !q.IsZero() { + requiredQuote = requiredQuote.Add(q.Mul(price)) + } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { + requiredQuote = requiredQuote.Add(amount) + } } } From 7fec736e7a5ad5c2002b6584b7a6708d8df8e029 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 14 Nov 2022 16:28:07 +0800 Subject: [PATCH 22/59] grid2: add GridProfitStats --- pkg/strategy/grid2/strategy.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c14326b6f..ca01b0148 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -27,6 +27,13 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +type GridProfitStats struct { + TotalProfit fixedpoint.Value `json:"totalProfit"` + FloatProfit fixedpoint.Value `json:"floatProfit"` + GridProfit fixedpoint.Value `json:"gridProfit"` + ArbitrageCount int `json:"arbitrageCount"` +} + type Strategy struct { Environment *bbgo.Environment @@ -169,16 +176,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } +func (s *Strategy) calculateRequiredInvestment(baseInvestment, quoteInvestment, totalBaseBalance, totalQuoteBalance fixedpoint.Value) { + +} + func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { return errors.Wrap(err, "failed to get the last trade price") } - // shift 1 grid because we will start from the buy order - // if the buy order is filled, then we will submit another sell order at the higher grid. - quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() - // check if base and quote are enough baseBalance, ok := session.Account.Balance(s.Market.BaseCurrency) if !ok { @@ -192,7 +199,11 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe totalBase := baseBalance.Available totalQuote := quoteBalance.Available + s.calculateRequiredInvestment(s.BaseInvestment, s.QuoteInvestment, totalBase, totalQuote) + // shift 1 grid because we will start from the buy order + // if the buy order is filled, then we will submit another sell order at the higher grid. + quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() if quantityOrAmountIsSet { requiredBase := fixedpoint.Zero requiredQuote := fixedpoint.Zero @@ -201,7 +212,8 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe price := fixedpoint.Value(pin) if price.Compare(lastPrice) >= 0 { - // sell orders + // for orders that sell + // if we still have the base balance if requiredBase.Compare(totalBase) < 0 { if q := s.QuantityOrAmount.Quantity; !q.IsZero() { requiredBase = requiredBase.Add(s.QuantityOrAmount.Quantity) @@ -220,7 +232,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } } else { - // buy orders + // for orders that buy if q := s.QuantityOrAmount.Quantity; !q.IsZero() { requiredQuote = requiredQuote.Add(q.Mul(price)) } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { From fa692d835f46dce4da412d75b6f1773164687b86 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 14 Nov 2022 16:28:42 +0800 Subject: [PATCH 23/59] grid2: add totalFee field and volume field --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index ca01b0148..c237bc4c0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -32,6 +32,8 @@ type GridProfitStats struct { FloatProfit fixedpoint.Value `json:"floatProfit"` GridProfit fixedpoint.Value `json:"gridProfit"` ArbitrageCount int `json:"arbitrageCount"` + TotalFee fixedpoint.Value `json:"totalFee"` + Volume fixedpoint.Value `json:"volume"` } type Strategy struct { From cde463e294254da751b7bf4d92c5100e996d3fa5 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 14 Nov 2022 16:29:25 +0800 Subject: [PATCH 24/59] grid2: remove notionalModifier --- pkg/strategy/grid2/strategy.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index c237bc4c0..2faaaa9a1 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -18,8 +18,6 @@ const ID = "grid2" var log = logrus.WithField("strategy", ID) -var notionalModifier = fixedpoint.NewFromFloat(1.0001) - func init() { // Register the pointer of the strategy struct, // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) From 3da86ab2e1d7b15b61c5a4fc32fe260b15a4da10 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 14 Nov 2022 17:37:32 +0800 Subject: [PATCH 25/59] grid2: pull out check code to checkRequiredInvestmentByQuantity --- pkg/strategy/grid2/strategy.go | 86 ++++++++++++++++------------------ 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2faaaa9a1..1f91fc8b7 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -176,8 +176,42 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -func (s *Strategy) calculateRequiredInvestment(baseInvestment, quoteInvestment, totalBaseBalance, totalQuoteBalance fixedpoint.Value) { +func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, totalBaseBalance, totalQuoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) error { + requiredBase := fixedpoint.Zero + requiredQuote := fixedpoint.Zero + for i := len(s.grid.Pins) - 1; i >= 0; i++ { + pin := s.grid.Pins[i] + price := fixedpoint.Value(pin) + // TODO: add fee if we don't have the platform token. BNB, OKEX or MAX... + if price.Compare(lastPrice) >= 0 { + // for orders that sell + // if we still have the base balance + if requiredBase.Compare(totalBaseBalance) < 0 { + requiredBase = requiredBase.Add(quantity) + } else if i > 0 { + // convert buy quote to requiredQuote + nextLowerPin := s.grid.Pins[i-1] + nextLowerPrice := fixedpoint.Value(nextLowerPin) + requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) + + } + } else { + requiredQuote = requiredQuote.Add(quantity.Mul(price)) + } + } + + if requiredBase.Compare(totalBaseBalance) < 0 && requiredQuote.Compare(totalQuoteBalance) < 0 { + return fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enought", + totalBaseBalance.Float64(), s.Market.BaseCurrency, + totalQuoteBalance.Float64(), s.Market.QuoteCurrency) + } + + if requiredBase.Compare(totalBaseBalance) < 0 { + // see if we can convert some quotes to base + } + + return nil } func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { @@ -199,56 +233,16 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe totalBase := baseBalance.Available totalQuote := quoteBalance.Available - s.calculateRequiredInvestment(s.BaseInvestment, s.QuoteInvestment, totalBase, totalQuote) // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() if quantityOrAmountIsSet { - requiredBase := fixedpoint.Zero - requiredQuote := fixedpoint.Zero - for i := len(s.grid.Pins) - 1; i >= 0; i++ { - pin := s.grid.Pins[i] - price := fixedpoint.Value(pin) - - if price.Compare(lastPrice) >= 0 { - // for orders that sell - // if we still have the base balance - if requiredBase.Compare(totalBase) < 0 { - if q := s.QuantityOrAmount.Quantity; !q.IsZero() { - requiredBase = requiredBase.Add(s.QuantityOrAmount.Quantity) - } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { - qq := s.QuantityOrAmount.CalculateQuantity(price) - requiredBase = requiredBase.Add(qq) - } - } else if i > 0 { - // convert buy quote to requiredQuote - nextLowerPin := s.grid.Pins[i-1] - nextLowerPrice := fixedpoint.Value(nextLowerPin) - if q := s.QuantityOrAmount.Quantity; !q.IsZero() { - requiredQuote = requiredQuote.Add(q.Mul(nextLowerPrice)) - } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { - requiredQuote = requiredQuote.Add(amount) - } - } - } else { - // for orders that buy - if q := s.QuantityOrAmount.Quantity; !q.IsZero() { - requiredQuote = requiredQuote.Add(q.Mul(price)) - } else if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { - requiredQuote = requiredQuote.Add(amount) - } - } - } - - if requiredBase.Compare(totalBase) < 0 && requiredQuote.Compare(totalQuote) < 0 { - return fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enought", - totalBase.Float64(), s.Market.BaseCurrency, - totalQuote.Float64(), s.Market.QuoteCurrency) - } - - if requiredBase.Compare(totalBase) < 0 { - // see if we can convert some quotes to base + if err2 := s.checkRequiredInvestmentByQuantity( + s.BaseInvestment, s.QuoteInvestment, + totalBase, totalQuote, + lastPrice, s.QuantityOrAmount.Quantity, s.grid.Pins); err != nil { + return err2 } } From d0bdc859fbbc1f8832731a4d34632c1587fe161c Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 15 Nov 2022 15:29:30 +0800 Subject: [PATCH 26/59] grid2: add basic investment check test checkRequiredInvestmentByQuantity --- pkg/strategy/grid2/strategy.go | 40 ++++++++++++++++++++--------- pkg/strategy/grid2/strategy_test.go | 27 +++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 pkg/strategy/grid2/strategy_test.go diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 1f91fc8b7..eb33e430f 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -176,38 +176,54 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, totalBaseBalance, totalQuoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) error { +type InvestmentBudget struct { + baseInvestment fixedpoint.Value + quoteInvestment fixedpoint.Value + baseBalance fixedpoint.Value + quoteBalance fixedpoint.Value +} + +func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) error { + if baseInvestment.Compare(baseBalance) > 0 { + return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) + } + + if quoteInvestment.Compare(quoteBalance) > 0 { + return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) + } + + // check more investment budget details requiredBase := fixedpoint.Zero requiredQuote := fixedpoint.Zero - for i := len(s.grid.Pins) - 1; i >= 0; i++ { - pin := s.grid.Pins[i] + for i := len(pins) - 1; i >= 0; i++ { + pin := pins[i] price := fixedpoint.Value(pin) - // TODO: add fee if we don't have the platform token. BNB, OKEX or MAX... + // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... if price.Compare(lastPrice) >= 0 { // for orders that sell // if we still have the base balance - if requiredBase.Compare(totalBaseBalance) < 0 { + if requiredBase.Compare(baseBalance) < 0 { requiredBase = requiredBase.Add(quantity) - } else if i > 0 { - // convert buy quote to requiredQuote + } else if i > 0 { // we do not want to sell at i == 0 + // convert sell to buy quote and add to requiredQuote nextLowerPin := s.grid.Pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) - } } else { + // for orders that buy requiredQuote = requiredQuote.Add(quantity.Mul(price)) } } - if requiredBase.Compare(totalBaseBalance) < 0 && requiredQuote.Compare(totalQuoteBalance) < 0 { + if requiredBase.Compare(baseBalance) > 0 && requiredQuote.Compare(quoteBalance) > 0 { return fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enought", - totalBaseBalance.Float64(), s.Market.BaseCurrency, - totalQuoteBalance.Float64(), s.Market.QuoteCurrency) + baseBalance.Float64(), s.Market.BaseCurrency, + quoteBalance.Float64(), s.Market.QuoteCurrency) } - if requiredBase.Compare(totalBaseBalance) < 0 { + if requiredBase.Compare(baseBalance) < 0 { // see if we can convert some quotes to base } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go new file mode 100644 index 000000000..a1cd344bc --- /dev/null +++ b/pkg/strategy/grid2/strategy_test.go @@ -0,0 +1,27 @@ +package grid2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { + s := &Strategy{} + + t.Run("basic base balance check", func(t *testing.T) { + err := s.checkRequiredInvestmentByQuantity(number(2.0), number(10_000.0), + number(1.0), number(10_000.0), + number(0.1), number(19000.0), []Pin{}) + assert.Error(t, err) + assert.EqualError(t, err, "baseInvestment setup 2.000000 is greater than the total base balance 1.000000") + }) + + t.Run("basic quote balance check", func(t *testing.T) { + err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), + number(1.0), number(100.0), + number(0.1), number(19_000.0), []Pin{}) + assert.Error(t, err) + assert.EqualError(t, err, "quoteInvestment setup 10000.000000 is greater than the total quote balance 100.000000") + }) +} From dcbce8aa5c66d6bb4a80a74b3b2cf67000843ec5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 12:09:55 +0800 Subject: [PATCH 27/59] grid2: fix TestStrategy_checkRequiredInvestmentByQuantity --- pkg/strategy/grid2/strategy.go | 33 ++++++++++++++++------------- pkg/strategy/grid2/strategy_test.go | 19 +++++++++++++++-- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index eb33e430f..b2f924076 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -183,19 +183,22 @@ type InvestmentBudget struct { quoteBalance fixedpoint.Value } -func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) error { +func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { if baseInvestment.Compare(baseBalance) > 0 { - return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) + return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) } if quoteInvestment.Compare(quoteBalance) > 0 { - return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) + return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) } // check more investment budget details - requiredBase := fixedpoint.Zero - requiredQuote := fixedpoint.Zero - for i := len(pins) - 1; i >= 0; i++ { + requiredBase = fixedpoint.Zero + requiredQuote = fixedpoint.Zero + + // when we need to place a buy-to-sell conversion order, we need to mark the price + buyPlacedPrice := fixedpoint.Zero + for i := len(pins) - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) @@ -203,31 +206,31 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvest if price.Compare(lastPrice) >= 0 { // for orders that sell // if we still have the base balance - if requiredBase.Compare(baseBalance) < 0 { + if requiredBase.Add(quantity).Compare(baseBalance) <= 0 { requiredBase = requiredBase.Add(quantity) } else if i > 0 { // we do not want to sell at i == 0 // convert sell to buy quote and add to requiredQuote - nextLowerPin := s.grid.Pins[i-1] + nextLowerPin := pins[i-1] nextLowerPrice := fixedpoint.Value(nextLowerPin) requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) + buyPlacedPrice = nextLowerPrice } } else { // for orders that buy + if price.Compare(buyPlacedPrice) == 0 { + continue + } requiredQuote = requiredQuote.Add(quantity.Mul(price)) } } if requiredBase.Compare(baseBalance) > 0 && requiredQuote.Compare(quoteBalance) > 0 { - return fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enought", + return requiredBase, requiredQuote, fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enough", baseBalance.Float64(), s.Market.BaseCurrency, quoteBalance.Float64(), s.Market.QuoteCurrency) } - if requiredBase.Compare(baseBalance) < 0 { - // see if we can convert some quotes to base - } - - return nil + return requiredBase, requiredQuote, nil } func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { @@ -254,7 +257,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe // if the buy order is filled, then we will submit another sell order at the higher grid. quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() if quantityOrAmountIsSet { - if err2 := s.checkRequiredInvestmentByQuantity( + if _, _, err2 := s.checkRequiredInvestmentByQuantity( s.BaseInvestment, s.QuoteInvestment, totalBase, totalQuote, lastPrice, s.QuantityOrAmount.Quantity, s.grid.Pins); err != nil { diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index a1cd344bc..f45558a52 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -10,7 +10,7 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { s := &Strategy{} t.Run("basic base balance check", func(t *testing.T) { - err := s.checkRequiredInvestmentByQuantity(number(2.0), number(10_000.0), + _, _, err := s.checkRequiredInvestmentByQuantity(number(2.0), number(10_000.0), number(1.0), number(10_000.0), number(0.1), number(19000.0), []Pin{}) assert.Error(t, err) @@ -18,10 +18,25 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { }) t.Run("basic quote balance check", func(t *testing.T) { - err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), + _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), number(1.0), number(100.0), number(0.1), number(19_000.0), []Pin{}) assert.Error(t, err) assert.EqualError(t, err, "quoteInvestment setup 10000.000000 is greater than the total quote balance 100.000000") }) + + t.Run("quote to base balance conversion check", func(t *testing.T) { + _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(10_000.0), + number(0.0), number(10_000.0), + number(0.1), number(13_500.0), []Pin{ + Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) + Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) + Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) + Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) + Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) + Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD + }) + assert.NoError(t, err) + assert.Equal(t, number(6000.0), requiredQuote) + }) } From f5219ae56ba3b45154c00d6899074d445688f070 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 13:29:55 +0800 Subject: [PATCH 28/59] grid2: fix error checking and add more tests --- pkg/strategy/grid2/strategy.go | 20 ++++++++++++++++++-- pkg/strategy/grid2/strategy_test.go | 25 ++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index b2f924076..6bb867e82 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -225,9 +225,25 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvest } if requiredBase.Compare(baseBalance) > 0 && requiredQuote.Compare(quoteBalance) > 0 { - return requiredBase, requiredQuote, fmt.Errorf("both base balance (%f %s) and quote balance (%f %s) are not enough", + return requiredBase, requiredQuote, fmt.Errorf("both base balance (%f %s) or quote balance (%f %s) is not enough, required = base %f + quote %f", baseBalance.Float64(), s.Market.BaseCurrency, - quoteBalance.Float64(), s.Market.QuoteCurrency) + quoteBalance.Float64(), s.Market.QuoteCurrency, + requiredBase.Float64(), + requiredQuote.Float64()) + } + + if requiredBase.Compare(baseBalance) > 0 { + return requiredBase, requiredQuote, fmt.Errorf("base balance (%f %s), required = base %f", + baseBalance.Float64(), s.Market.BaseCurrency, + requiredBase.Float64(), + ) + } + + if requiredQuote.Compare(quoteBalance) > 0 { + return requiredBase, requiredQuote, fmt.Errorf("quote balance (%f %s) is not enough, required = quote %f", + quoteBalance.Float64(), s.Market.QuoteCurrency, + requiredQuote.Float64(), + ) } return requiredBase, requiredQuote, nil diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index f45558a52..4c3ca3184 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -4,10 +4,17 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" ) func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { - s := &Strategy{} + s := &Strategy{ + Market: types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + }, + } t.Run("basic base balance check", func(t *testing.T) { _, _, err := s.checkRequiredInvestmentByQuantity(number(2.0), number(10_000.0), @@ -39,4 +46,20 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { assert.NoError(t, err) assert.Equal(t, number(6000.0), requiredQuote) }) + + t.Run("quote to base balance conversion not enough", func(t *testing.T) { + _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(5_000.0), + number(0.0), number(5_000.0), + number(0.1), number(13_500.0), []Pin{ + Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) + Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) + Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) + Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) + Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) + Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD + }) + assert.EqualError(t, err, "quote balance (5000.000000 USDT) is not enough, required = quote 6000.000000") + assert.Equal(t, number(6000.0), requiredQuote) + }) + } From 2aaa2e77750e1453c58306d274c12021f6b2729f Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 15:11:42 +0800 Subject: [PATCH 29/59] grid2: add checkRequiredInvestmentByAmount test --- pkg/strategy/grid2/strategy.go | 67 +++++++++++++++++++++++++++++ pkg/strategy/grid2/strategy_test.go | 26 ++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6bb867e82..7d36d00d1 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -249,6 +249,73 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvest return requiredBase, requiredQuote, nil } +func (s *Strategy) checkRequiredInvestmentByAmount(baseInvestment, quoteInvestment, baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { + if baseInvestment.Compare(baseBalance) > 0 { + return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) + } + + if quoteInvestment.Compare(quoteBalance) > 0 { + return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) + } + + // check more investment budget details + requiredBase = fixedpoint.Zero + requiredQuote = fixedpoint.Zero + + // when we need to place a buy-to-sell conversion order, we need to mark the price + buyPlacedPrice := fixedpoint.Zero + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + + // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... + if price.Compare(lastPrice) >= 0 { + // for orders that sell + // if we still have the base balance + quantity := amount.Div(lastPrice) + if requiredBase.Add(quantity).Compare(baseBalance) <= 0 { + requiredBase = requiredBase.Add(quantity) + } else if i > 0 { // we do not want to sell at i == 0 + // convert sell to buy quote and add to requiredQuote + nextLowerPin := pins[i-1] + nextLowerPrice := fixedpoint.Value(nextLowerPin) + requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) + buyPlacedPrice = nextLowerPrice + } + } else { + // for orders that buy + if price.Compare(buyPlacedPrice) == 0 { + continue + } + requiredQuote = requiredQuote.Add(amount) + } + } + + if requiredBase.Compare(baseBalance) > 0 && requiredQuote.Compare(quoteBalance) > 0 { + return requiredBase, requiredQuote, fmt.Errorf("both base balance (%f %s) or quote balance (%f %s) is not enough, required = base %f + quote %f", + baseBalance.Float64(), s.Market.BaseCurrency, + quoteBalance.Float64(), s.Market.QuoteCurrency, + requiredBase.Float64(), + requiredQuote.Float64()) + } + + if requiredBase.Compare(baseBalance) > 0 { + return requiredBase, requiredQuote, fmt.Errorf("base balance (%f %s), required = base %f", + baseBalance.Float64(), s.Market.BaseCurrency, + requiredBase.Float64(), + ) + } + + if requiredQuote.Compare(quoteBalance) > 0 { + return requiredBase, requiredQuote, fmt.Errorf("quote balance (%f %s) is not enough, required = quote %f", + quoteBalance.Float64(), s.Market.QuoteCurrency, + requiredQuote.Float64(), + ) + } + + return requiredBase, requiredQuote, nil +} + func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 4c3ca3184..ff3163497 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -61,5 +61,29 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { assert.EqualError(t, err, "quote balance (5000.000000 USDT) is not enough, required = quote 6000.000000") assert.Equal(t, number(6000.0), requiredQuote) }) - +} + +func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { + s := &Strategy{ + Market: types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + }, + } + + t.Run("quote to base balance conversion", func(t *testing.T) { + _, requiredQuote, err := s.checkRequiredInvestmentByAmount(number(0.0), number(3_000.0), + number(0.0), number(3_000.0), + number(1000.0), + number(13_500.0), []Pin{ + Pin(number(10_000.0)), + Pin(number(11_000.0)), + Pin(number(12_000.0)), + Pin(number(13_000.0)), + Pin(number(14_000.0)), + Pin(number(15_000.0)), + }) + assert.EqualError(t, err, "quote balance (3000.000000 USDT) is not enough, required = quote 4999.999890") + assert.Equal(t, number(4999.99989), requiredQuote) + }) } From 991dc4121c772cb2769fb75811ae07e8ffc987cb Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 16:42:14 +0800 Subject: [PATCH 30/59] fixedpoint: add Floor() method on dnum --- pkg/fixedpoint/dec.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/fixedpoint/dec.go b/pkg/fixedpoint/dec.go index 2b5f3743b..43c52d499 100644 --- a/pkg/fixedpoint/dec.go +++ b/pkg/fixedpoint/dec.go @@ -131,7 +131,7 @@ func NewFromInt(n int64) Value { if n == 0 { return Zero } - //n0 := n + // n0 := n sign := int8(signPos) if n < 0 { n = -n @@ -527,7 +527,7 @@ func NewFromString(s string) (Value, error) { coef *= pow10[p] exp -= p } - //check(coefMin <= coef && coef <= coefMax) + // check(coefMin <= coef && coef <= coefMax) return Value{coef, sign, exp}, nil } @@ -577,7 +577,7 @@ func NewFromBytes(s []byte) (Value, error) { coef *= pow10[p] exp -= p } - //check(coefMin <= coef && coef <= coefMax) + // check(coefMin <= coef && coef <= coefMax) return Value{coef, sign, exp}, nil } @@ -958,6 +958,10 @@ func (dn Value) integer(mode RoundingMode) Value { return Value{i, dn.sign, dn.exp} } +func (dn Value) Floor() Value { + return dn.Round(0, Down) +} + func (dn Value) Round(r int, mode RoundingMode) Value { if dn.sign == 0 || dn.sign == signNegInf || dn.sign == signPosInf || r >= digitsMax { @@ -1171,9 +1175,9 @@ func align(x, y *Value) bool { return false } yshift = e - //check(0 <= yshift && yshift <= 20) + // check(0 <= yshift && yshift <= 20) y.coef = (y.coef + halfpow10[yshift]) / pow10[yshift] - //check(int(y.exp)+yshift == int(x.exp)) + // check(int(y.exp)+yshift == int(x.exp)) return true } @@ -1278,7 +1282,7 @@ func (dn Value) Format(mask string) string { nd := len(digits) di := e - before - //check(di <= 0) + // check(di <= 0) var buf strings.Builder sign := n.Sign() signok := (sign >= 0) From 051755ec54d1de4bebb54ba9f77a9c87d7bd420c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 17:08:40 +0800 Subject: [PATCH 31/59] fixedpoint: add Floor test --- pkg/fixedpoint/dec_dnum_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/fixedpoint/dec_dnum_test.go b/pkg/fixedpoint/dec_dnum_test.go index d92d49639..051a8f0e6 100644 --- a/pkg/fixedpoint/dec_dnum_test.go +++ b/pkg/fixedpoint/dec_dnum_test.go @@ -3,8 +3,9 @@ package fixedpoint import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestDelta(t *testing.T) { @@ -13,6 +14,12 @@ func TestDelta(t *testing.T) { assert.InDelta(t, f1.Mul(f2).Float64(), 41.3, 1e-14) } +func TestFloor(t *testing.T) { + f1 := MustNewFromString("10.333333") + f2 := f1.Floor() + assert.Equal(t, "10", f2.String()) +} + func TestInternal(t *testing.T) { r := &reader{"1.1e-15", 0} c, e := r.getCoef() From 4eb21d5209e7a92a56af0bf8e59341ac743657e5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 16 Nov 2022 17:08:57 +0800 Subject: [PATCH 32/59] grid2: move out baseInvestment, quoteInvestment check --- pkg/strategy/grid2/strategy.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7d36d00d1..d4b981f79 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -250,13 +250,6 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvest } func (s *Strategy) checkRequiredInvestmentByAmount(baseInvestment, quoteInvestment, baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { - if baseInvestment.Compare(baseBalance) > 0 { - return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) - } - - if quoteInvestment.Compare(quoteBalance) > 0 { - return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) - } // check more investment budget details requiredBase = fixedpoint.Zero @@ -336,6 +329,15 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe totalBase := baseBalance.Available totalQuote := quoteBalance.Available + if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { + if s.BaseInvestment.Compare(totalBase) > 0 { + return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", s.BaseInvestment.Float64(), totalBase.Float64()) + } + if s.QuoteInvestment.Compare(totalQuote) > 0 { + return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", s.QuoteInvestment.Float64(), totalQuote.Float64()) + } + } + // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() From 4407aa7f9788875950d0ed0178ddac2d2ea6102d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 17 Nov 2022 17:40:59 +0800 Subject: [PATCH 33/59] grid2: refactor checkRequiredInvestmentByAmount and checkRequiredInvestmentByQuantity --- pkg/strategy/grid2/strategy.go | 29 ++++++++----------- pkg/strategy/grid2/strategy_test.go | 44 ++++++++++++----------------- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index d4b981f79..4b2d69765 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -183,15 +183,7 @@ type InvestmentBudget struct { quoteBalance fixedpoint.Value } -func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvestment, baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { - if baseInvestment.Compare(baseBalance) > 0 { - return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", baseInvestment.Float64(), baseBalance.Float64()) - } - - if quoteInvestment.Compare(quoteBalance) > 0 { - return fixedpoint.Zero, fixedpoint.Zero, fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", quoteInvestment.Float64(), quoteBalance.Float64()) - } - +func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, quantity, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { // check more investment budget details requiredBase = fixedpoint.Zero requiredQuote = fixedpoint.Zero @@ -249,7 +241,7 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseInvestment, quoteInvest return requiredBase, requiredQuote, nil } -func (s *Strategy) checkRequiredInvestmentByAmount(baseInvestment, quoteInvestment, baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { +func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) { // check more investment budget details requiredBase = fixedpoint.Zero @@ -340,13 +332,16 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. - quantityOrAmountIsSet := s.QuantityOrAmount.IsSet() - if quantityOrAmountIsSet { - if _, _, err2 := s.checkRequiredInvestmentByQuantity( - s.BaseInvestment, s.QuoteInvestment, - totalBase, totalQuote, - lastPrice, s.QuantityOrAmount.Quantity, s.grid.Pins); err != nil { - return err2 + if s.QuantityOrAmount.IsSet() { + if quantity := s.QuantityOrAmount.Quantity; !quantity.IsZero() { + if _, _, err2 := s.checkRequiredInvestmentByQuantity(totalBase, totalQuote, lastPrice, s.QuantityOrAmount.Quantity, s.grid.Pins); err != nil { + return err2 + } + } + if amount := s.QuantityOrAmount.Amount; !amount.IsZero() { + if _, _, err2 := s.checkRequiredInvestmentByAmount(totalBase, totalQuote, lastPrice, amount, s.grid.Pins); err != nil { + return err2 + } } } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index ff3163497..41217c4bb 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -17,47 +17,39 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { } t.Run("basic base balance check", func(t *testing.T) { - _, _, err := s.checkRequiredInvestmentByQuantity(number(2.0), number(10_000.0), - number(1.0), number(10_000.0), - number(0.1), number(19000.0), []Pin{}) + _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), number(0.1), number(19000.0), []Pin{}) assert.Error(t, err) assert.EqualError(t, err, "baseInvestment setup 2.000000 is greater than the total base balance 1.000000") }) t.Run("basic quote balance check", func(t *testing.T) { - _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), - number(1.0), number(100.0), - number(0.1), number(19_000.0), []Pin{}) + _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(100.0), number(0.1), number(19_000.0), []Pin{}) assert.Error(t, err) assert.EqualError(t, err, "quoteInvestment setup 10000.000000 is greater than the total quote balance 100.000000") }) t.Run("quote to base balance conversion check", func(t *testing.T) { - _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(10_000.0), - number(0.0), number(10_000.0), - number(0.1), number(13_500.0), []Pin{ - Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) - Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) - Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) - Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) - Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) - Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD - }) + _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(10_000.0), number(0.1), number(13_500.0), []Pin{ + Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) + Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) + Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) + Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) + Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) + Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD + }) assert.NoError(t, err) assert.Equal(t, number(6000.0), requiredQuote) }) t.Run("quote to base balance conversion not enough", func(t *testing.T) { - _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(5_000.0), - number(0.0), number(5_000.0), - number(0.1), number(13_500.0), []Pin{ - Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) - Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) - Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) - Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) - Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) - Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD - }) + _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(5_000.0), number(0.1), number(13_500.0), []Pin{ + Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) + Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy) + Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy) + Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy) + Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy) + Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD + }) assert.EqualError(t, err, "quote balance (5000.000000 USDT) is not enough, required = quote 6000.000000") assert.Equal(t, number(6000.0), requiredQuote) }) From e3c735b7005a7de78944d2a2e9ecfbe817459a86 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 24 Nov 2022 15:55:02 +0800 Subject: [PATCH 34/59] grid2: add more code to setupGridOrders --- pkg/strategy/grid2/strategy.go | 117 ++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 44 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 4b2d69765..6a28059c2 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -301,6 +301,9 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am return requiredBase, requiredQuote, nil } +// setupGridOrders +// 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. +// 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSession) error { lastPrice, err := s.getLastTradePrice(ctx, session) if err != nil { @@ -321,15 +324,6 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe totalBase := baseBalance.Available totalQuote := quoteBalance.Available - if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { - if s.BaseInvestment.Compare(totalBase) > 0 { - return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", s.BaseInvestment.Float64(), totalBase.Float64()) - } - if s.QuoteInvestment.Compare(totalQuote) > 0 { - return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", s.QuoteInvestment.Float64(), totalQuote.Float64()) - } - } - // shift 1 grid because we will start from the buy order // if the buy order is filled, then we will submit another sell order at the higher grid. if s.QuantityOrAmount.IsSet() { @@ -345,42 +339,77 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } - for i := len(s.grid.Pins) - 2; i >= 0; i++ { - pin := s.grid.Pins[i] - price := fixedpoint.Value(pin) - - if price.Compare(lastPrice) >= 0 { - // check sell order - if quantityOrAmountIsSet { - if s.QuantityOrAmount.Quantity.Sign() > 0 { - quantity := s.QuantityOrAmount.Quantity - - createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Quantity: quantity, - Price: price, - Market: s.Market, - TimeInForce: types.TimeInForceGTC, - Tag: "grid", - }) - - if err2 != nil { - return err2 - } - - _ = createdOrders - - } else if s.QuantityOrAmount.Amount.Sign() > 0 { - - } - } else if s.BaseInvestment.Sign() > 0 { - - } else { - // error: either quantity, amount, baseInvestment is not set. - } + if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { + if s.BaseInvestment.Compare(totalBase) > 0 { + return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", s.BaseInvestment.Float64(), totalBase.Float64()) } + if s.QuoteInvestment.Compare(totalQuote) > 0 { + return fmt.Errorf("quoteInvestment setup %f is greater than the total quote balance %f", s.QuoteInvestment.Float64(), totalQuote.Float64()) + } + + if !s.QuantityOrAmount.IsSet() { + // TODO: calculate and override the quantity here + } + } + + var buyPlacedPrice = fixedpoint.Zero + var pins = s.grid.Pins + var usedBase = fixedpoint.Zero + var usedQuote = fixedpoint.Zero + var submitOrders []types.SubmitOrder + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + quantity := s.QuantityOrAmount.Quantity + if quantity.IsZero() { + quantity = s.QuantityOrAmount.Amount.Div(price) + } + + // TODO: add fee if we don't have the platform token. BNB, OKB or MAX... + if price.Compare(lastPrice) >= 0 { + if usedBase.Add(quantity).Compare(totalBase) < 0 { + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeSell, + Price: price, + Quantity: quantity, + }) + usedBase = usedBase.Add(quantity) + } else if i > 0 { + // next price + nextPin := pins[i-1] + nextPrice := fixedpoint.Value(nextPin) + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeBuy, + Price: nextPrice, + Quantity: quantity, + }) + quoteQuantity := quantity.Mul(price) + usedQuote = usedQuote.Add(quoteQuantity) + buyPlacedPrice = nextPrice + } + } else { + } + + /* + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", + }) + + if err2 != nil { + return err2 + } + */ } return nil From 020e7c8604622f1de05fe8357ff8aab175d0f220 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 24 Nov 2022 16:35:31 +0800 Subject: [PATCH 35/59] grid2: handle grid orders submission --- pkg/strategy/grid2/strategy.go | 49 +++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6a28059c2..fbbbed77c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -369,11 +369,14 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe if price.Compare(lastPrice) >= 0 { if usedBase.Add(quantity).Compare(totalBase) < 0 { submitOrders = append(submitOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimitMaker, - Side: types.SideTypeSell, - Price: price, - Quantity: quantity, + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeSell, + Price: price, + Quantity: quantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", }) usedBase = usedBase.Add(quantity) } else if i > 0 { @@ -381,35 +384,43 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe nextPin := pins[i-1] nextPrice := fixedpoint.Value(nextPin) submitOrders = append(submitOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimitMaker, - Side: types.SideTypeBuy, - Price: nextPrice, - Quantity: quantity, + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeBuy, + Price: nextPrice, + Quantity: quantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + Tag: "grid", }) quoteQuantity := quantity.Mul(price) usedQuote = usedQuote.Add(quoteQuantity) buyPlacedPrice = nextPrice } } else { - } + if price.Compare(buyPlacedPrice) >= 0 { + continue + } - /* - createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Quantity: quantity, Price: price, + Quantity: quantity, Market: s.Market, TimeInForce: types.TimeInForceGTC, Tag: "grid", }) + quoteQuantity := quantity.Mul(price) + usedQuote = usedQuote.Add(quoteQuantity) + } - if err2 != nil { - return err2 - } - */ + createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...) + if err2 != nil { + return err + } + _ = createdOrders } return nil From 622fe75ed3db432e39cf96c3e8aba76cdd2cbe6b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 25 Nov 2022 15:05:11 +0800 Subject: [PATCH 36/59] grid2: check buy placed order price --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index fbbbed77c..42897e184 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -209,7 +209,7 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance, } } else { // for orders that buy - if price.Compare(buyPlacedPrice) == 0 { + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { continue } requiredQuote = requiredQuote.Add(quantity.Mul(price)) @@ -269,7 +269,7 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am } } else { // for orders that buy - if price.Compare(buyPlacedPrice) == 0 { + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { continue } requiredQuote = requiredQuote.Add(amount) @@ -398,7 +398,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe buyPlacedPrice = nextPrice } } else { - if price.Compare(buyPlacedPrice) >= 0 { + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) >= 0 { continue } From 1629a25beb5c3417d6200bf35df47f1091478d06 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 25 Nov 2022 15:42:45 +0800 Subject: [PATCH 37/59] grid2: fix tests --- pkg/strategy/grid2/strategy_test.go | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 41217c4bb..75759c8cd 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -16,18 +16,6 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { }, } - t.Run("basic base balance check", func(t *testing.T) { - _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(10_000.0), number(0.1), number(19000.0), []Pin{}) - assert.Error(t, err) - assert.EqualError(t, err, "baseInvestment setup 2.000000 is greater than the total base balance 1.000000") - }) - - t.Run("basic quote balance check", func(t *testing.T) { - _, _, err := s.checkRequiredInvestmentByQuantity(number(1.0), number(100.0), number(0.1), number(19_000.0), []Pin{}) - assert.Error(t, err) - assert.EqualError(t, err, "quoteInvestment setup 10000.000000 is greater than the total quote balance 100.000000") - }) - t.Run("quote to base balance conversion check", func(t *testing.T) { _, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(10_000.0), number(0.1), number(13_500.0), []Pin{ Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy) @@ -64,7 +52,7 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { } t.Run("quote to base balance conversion", func(t *testing.T) { - _, requiredQuote, err := s.checkRequiredInvestmentByAmount(number(0.0), number(3_000.0), + _, requiredQuote, err := s.checkRequiredInvestmentByAmount( number(0.0), number(3_000.0), number(1000.0), number(13_500.0), []Pin{ @@ -76,6 +64,6 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { Pin(number(15_000.0)), }) assert.EqualError(t, err, "quote balance (3000.000000 USDT) is not enough, required = quote 4999.999890") - assert.Equal(t, number(4999.99989), requiredQuote) + assert.InDelta(t, 4999.99989, requiredQuote.Float64(), number(0.001).Float64()) }) } From e981ad641a313fc1abf7b8e635e05bebaa1b1a9c Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 25 Nov 2022 18:07:02 +0800 Subject: [PATCH 38/59] grid2: ignore test build for dnum --- pkg/strategy/grid2/grid.go | 7 +++++-- pkg/strategy/grid2/grid_test.go | 13 ++++++++++--- pkg/strategy/grid2/strategy_test.go | 2 ++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/grid2/grid.go b/pkg/strategy/grid2/grid.go index 9121a7ea9..b2939e223 100644 --- a/pkg/strategy/grid2/grid.go +++ b/pkg/strategy/grid2/grid.go @@ -34,10 +34,13 @@ type Pin fixedpoint.Value func calculateArithmeticPins(lower, upper, spread, tickSize fixedpoint.Value) []Pin { var pins []Pin + var ts = tickSize.Float64() for p := lower; p.Compare(upper) <= 0; p = p.Add(spread) { // tickSize here = 0.01 - pp := math.Trunc(p.Float64()/tickSize.Float64()) * tickSize.Float64() - pins = append(pins, Pin(fixedpoint.NewFromFloat(pp))) + pp := p.Float64() / ts + pp = math.Trunc(pp) * ts + pin := Pin(fixedpoint.NewFromFloat(pp)) + pins = append(pins, pin) } return pins diff --git a/pkg/strategy/grid2/grid_test.go b/pkg/strategy/grid2/grid_test.go index ccd7245cf..8edfe59cd 100644 --- a/pkg/strategy/grid2/grid_test.go +++ b/pkg/strategy/grid2/grid_test.go @@ -1,3 +1,5 @@ +//go:build !dnum + package grid2 import ( @@ -170,7 +172,7 @@ func Test_calculateArithmeticPins(t *testing.T) { Pin(number(1000.0)), Pin(number(1066.660)), Pin(number(1133.330)), - Pin(number(1199.990)), + Pin(number("1199.99")), Pin(number(1266.660)), Pin(number(1333.330)), Pin(number(1399.990)), @@ -197,14 +199,19 @@ func Test_calculateArithmeticPins(t *testing.T) { Pin(number(2799.990)), Pin(number(2866.660)), Pin(number(2933.330)), - Pin(number(2999.990)), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { spread := tt.args.upper.Sub(tt.args.lower).Div(tt.args.size) - assert.Equalf(t, tt.want, calculateArithmeticPins(tt.args.lower, tt.args.upper, spread, tt.args.tickSize), "calculateArithmeticPins(%v, %v, %v, %v)", tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize) + pins := calculateArithmeticPins(tt.args.lower, tt.args.upper, spread, tt.args.tickSize) + for i := 0; i < len(tt.want); i++ { + assert.InDelta(t, fixedpoint.Value(tt.want[i]).Float64(), + fixedpoint.Value(pins[i]).Float64(), + 0.001, + "calculateArithmeticPins(%v, %v, %v, %v)", tt.args.lower, tt.args.upper, tt.args.size, tt.args.tickSize) + } }) } } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 75759c8cd..e27c87ac9 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -1,3 +1,5 @@ +//go:build !dnum + package grid2 import ( From e385b589b66bddb78ea99d24a8670cb7d041a340 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 27 Nov 2022 00:19:59 +0800 Subject: [PATCH 39/59] config: add grid2 config --- config/grid2.yaml | 33 +++++++++++++++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 6 +++++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 config/grid2.yaml diff --git a/config/grid2.yaml b/config/grid2.yaml new file mode 100644 index 000000000..e4338bcfd --- /dev/null +++ b/config/grid2.yaml @@ -0,0 +1,33 @@ +--- +sessions: + binance: + exchange: binance + envVarPrefix: binance + max: + exchange: max + envVarPrefix: max + +# example command: +# godotenv -f .env.local -- go run ./cmd/bbgo backtest --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline +backtest: + startTime: "2022-01-01" + endTime: "2022-11-25" + symbols: + - BTCUSDT + sessions: [max] + accounts: + binance: + balances: + BTC: 0.0 + USDT: 10000.0 + +exchangeStrategies: + +- on: max + grid2: + symbol: BTCUSDT + upperPrice: 10_000.0 + lowerPrice: 15_000.0 + gridNumber: 10 + quantity: 0.001 + # profitSpread: 1000.0 # The profit price spread that you want to add to your sell order when your buy order is executed diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 42897e184..1110917a9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -337,6 +337,8 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe return err2 } } + } else { + // TODO: calculate the quantity from the investment configuration } if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { @@ -420,7 +422,9 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe if err2 != nil { return err } - _ = createdOrders + for _, order := range createdOrders { + log.Infof(order.String()) + } } return nil From 9f2e4d3f711a09c7fac3cff1be551c8cc3bf2c2b Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 27 Nov 2022 19:11:45 +0800 Subject: [PATCH 40/59] grid2: add calculateQuoteInvestmentQuantity so that we can calculate quantity from the quote investment --- pkg/strategy/grid2/strategy.go | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 1110917a9..261f3494e 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -301,6 +301,43 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am return requiredBase, requiredQuote, nil } +func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { + buyPlacedPrice := fixedpoint.Zero + + // quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + .... + // => + // quoteInvestment = (p1 + p2 + p3) * q + // q = quoteInvestment / (p1 + p2 + p3) + totalQuotePrice := fixedpoint.Zero + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + + if price.Compare(lastPrice) >= 0 { + // for orders that sell + // if we still have the base balance + // quantity := amount.Div(lastPrice) + if i > 0 { // we do not want to sell at i == 0 + // convert sell to buy quote and add to requiredQuote + nextLowerPin := pins[i-1] + nextLowerPrice := fixedpoint.Value(nextLowerPin) + // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) + totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) + buyPlacedPrice = nextLowerPrice + } + } else { + // for orders that buy + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { + continue + } + + totalQuotePrice = totalQuotePrice.Add(price) + } + } + + return quoteInvestment.Div(totalQuotePrice), nil +} + // setupGridOrders // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. @@ -339,6 +376,13 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } else { // TODO: calculate the quantity from the investment configuration + if !s.QuoteInvestment.IsZero() { + quantity, err2 := s.calculateQuoteInvestmentQuantity(s.QuoteInvestment, lastPrice, s.grid.Pins) + if err2 != nil { + return err2 + } + _ = quantity + } } if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { From 2260fd6908d6886d39e61fd8ac993e69e3852a33 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 27 Nov 2022 19:19:54 +0800 Subject: [PATCH 41/59] grid2: add TestStrategy_calculateQuoteInvestmentQuantity test case --- pkg/strategy/grid2/strategy_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index e27c87ac9..7e2f06483 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -69,3 +69,29 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { assert.InDelta(t, 4999.99989, requiredQuote.Float64(), number(0.001).Float64()) }) } + +func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { + s := &Strategy{ + Market: types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + }, + } + + t.Run("calculate quote quantity from quote investment", func(t *testing.T) { + // quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000) * q + // q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000) + // q = 12_000 / (10,000 + 11,000 + 12,000 + 13,000 + 14,000) + // q = 0.2 + quantity, err := s.calculateQuoteInvestmentQuantity(number(12_000.0), number(13_500.0), []Pin{ + Pin(number(10_000.0)), // buy + Pin(number(11_000.0)), // buy + Pin(number(12_000.0)), // buy + Pin(number(13_000.0)), // buy + Pin(number(14_000.0)), // buy + Pin(number(15_000.0)), + }) + assert.NoError(t, err) + assert.Equal(t, number(0.2).String(), quantity.String()) + }) +} From 4eb652b560a2804fa8c2968291dbcaf0da0040a8 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 30 Nov 2022 12:46:39 +0800 Subject: [PATCH 42/59] grid2: add calculateQuoteBaseInvestmentQuantity --- pkg/strategy/grid2/strategy.go | 70 +++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 261f3494e..daf3636a4 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -338,6 +338,65 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f return quoteInvestment.Div(totalQuotePrice), nil } +func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { + // q_p1 = q_p2 = q_p3 = q_p4 + // baseInvestment = q_p1 + q_p2 + q_p3 + q_p4 + .... + // baseInvestment = numberOfSellOrders * q + // maxBaseQuantity = baseInvestment / numberOfSellOrders + // if maxBaseQuantity < minQuantity or maxBaseQuantity * priceLowest < minNotional + // then reduce the numberOfSellOrders + numberOfSellOrders := 0 + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + if price.Compare(lastPrice) < 0 { + break + } + numberOfSellOrders++ + } + + numberOfSellOrders++ + maxBaseQuantity := fixedpoint.Zero + for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 { + numberOfSellOrders-- + maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(numberOfSellOrders))) + } + + buyPlacedPrice := fixedpoint.Zero + totalQuotePrice := fixedpoint.Zero + // quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + .... + // => + // quoteInvestment = (p1 + p2 + p3) * q + // maxBuyQuantity = quoteInvestment / (p1 + p2 + p3) + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + + if price.Compare(lastPrice) >= 0 { + // for orders that sell + // if we still have the base balance + // quantity := amount.Div(lastPrice) + if i > 0 { // we do not want to sell at i == 0 + // convert sell to buy quote and add to requiredQuote + nextLowerPin := pins[i-1] + nextLowerPrice := fixedpoint.Value(nextLowerPin) + // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) + totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) + buyPlacedPrice = nextLowerPrice + } + } else { + // for orders that buy + if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 { + continue + } + + totalQuotePrice = totalQuotePrice.Add(price) + } + } + + return quoteInvestment.Div(totalQuotePrice), nil +} + // setupGridOrders // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. @@ -376,12 +435,19 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } else { // TODO: calculate the quantity from the investment configuration - if !s.QuoteInvestment.IsZero() { + if !s.QuoteInvestment.IsZero() && !s.BaseInvestment.IsZero() { + quantity, err2 := s.calculateQuoteBaseInvestmentQuantity(s.QuoteInvestment, s.BaseInvestment, lastPrice, s.grid.Pins) + if err2 != nil { + return err2 + } + s.QuantityOrAmount.Quantity = quantity + + } else if !s.QuoteInvestment.IsZero() { quantity, err2 := s.calculateQuoteInvestmentQuantity(s.QuoteInvestment, lastPrice, s.grid.Pins) if err2 != nil { return err2 } - _ = quantity + s.QuantityOrAmount.Quantity = quantity } } From 45328a9f3d29ea4b0428f1690cd31a281d4b423a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 30 Nov 2022 12:52:04 +0800 Subject: [PATCH 43/59] grid2: add comment for the quantity loop --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index daf3636a4..4cc2eb131 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -355,6 +355,8 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv numberOfSellOrders++ } + // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders + // so that the quantity can be increased. numberOfSellOrders++ maxBaseQuantity := fixedpoint.Zero for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 { From 46bebb102209f8f7c4b03d47f007fdcbc46de6e4 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 30 Nov 2022 12:55:23 +0800 Subject: [PATCH 44/59] grid2: calculate minBaseQuantity --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 4cc2eb131..25cf7940c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -358,11 +358,13 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders // so that the quantity can be increased. numberOfSellOrders++ + minBaseQuantity := fixedpoint.Max(s.Market.MinNotional.Div(lastPrice), s.Market.MinQuantity) maxBaseQuantity := fixedpoint.Zero for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 { numberOfSellOrders-- maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(numberOfSellOrders))) } + log.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) buyPlacedPrice := fixedpoint.Zero totalQuotePrice := fixedpoint.Zero From e80c8f295974c8ac6e96490b8e9737f55f031844 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 30 Nov 2022 12:57:53 +0800 Subject: [PATCH 45/59] grid2: pull out maxNumberOfSellOrders --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 25cf7940c..76b72d084 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -357,12 +357,12 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders // so that the quantity can be increased. - numberOfSellOrders++ + maxNumberOfSellOrders := numberOfSellOrders + 1 minBaseQuantity := fixedpoint.Max(s.Market.MinNotional.Div(lastPrice), s.Market.MinQuantity) maxBaseQuantity := fixedpoint.Zero for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 { - numberOfSellOrders-- - maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(numberOfSellOrders))) + maxNumberOfSellOrders-- + maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) } log.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) From 22569fcb30231db819a06778ec62122de166f333 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Dec 2022 14:51:28 +0800 Subject: [PATCH 46/59] grid2: fix quantity calculation --- pkg/strategy/grid2/strategy.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 76b72d084..7fd199a5b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -360,7 +360,7 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv maxNumberOfSellOrders := numberOfSellOrders + 1 minBaseQuantity := fixedpoint.Max(s.Market.MinNotional.Div(lastPrice), s.Market.MinQuantity) maxBaseQuantity := fixedpoint.Zero - for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 { + for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 || maxBaseQuantity.Compare(minBaseQuantity) <= 0 { maxNumberOfSellOrders-- maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) } @@ -398,7 +398,8 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv } } - return quoteInvestment.Div(totalQuotePrice), nil + quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) + return fixedpoint.Max(quoteSideQuantity, maxBaseQuantity), nil } // setupGridOrders From 29f3ff7ba2e9f023b67b885911d220fd98acca58 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 1 Dec 2022 16:30:44 +0800 Subject: [PATCH 47/59] grid2: remove todo --- pkg/strategy/grid2/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7fd199a5b..f698ce0ba 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -439,7 +439,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } } else { - // TODO: calculate the quantity from the investment configuration + // calculate the quantity from the investment configuration if !s.QuoteInvestment.IsZero() && !s.BaseInvestment.IsZero() { quantity, err2 := s.calculateQuoteBaseInvestmentQuantity(s.QuoteInvestment, s.BaseInvestment, lastPrice, s.grid.Pins) if err2 != nil { From 2b148038296de94726b9067d40b0823ee39f7950 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 2 Dec 2022 00:09:47 +0800 Subject: [PATCH 48/59] grid2: add comment --- pkg/strategy/grid2/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f698ce0ba..889ce038b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -456,6 +456,8 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe } } + // if base investment and quote investment is set, when we should check if the + // investment configuration is valid with the current balances if !s.BaseInvestment.IsZero() && !s.QuoteInvestment.IsZero() { if s.BaseInvestment.Compare(totalBase) > 0 { return fmt.Errorf("baseInvestment setup %f is greater than the total base balance %f", s.BaseInvestment.Float64(), totalBase.Float64()) From 3fc65122b6e95bcef88a3dfb514ef6ecf74e5c1e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 2 Dec 2022 13:46:09 +0800 Subject: [PATCH 49/59] config: update grid2 config --- config/grid2.yaml | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/config/grid2.yaml b/config/grid2.yaml index e4338bcfd..6696201cf 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -15,6 +15,7 @@ backtest: symbols: - BTCUSDT sessions: [max] + # sessions: [binance] accounts: binance: balances: @@ -29,5 +30,25 @@ exchangeStrategies: upperPrice: 10_000.0 lowerPrice: 15_000.0 gridNumber: 10 - quantity: 0.001 - # profitSpread: 1000.0 # The profit price spread that you want to add to your sell order when your buy order is executed + + ## profitSpread is the profit spread of the arbitrage order (sell order) + ## greater the profitSpread, greater the profit you make when the sell order is filled. + ## you can set this instead of the default grid profit spread. + ## by default, profitSpread = (upperPrice - lowerPrice) / gridNumber + ## that is, greater the gridNumber, lesser the profit of each grid. + # profitSpread: 1000.0 + + ## There are 3 kinds of setup + ## NOTICE: you can only choose one, uncomment the config to enable it + ## + ## 1) fixed amount: amount is the quote unit (e.g. USDT in BTCUSDT) + # amount: 10.0 + + ## 2) fixed quantity: it will use your balance to place orders with the fixed quantity. e.g. 0.001 BTC + # quantity: 0.001 + + ## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid. + ## quoteInvestment is required, and baseInvestment is optional (could be zero) + ## if you have existing BTC position and want to reuse it you can set the baseInvestment. + quoteInvestment: 10_000 + baseInvestment: 1.0 From 26e221cf7e84a4d16ecbae101bb0ed20d79bfc41 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:02:36 +0800 Subject: [PATCH 50/59] service: fix backtest test for binance restrict --- pkg/service/backtest_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/service/backtest_test.go b/pkg/service/backtest_test.go index ca863e287..2d7b5bbb2 100644 --- a/pkg/service/backtest_test.go +++ b/pkg/service/backtest_test.go @@ -3,6 +3,8 @@ package service import ( "context" "database/sql" + "os" + "strconv" "testing" "time" @@ -15,6 +17,10 @@ import ( ) func TestBacktestService_FindMissingTimeRanges_EmptyData(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + db, err := prepareDB(t) if err != nil { t.Fatal(err) @@ -40,6 +46,10 @@ func TestBacktestService_FindMissingTimeRanges_EmptyData(t *testing.T) { } func TestBacktestService_QueryExistingDataRange(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + db, err := prepareDB(t) if err != nil { t.Fatal(err) @@ -67,6 +77,10 @@ func TestBacktestService_QueryExistingDataRange(t *testing.T) { } func TestBacktestService_SyncPartial(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + db, err := prepareDB(t) if err != nil { t.Fatal(err) @@ -113,6 +127,10 @@ func TestBacktestService_SyncPartial(t *testing.T) { } func TestBacktestService_FindMissingTimeRanges(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + db, err := prepareDB(t) if err != nil { t.Fatal(err) From 1e13fe619124ec43779def0bb81a18023d21be26 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:02:55 +0800 Subject: [PATCH 51/59] grid2: fix grid2 strategy validation --- pkg/strategy/grid2/strategy.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 889ce038b..db94b253a 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -100,17 +100,18 @@ func (s *Strategy) Validate() error { return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) } - if s.ProfitSpread.Sign() <= 0 { - // If profitSpread is empty or its value is negative - return fmt.Errorf("profit spread should bigger than 0") - } - if s.GridNum == 0 { return fmt.Errorf("gridNum can not be zero") } if err := s.QuantityOrAmount.Validate(); err != nil { - return err + if s.QuoteInvestment.IsZero() && s.BaseInvestment.IsZero() { + return err + } + } + + if !s.QuantityOrAmount.IsSet() && s.QuoteInvestment.IsZero() && s.BaseInvestment.IsZero() { + return fmt.Errorf("one of quantity, amount, quoteInvestment must be set") } return nil From f5bb22c82da5d3f6df83ebeac3dcfd2519808697 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:16:29 +0800 Subject: [PATCH 52/59] config: update grid2 config files --- config/grid2-max.yaml | 50 +++++++++++++++++++++++++++++++++++++++++++ config/grid2.yaml | 14 +++++------- 2 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 config/grid2-max.yaml diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml new file mode 100644 index 000000000..9fadfa312 --- /dev/null +++ b/config/grid2-max.yaml @@ -0,0 +1,50 @@ +--- +sessions: + max: + exchange: max + envVarPrefix: max + +# example command: +# godotenv -f .env.local -- go run ./cmd/bbgo backtest --config config/grid2-max.yaml --base-asset-baseline +backtest: + startTime: "2022-01-01" + endTime: "2022-11-25" + symbols: + - BTCUSDT + sessions: [max] + accounts: + binance: + balances: + BTC: 0.0 + USDT: 10000.0 + +exchangeStrategies: + +- on: max + grid2: + symbol: BTCUSDT + upperPrice: 15_000.0 + lowerPrice: 10_000.0 + gridNumber: 10 + + ## profitSpread is the profit spread of the arbitrage order (sell order) + ## greater the profitSpread, greater the profit you make when the sell order is filled. + ## you can set this instead of the default grid profit spread. + ## by default, profitSpread = (upperPrice - lowerPrice) / gridNumber + ## that is, greater the gridNumber, lesser the profit of each grid. + # profitSpread: 1000.0 + + ## There are 3 kinds of setup + ## NOTICE: you can only choose one, uncomment the config to enable it + ## + ## 1) fixed amount: amount is the quote unit (e.g. USDT in BTCUSDT) + # amount: 10.0 + + ## 2) fixed quantity: it will use your balance to place orders with the fixed quantity. e.g. 0.001 BTC + # quantity: 0.001 + + ## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid. + ## quoteInvestment is required, and baseInvestment is optional (could be zero) + ## if you have existing BTC position and want to reuse it you can set the baseInvestment. + quoteInvestment: 10_000 + baseInvestment: 1.0 diff --git a/config/grid2.yaml b/config/grid2.yaml index 6696201cf..a6aa3d920 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -3,19 +3,15 @@ sessions: binance: exchange: binance envVarPrefix: binance - max: - exchange: max - envVarPrefix: max # example command: -# godotenv -f .env.local -- go run ./cmd/bbgo backtest --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline +# go run ./cmd/bbgo backtest --config config/grid2.yaml --base-asset-baseline backtest: startTime: "2022-01-01" endTime: "2022-11-25" symbols: - BTCUSDT - sessions: [max] - # sessions: [binance] + sessions: [binance] accounts: binance: balances: @@ -24,11 +20,11 @@ backtest: exchangeStrategies: -- on: max +- on: binance grid2: symbol: BTCUSDT - upperPrice: 10_000.0 - lowerPrice: 15_000.0 + upperPrice: 15_000.0 + lowerPrice: 10_000.0 gridNumber: 10 ## profitSpread is the profit spread of the arbitrage order (sell order) From d91921f6c2b9e32e613cc18bd5a9c2698da776f7 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:25:18 +0800 Subject: [PATCH 53/59] grid2: fix grid sell order quantity calculation --- pkg/strategy/grid2/strategy.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index db94b253a..7c3a82dd5 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -340,6 +340,7 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f } func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { + log.Infof("calculating quantity by quote/base investment: %f / %f", baseInvestment.Float64(), quoteInvestment.Float64()) // q_p1 = q_p2 = q_p3 = q_p4 // baseInvestment = q_p1 + q_p2 + q_p3 + q_p4 + .... // baseInvestment = numberOfSellOrders * q @@ -365,7 +366,10 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv maxNumberOfSellOrders-- maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) } - log.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) + log.Infof("grid %s base investment sell orders: %d", s.Symbol, maxNumberOfSellOrders) + if maxNumberOfSellOrders > 0 { + log.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) + } buyPlacedPrice := fixedpoint.Zero totalQuotePrice := fixedpoint.Zero @@ -400,7 +404,11 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv } quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) - return fixedpoint.Max(quoteSideQuantity, maxBaseQuantity), nil + if maxNumberOfSellOrders > 0 { + return fixedpoint.Max(quoteSideQuantity, maxBaseQuantity), nil + } + + return quoteSideQuantity, nil } // setupGridOrders From a71593310631c3906452cfc80aff7710157470de Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:31:44 +0800 Subject: [PATCH 54/59] grid2: allocate logger instance for fields --- pkg/strategy/grid2/strategy.go | 20 ++++++++++++-------- pkg/strategy/grid2/strategy_test.go | 6 ++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7c3a82dd5..221af5b4c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -72,15 +72,14 @@ type Strategy struct { // orderStore is used to store all the created orders, so that we can filter the trades. orderStore *bbgo.OrderStore - // activeOrders is the locally maintained active order book of the maker orders. - activeOrders *bbgo.ActiveOrderBook - tradeCollector *bbgo.TradeCollector orderExecutor *bbgo.GeneralOrderExecutor // groupID is the group ID used for the strategy instance for canceling orders groupID uint32 + + logger *logrus.Entry } func (s *Strategy) ID() string { @@ -133,9 +132,13 @@ func (s *Strategy) handleOrderFilled(o types.Order) { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() + s.logger = log.WithFields(logrus.Fields{ + "symbol": s.Symbol, + }) + s.groupID = util.FNV32(instanceID) - log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.Market) @@ -162,9 +165,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.Sync(ctx, s) // now we can cancel the open orders - log.Infof("canceling active orders...") - if err := session.Exchange.CancelOrders(context.Background(), s.activeOrders.Orders()...); err != nil { - log.WithError(err).Errorf("cancel order error") + s.logger.Infof("canceling active orders...") + + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("graceful order cancel error") } }) @@ -340,7 +344,7 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f } func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) { - log.Infof("calculating quantity by quote/base investment: %f / %f", baseInvestment.Float64(), quoteInvestment.Float64()) + s.logger.Infof("calculating quantity by quote/base investment: %f / %f", baseInvestment.Float64(), quoteInvestment.Float64()) // q_p1 = q_p2 = q_p3 = q_p4 // baseInvestment = q_p1 + q_p2 + q_p3 + q_p4 + .... // baseInvestment = numberOfSellOrders * q diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 7e2f06483..fd7b8493f 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -5,6 +5,7 @@ package grid2 import ( "testing" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/types" @@ -12,6 +13,8 @@ import ( func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { s := &Strategy{ + logger: logrus.NewEntry(logrus.New()), + Market: types.Market{ BaseCurrency: "BTC", QuoteCurrency: "USDT", @@ -47,6 +50,8 @@ func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { s := &Strategy{ + + logger: logrus.NewEntry(logrus.New()), Market: types.Market{ BaseCurrency: "BTC", QuoteCurrency: "USDT", @@ -72,6 +77,7 @@ func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) { func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) { s := &Strategy{ + logger: logrus.NewEntry(logrus.New()), Market: types.Market{ BaseCurrency: "BTC", QuoteCurrency: "USDT", From a825ae5d04da5fe56f4cffdf9fce3d438a5ddeeb Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 11:36:14 +0800 Subject: [PATCH 55/59] grid2: use custom logger entry --- pkg/strategy/grid2/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 221af5b4c..dfefe60d8 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -370,9 +370,9 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv maxNumberOfSellOrders-- maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders))) } - log.Infof("grid %s base investment sell orders: %d", s.Symbol, maxNumberOfSellOrders) + s.logger.Infof("grid %s base investment sell orders: %d", s.Symbol, maxNumberOfSellOrders) if maxNumberOfSellOrders > 0 { - log.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) + s.logger.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64()) } buyPlacedPrice := fixedpoint.Zero @@ -553,7 +553,7 @@ func (s *Strategy) setupGridOrders(ctx context.Context, session *bbgo.ExchangeSe return err } for _, order := range createdOrders { - log.Infof(order.String()) + s.logger.Infof(order.String()) } } From de398ef146c289327e143b487c4099f2e5872b8c Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 12:31:09 +0800 Subject: [PATCH 56/59] config: update grid2 max config --- config/grid2-max.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 9fadfa312..fb5416da9 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -23,9 +23,9 @@ exchangeStrategies: - on: max grid2: symbol: BTCUSDT - upperPrice: 15_000.0 + upperPrice: 20_000.0 lowerPrice: 10_000.0 - gridNumber: 10 + gridNumber: 50 ## profitSpread is the profit spread of the arbitrage order (sell order) ## greater the profitSpread, greater the profit you make when the sell order is filled. From d5f8c3e7568082b84f8f726abbf16ad5a85dac8d Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 12:35:04 +0800 Subject: [PATCH 57/59] binance: fix binanceapi client test --- pkg/exchange/binance/binanceapi/client_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/exchange/binance/binanceapi/client_test.go b/pkg/exchange/binance/binanceapi/client_test.go index e86bba783..2c06f858f 100644 --- a/pkg/exchange/binance/binanceapi/client_test.go +++ b/pkg/exchange/binance/binanceapi/client_test.go @@ -4,6 +4,8 @@ import ( "context" "log" "net/http/httputil" + "os" + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -12,6 +14,10 @@ import ( ) func getTestClientOrSkip(t *testing.T) *RestClient { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + key, secret, ok := testutil.IntegrationTestConfigured(t, "BINANCE") if !ok { t.SkipNow() @@ -101,6 +107,10 @@ func TestClient_NewGetMarginInterestRateHistoryRequest(t *testing.T) { } func TestClient_privateCall(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + key, secret, ok := testutil.IntegrationTestConfigured(t, "BINANCE") if !ok { t.SkipNow() @@ -136,6 +146,10 @@ func TestClient_privateCall(t *testing.T) { } func TestClient_setTimeOffsetFromServer(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + client := NewClient("") err := client.SetTimeOffsetFromServer(context.Background()) assert.NoError(t, err) From 3521d423105dc8fb57e6520b7699b809df10b149 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 12:36:51 +0800 Subject: [PATCH 58/59] trendtrader: fix converge lint issue --- pkg/strategy/trendtrader/trend.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/trendtrader/trend.go b/pkg/strategy/trendtrader/trend.go index fb27c8568..be12f575a 100644 --- a/pkg/strategy/trendtrader/trend.go +++ b/pkg/strategy/trendtrader/trend.go @@ -2,6 +2,7 @@ package trendtrader import ( "context" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" @@ -149,8 +150,5 @@ func line(p1, p2, p3 float64) int64 { } func converge(mr, ms float64) bool { - if ms > mr { - return true - } - return false + return ms > mr } From 3d77a319fc9cc7ebe840813dfa8c47e85cdf77f4 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 3 Dec 2022 12:42:48 +0800 Subject: [PATCH 59/59] github: set default timeout --- .github/workflows/go.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 196516304..b18cdf76a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -9,6 +9,7 @@ on: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 15 strategy: matrix: