mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-11 01:23:51 +00:00
Merge pull request #1006 from c9s/feature/grid2
strategy: grid2 [part 1] - initializing grid orders
This commit is contained in:
commit
8a8314ec33
1
.github/workflows/go.yml
vendored
1
.github/workflows/go.yml
vendored
|
@ -9,6 +9,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|
50
config/grid2-max.yaml
Normal file
50
config/grid2-max.yaml
Normal file
|
@ -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: 20_000.0
|
||||||
|
lowerPrice: 10_000.0
|
||||||
|
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.
|
||||||
|
## 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
|
50
config/grid2.yaml
Normal file
50
config/grid2.yaml
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
sessions:
|
||||||
|
binance:
|
||||||
|
exchange: binance
|
||||||
|
envVarPrefix: binance
|
||||||
|
|
||||||
|
# example command:
|
||||||
|
# go run ./cmd/bbgo backtest --config config/grid2.yaml --base-asset-baseline
|
||||||
|
backtest:
|
||||||
|
startTime: "2022-01-01"
|
||||||
|
endTime: "2022-11-25"
|
||||||
|
symbols:
|
||||||
|
- BTCUSDT
|
||||||
|
sessions: [binance]
|
||||||
|
accounts:
|
||||||
|
binance:
|
||||||
|
balances:
|
||||||
|
BTC: 0.0
|
||||||
|
USDT: 10000.0
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
|
||||||
|
- on: binance
|
||||||
|
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
|
|
@ -17,6 +17,7 @@ import (
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/fmaker"
|
_ "github.com/c9s/bbgo/pkg/strategy/fmaker"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/funding"
|
_ "github.com/c9s/bbgo/pkg/strategy/funding"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/grid"
|
_ "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/harmonic"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/irr"
|
_ "github.com/c9s/bbgo/pkg/strategy/irr"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/kline"
|
_ "github.com/c9s/bbgo/pkg/strategy/kline"
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -12,6 +14,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTestClientOrSkip(t *testing.T) *RestClient {
|
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")
|
key, secret, ok := testutil.IntegrationTestConfigured(t, "BINANCE")
|
||||||
if !ok {
|
if !ok {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
|
@ -101,6 +107,10 @@ func TestClient_NewGetMarginInterestRateHistoryRequest(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_privateCall(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")
|
key, secret, ok := testutil.IntegrationTestConfigured(t, "BINANCE")
|
||||||
if !ok {
|
if !ok {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
|
@ -136,6 +146,10 @@ func TestClient_privateCall(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_setTimeOffsetFromServer(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("")
|
client := NewClient("")
|
||||||
err := client.SetTimeOffsetFromServer(context.Background())
|
err := client.SetTimeOffsetFromServer(context.Background())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -958,6 +958,10 @@ func (dn Value) integer(mode RoundingMode) Value {
|
||||||
return Value{i, dn.sign, dn.exp}
|
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 {
|
func (dn Value) Round(r int, mode RoundingMode) Value {
|
||||||
if dn.sign == 0 || dn.sign == signNegInf || dn.sign == signPosInf ||
|
if dn.sign == 0 || dn.sign == signNegInf || dn.sign == signPosInf ||
|
||||||
r >= digitsMax {
|
r >= digitsMax {
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
package fixedpoint
|
package fixedpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDelta(t *testing.T) {
|
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)
|
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) {
|
func TestInternal(t *testing.T) {
|
||||||
r := &reader{"1.1e-15", 0}
|
r := &reader{"1.1e-15", 0}
|
||||||
c, e := r.getCoef()
|
c, e := r.getCoef()
|
||||||
|
|
|
@ -3,6 +3,8 @@ package service
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -15,6 +17,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBacktestService_FindMissingTimeRanges_EmptyData(t *testing.T) {
|
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)
|
db, err := prepareDB(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -40,6 +46,10 @@ func TestBacktestService_FindMissingTimeRanges_EmptyData(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBacktestService_QueryExistingDataRange(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)
|
db, err := prepareDB(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -67,6 +77,10 @@ func TestBacktestService_QueryExistingDataRange(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBacktestService_SyncPartial(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)
|
db, err := prepareDB(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -113,6 +127,10 @@ func TestBacktestService_SyncPartial(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBacktestService_FindMissingTimeRanges(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)
|
db, err := prepareDB(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
187
pkg/strategy/grid2/grid.go
Normal file
187
pkg/strategy/grid2/grid.go
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
package grid2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PinCalculator func() []Pin
|
||||||
|
|
||||||
|
type Grid struct {
|
||||||
|
UpperPrice fixedpoint.Value `json:"upperPrice"`
|
||||||
|
LowerPrice fixedpoint.Value `json:"lowerPrice"`
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
|
||||||
|
// Spread is a immutable number
|
||||||
|
Spread fixedpoint.Value `json:"spread"`
|
||||||
|
|
||||||
|
// Pins are the pinned grid prices, from low to high
|
||||||
|
Pins []Pin `json:"pins"`
|
||||||
|
|
||||||
|
pinsCache map[Pin]struct{} `json:"-"`
|
||||||
|
|
||||||
|
calculator PinCalculator
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := p.Float64() / ts
|
||||||
|
pp = math.Trunc(pp) * ts
|
||||||
|
pin := Pin(fixedpoint.NewFromFloat(pp))
|
||||||
|
pins = append(pins, pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
height := upper.Sub(lower)
|
||||||
|
spread := height.Div(size)
|
||||||
|
|
||||||
|
grid := &Grid{
|
||||||
|
UpperPrice: upper,
|
||||||
|
LowerPrice: lower,
|
||||||
|
Size: size,
|
||||||
|
TickSize: tickSize,
|
||||||
|
Spread: spread,
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return g.UpperPrice.Sub(g.LowerPrice)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Grid) Above(price fixedpoint.Value) bool {
|
||||||
|
return price.Compare(g.UpperPrice) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Grid) Below(price fixedpoint.Value) bool {
|
||||||
|
return price.Compare(g.LowerPrice) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Grid) OutOfRange(price fixedpoint.Value) bool {
|
||||||
|
return price.Compare(g.LowerPrice) < 0 || price.Compare(g.UpperPrice) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Grid) HasPin(pin Pin) (ok bool) {
|
||||||
|
_, ok = g.pinsCache[pin]
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
newPins = calculateArithmeticPins(g.UpperPrice.Add(g.Spread), upper, g.Spread, g.TickSize)
|
||||||
|
g.UpperPrice = upper
|
||||||
|
g.addPins(newPins)
|
||||||
|
return newPins
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Grid) ExtendLowerPrice(lower fixedpoint.Value) (newPins []Pin) {
|
||||||
|
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...)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Grid) updatePinsCache() {
|
||||||
|
g.pinsCache = buildPinCache(g.Pins)
|
||||||
|
}
|
217
pkg/strategy/grid2/grid_test.go
Normal file
217
pkg/strategy/grid2/grid_test.go
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
//go:build !dnum
|
||||||
|
|
||||||
|
package grid2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
func number(a interface{}) fixedpoint.Value {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(0.01))
|
||||||
|
grid.CalculateArithmeticPins()
|
||||||
|
|
||||||
|
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, Pin(number(100.0)), grid.Pins[0])
|
||||||
|
assert.Equal(t, Pin(number(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, number(0.01))
|
||||||
|
grid.CalculateArithmeticPins()
|
||||||
|
|
||||||
|
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 := number(500.0)
|
||||||
|
lower := number(100.0)
|
||||||
|
size := number(4.0)
|
||||||
|
grid := NewGrid(lower, upper, size, number(0.01))
|
||||||
|
grid.CalculateArithmeticPins()
|
||||||
|
|
||||||
|
originalSpread := grid.Spread
|
||||||
|
|
||||||
|
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)
|
||||||
|
t.Logf("pins: %+v", grid.Pins)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrid_ExtendLowerPrice(t *testing.T) {
|
||||||
|
upper := fixedpoint.NewFromFloat(3000.0)
|
||||||
|
lower := fixedpoint.NewFromFloat(2000.0)
|
||||||
|
size := fixedpoint.NewFromFloat(10.0)
|
||||||
|
grid := NewGrid(lower, upper, size, number(0.01))
|
||||||
|
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")
|
||||||
|
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, 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, 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 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))
|
||||||
|
grid.CalculateArithmeticPins()
|
||||||
|
|
||||||
|
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))
|
||||||
|
grid.CalculateArithmeticPins()
|
||||||
|
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
|
||||||
|
upper fixedpoint.Value
|
||||||
|
size fixedpoint.Value
|
||||||
|
tickSize fixedpoint.Value
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []Pin
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// (3000-1000)/30 = 66.6666666
|
||||||
|
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.99")),
|
||||||
|
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)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
spread := tt.args.upper.Sub(tt.args.lower).Div(tt.args.size)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
588
pkg/strategy/grid2/strategy.go
Normal file
588
pkg/strategy/grid2/strategy.go
Normal file
|
@ -0,0 +1,588 @@
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 GridProfitStats struct {
|
||||||
|
TotalProfit fixedpoint.Value `json:"totalProfit"`
|
||||||
|
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 {
|
||||||
|
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:"-"`
|
||||||
|
|
||||||
|
// These fields will be filled from the config file (it translates YAML to JSON)
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
|
||||||
|
// ProfitSpread is the fixed profit spread you want to submit the sell order
|
||||||
|
ProfitSpread fixedpoint.Value `json:"profitSpread"`
|
||||||
|
|
||||||
|
// GridNum is the grid number, how many orders you want to post on the orderbook.
|
||||||
|
GridNum int64 `json:"gridNumber"`
|
||||||
|
|
||||||
|
UpperPrice fixedpoint.Value `json:"upperPrice"`
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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.GridNum == 0 {
|
||||||
|
return fmt.Errorf("gridNum can not be zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.QuantityOrAmount.Validate(); err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.logger = log.WithFields(logrus.Fields{
|
||||||
|
"symbol": s.Symbol,
|
||||||
|
})
|
||||||
|
|
||||||
|
s.groupID = util.FNV32(instanceID)
|
||||||
|
|
||||||
|
s.logger.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.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()
|
||||||
|
|
||||||
|
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
bbgo.Sync(ctx, s)
|
||||||
|
|
||||||
|
// now we can cancel the open orders
|
||||||
|
s.logger.Infof("canceling active orders...")
|
||||||
|
|
||||||
|
if err := s.orderExecutor.GracefulCancel(ctx); err != nil {
|
||||||
|
log.WithError(err).Errorf("graceful order cancel error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
session.UserDataStream.OnStart(func() {
|
||||||
|
if err := s.setupGridOrders(ctx, session); err != nil {
|
||||||
|
log.WithError(err).Errorf("failed to setup grid orders")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvestmentBudget struct {
|
||||||
|
baseInvestment fixedpoint.Value
|
||||||
|
quoteInvestment fixedpoint.Value
|
||||||
|
baseBalance fixedpoint.Value
|
||||||
|
quoteBalance fixedpoint.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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 !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
requiredQuote = requiredQuote.Add(quantity.Mul(price))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, amount, lastPrice fixedpoint.Value, pins []Pin) (requiredBase, requiredQuote fixedpoint.Value, err error) {
|
||||||
|
|
||||||
|
// 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 !buyPlacedPrice.IsZero() && 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) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice fixedpoint.Value, pins []Pin) (fixedpoint.Value, error) {
|
||||||
|
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
|
||||||
|
// 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++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
maxNumberOfSellOrders := numberOfSellOrders + 1
|
||||||
|
minBaseQuantity := fixedpoint.Max(s.Market.MinNotional.Div(lastPrice), s.Market.MinQuantity)
|
||||||
|
maxBaseQuantity := fixedpoint.Zero
|
||||||
|
for maxBaseQuantity.Compare(s.Market.MinQuantity) <= 0 || maxBaseQuantity.Compare(minBaseQuantity) <= 0 {
|
||||||
|
maxNumberOfSellOrders--
|
||||||
|
maxBaseQuantity = baseInvestment.Div(fixedpoint.NewFromInt(int64(maxNumberOfSellOrders)))
|
||||||
|
}
|
||||||
|
s.logger.Infof("grid %s base investment sell orders: %d", s.Symbol, maxNumberOfSellOrders)
|
||||||
|
if maxNumberOfSellOrders > 0 {
|
||||||
|
s.logger.Infof("grid %s base investment quantity range: %f <=> %f", s.Symbol, minBaseQuantity.Float64(), maxBaseQuantity.Float64())
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteSideQuantity := quoteInvestment.Div(totalQuotePrice)
|
||||||
|
if maxNumberOfSellOrders > 0 {
|
||||||
|
return fixedpoint.Max(quoteSideQuantity, maxBaseQuantity), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return quoteSideQuantity, 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 {
|
||||||
|
return errors.Wrap(err, "failed to get the last trade price")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
s.QuantityOrAmount.Quantity = quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
Market: s.Market,
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
Tag: "grid",
|
||||||
|
})
|
||||||
|
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,
|
||||||
|
Market: s.Market,
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
Tag: "grid",
|
||||||
|
})
|
||||||
|
quoteQuantity := quantity.Mul(price)
|
||||||
|
usedQuote = usedQuote.Add(quoteQuantity)
|
||||||
|
buyPlacedPrice = nextPrice
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !buyPlacedPrice.IsZero() && price.Compare(buyPlacedPrice) >= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
submitOrders = append(submitOrders, types.SubmitOrder{
|
||||||
|
Symbol: s.Symbol,
|
||||||
|
Type: types.OrderTypeLimitMaker,
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Price: price,
|
||||||
|
Quantity: quantity,
|
||||||
|
Market: s.Market,
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
Tag: "grid",
|
||||||
|
})
|
||||||
|
quoteQuantity := quantity.Mul(price)
|
||||||
|
usedQuote = usedQuote.Add(quoteQuantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdOrders, err2 := s.orderExecutor.SubmitOrders(ctx, submitOrders...)
|
||||||
|
if err2 != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, order := range createdOrders {
|
||||||
|
s.logger.Infof(order.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
103
pkg/strategy/grid2/strategy_test.go
Normal file
103
pkg/strategy/grid2/strategy_test.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
//go:build !dnum
|
||||||
|
|
||||||
|
package grid2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) {
|
||||||
|
s := &Strategy{
|
||||||
|
logger: logrus.NewEntry(logrus.New()),
|
||||||
|
|
||||||
|
Market: types.Market{
|
||||||
|
BaseCurrency: "BTC",
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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.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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) {
|
||||||
|
s := &Strategy{
|
||||||
|
|
||||||
|
logger: logrus.NewEntry(logrus.New()),
|
||||||
|
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(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.InDelta(t, 4999.99989, requiredQuote.Float64(), number(0.001).Float64())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) {
|
||||||
|
s := &Strategy{
|
||||||
|
logger: logrus.NewEntry(logrus.New()),
|
||||||
|
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())
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package trendtrader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/indicator"
|
"github.com/c9s/bbgo/pkg/indicator"
|
||||||
|
@ -149,8 +150,5 @@ func line(p1, p2, p3 float64) int64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
func converge(mr, ms float64) bool {
|
func converge(mr, ms float64) bool {
|
||||||
if ms > mr {
|
return ms > mr
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,10 +150,10 @@ func (m Market) FormatPriceCurrency(val fixedpoint.Value) string {
|
||||||
|
|
||||||
func (m Market) FormatPrice(val fixedpoint.Value) string {
|
func (m Market) FormatPrice(val fixedpoint.Value) string {
|
||||||
// p := math.Pow10(m.PricePrecision)
|
// 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()))))
|
prec := int(math.Round(math.Abs(math.Log10(tickSize.Float64()))))
|
||||||
return price.FormatString(prec)
|
return price.FormatString(prec)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,12 +26,12 @@ func TestFormatQuantity(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFormatPrice(t *testing.T) {
|
func TestFormatPrice(t *testing.T) {
|
||||||
price := formatPrice(
|
price := FormatPrice(
|
||||||
s("26.288256"),
|
s("26.288256"),
|
||||||
s("0.0001"))
|
s("0.0001"))
|
||||||
assert.Equal(t, "26.2882", price)
|
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)
|
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 {
|
type args struct {
|
||||||
price fixedpoint.Value
|
price fixedpoint.Value
|
||||||
tickSize 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})?$")
|
binanceFormatRE := regexp.MustCompile("^([0-9]{1,20})(.[0-9]{1,20})?$")
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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 {
|
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)
|
assert.Regexp(t, binanceFormatRE, got)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user