mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #1019 from c9s/feature/grid2
strategy: grid2: profit spread, prune historical trades . etc
This commit is contained in:
commit
4b0f03227e
9
.github/workflows/go.yml
vendored
9
.github/workflows/go.yml
vendored
|
@ -26,6 +26,9 @@ jobs:
|
|||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: 'true'
|
||||
ssh-key: ${{ secrets.git_ssh_key }}
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
|
@ -66,6 +69,12 @@ jobs:
|
|||
- name: Build
|
||||
run: go build -v ./cmd/bbgo
|
||||
|
||||
- name: Link Market Data
|
||||
run: |
|
||||
mkdir -p ~/.bbgo/cache
|
||||
ln -s $PWD/data/binance-markets.json ~/.bbgo/cache/binance-markets.json
|
||||
touch data/binance-markets.json
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
go test -race -coverprofile coverage.txt -covermode atomic ./pkg/...
|
||||
|
|
|
@ -16,8 +16,8 @@ sessions:
|
|||
# example command:
|
||||
# go run ./cmd/bbgo backtest --config config/grid2.yaml --base-asset-baseline
|
||||
backtest:
|
||||
startTime: "2022-06-01"
|
||||
endTime: "2022-06-30"
|
||||
startTime: "2021-06-01"
|
||||
endTime: "2021-06-30"
|
||||
symbols:
|
||||
- BTCUSDT
|
||||
sessions: [binance]
|
||||
|
@ -25,16 +25,16 @@ backtest:
|
|||
binance:
|
||||
balances:
|
||||
BTC: 0.0
|
||||
USDT: 10000.0
|
||||
USDT: 21_000.0
|
||||
|
||||
exchangeStrategies:
|
||||
|
||||
- on: binance
|
||||
grid2:
|
||||
symbol: BTCUSDT
|
||||
upperPrice: 15_000.0
|
||||
lowerPrice: 10_000.0
|
||||
gridNumber: 10
|
||||
upperPrice: 60_000.0
|
||||
lowerPrice: 28_000.0
|
||||
gridNumber: 1000
|
||||
|
||||
## compound is used for buying more inventory when the profit is made by the filled SELL order.
|
||||
## when compound is disabled, fixed quantity is used for each grid order.
|
||||
|
@ -45,14 +45,14 @@ exchangeStrategies:
|
|||
## meaning that earn BTC instead of USDT when trading in the BTCUSDT pair.
|
||||
# earnBase: true
|
||||
|
||||
## triggerPrice is used for opening your grid only when the last price touches your trigger price.
|
||||
## triggerPrice (optional) is used for opening your grid only when the last price touches your trigger price.
|
||||
## this is useful when you don't want to create a grid from a higher price.
|
||||
## for example, when the last price hit 17_000.0 then open a grid with the price range 13_000 to 20_000
|
||||
triggerPrice: 17_000.0
|
||||
# triggerPrice: 17_000.0
|
||||
|
||||
## triggerPrice is used for closing your grid only when the last price touches your stop loss price.
|
||||
## triggerPrice (optional) is used for closing your grid only when the last price touches your stop loss price.
|
||||
## for example, when the price drops to 17_000.0 then close the grid and sell all base inventory.
|
||||
stopLossPrice: 10_000.0
|
||||
# stopLossPrice: 10_000.0
|
||||
|
||||
## 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.
|
||||
|
@ -73,12 +73,20 @@ exchangeStrategies:
|
|||
## 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
|
||||
quoteInvestment: 20_000
|
||||
|
||||
## baseInvestment is optional
|
||||
## baseInvestment (optional) can be useful when you have existing inventory, maybe bought at much lower price
|
||||
baseInvestment: 0.0
|
||||
|
||||
## closeWhenCancelOrder (optional)
|
||||
## default to false
|
||||
closeWhenCancelOrder: true
|
||||
|
||||
## resetPositionWhenStart (optional)
|
||||
## default to false
|
||||
resetPositionWhenStart: false
|
||||
|
||||
## clearOpenOrdersWhenStart (optional)
|
||||
## default to false
|
||||
clearOpenOrdersWhenStart: false
|
||||
keepOrdersWhenShutdown: false
|
||||
|
|
1
data/binance-markets.json
Normal file
1
data/binance-markets.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -43,7 +43,7 @@ var BackTestService *service.BacktestService
|
|||
|
||||
func SetBackTesting(s *service.BacktestService) {
|
||||
BackTestService = s
|
||||
IsBackTesting = true
|
||||
IsBackTesting = s != nil
|
||||
}
|
||||
|
||||
var LoadedExchangeStrategies = make(map[string]SingleExchangeStrategy)
|
||||
|
|
|
@ -2,15 +2,22 @@ package bbgo
|
|||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
const TradeExpiryTime = 24 * time.Hour
|
||||
const PruneTriggerNumOfTrades = 10_000
|
||||
|
||||
type TradeStore struct {
|
||||
// any created trades for tracking trades
|
||||
sync.Mutex
|
||||
|
||||
trades map[uint64]types.Trade
|
||||
EnablePrune bool
|
||||
|
||||
trades map[uint64]types.Trade
|
||||
lastTradeTime time.Time
|
||||
}
|
||||
|
||||
func NewTradeStore() *TradeStore {
|
||||
|
@ -93,11 +100,54 @@ func (s *TradeStore) Add(trades ...types.Trade) {
|
|||
|
||||
for _, trade := range trades {
|
||||
s.trades[trade.ID] = trade
|
||||
s.touchLastTradeTime(trade)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TradeStore) touchLastTradeTime(trade types.Trade) {
|
||||
if trade.Time.Time().After(s.lastTradeTime) {
|
||||
s.lastTradeTime = trade.Time.Time()
|
||||
}
|
||||
}
|
||||
|
||||
// pruneExpiredTrades prunes trades that are older than the expiry time
|
||||
// see TradeExpiryTime
|
||||
func (s *TradeStore) pruneExpiredTrades(curTime time.Time) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
var trades = make(map[uint64]types.Trade)
|
||||
var cutOffTime = curTime.Add(-TradeExpiryTime)
|
||||
for _, trade := range s.trades {
|
||||
if trade.Time.Before(cutOffTime) {
|
||||
continue
|
||||
}
|
||||
|
||||
trades[trade.ID] = trade
|
||||
}
|
||||
|
||||
s.trades = trades
|
||||
}
|
||||
|
||||
func (s *TradeStore) Prune(curTime time.Time) {
|
||||
s.pruneExpiredTrades(curTime)
|
||||
}
|
||||
|
||||
func (s *TradeStore) isCoolTrade(trade types.Trade) bool {
|
||||
// if the time of last trade is over 1 hour, we call it's cool trade
|
||||
return s.lastTradeTime != (time.Time{}) && time.Time(trade.Time).Sub(s.lastTradeTime) > time.Hour
|
||||
}
|
||||
|
||||
func (s *TradeStore) BindStream(stream types.Stream) {
|
||||
stream.OnTradeUpdate(func(trade types.Trade) {
|
||||
s.Add(trade)
|
||||
})
|
||||
|
||||
if s.EnablePrune {
|
||||
stream.OnTradeUpdate(func(trade types.Trade) {
|
||||
if s.isCoolTrade(trade) {
|
||||
s.Prune(time.Time(trade.Time))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
39
pkg/bbgo/trade_store_test.go
Normal file
39
pkg/bbgo/trade_store_test.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
func TestTradeStore_isCoolTrade(t *testing.T) {
|
||||
now := time.Now()
|
||||
store := NewTradeStore()
|
||||
store.lastTradeTime = now.Add(-2 * time.Hour)
|
||||
ok := store.isCoolTrade(types.Trade{
|
||||
Time: types.Time(now),
|
||||
})
|
||||
assert.True(t, ok)
|
||||
|
||||
store.lastTradeTime = now.Add(-2 * time.Minute)
|
||||
ok = store.isCoolTrade(types.Trade{
|
||||
Time: types.Time(now),
|
||||
})
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestTradeStore_Prune(t *testing.T) {
|
||||
now := time.Now()
|
||||
store := NewTradeStore()
|
||||
store.Add(
|
||||
types.Trade{ID: 1, Time: types.Time(now.Add(-25 * time.Hour))},
|
||||
types.Trade{ID: 2, Time: types.Time(now.Add(-23 * time.Hour))},
|
||||
types.Trade{ID: 3, Time: types.Time(now.Add(-2 * time.Minute))},
|
||||
types.Trade{ID: 4, Time: types.Time(now.Add(-1 * time.Minute))},
|
||||
)
|
||||
store.Prune(now)
|
||||
assert.Equal(t, 3, len(store.trades))
|
||||
}
|
|
@ -37,6 +37,11 @@ type Strategy struct {
|
|||
Symbol string `json:"symbol"`
|
||||
|
||||
// ProfitSpread is the fixed profit spread you want to submit the sell order
|
||||
// When ProfitSpread is enabled, the grid will shift up, e.g.,
|
||||
// If you opened a grid with the price range 10_000 to 20_000
|
||||
// With profit spread set to 3_000
|
||||
// The sell orders will be placed in the range 13_000 to 23_000
|
||||
// And the buy orders will be placed in the original price range 10_000 to 20_000
|
||||
ProfitSpread fixedpoint.Value `json:"profitSpread"`
|
||||
|
||||
// GridNum is the grid number, how many orders you want to post on the orderbook.
|
||||
|
@ -302,7 +307,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) {
|
|||
|
||||
// use the profit to buy more inventory in the grid
|
||||
if s.Compound || s.EarnBase {
|
||||
newQuantity = orderQuoteQuantity.Div(newPrice)
|
||||
newQuantity = fixedpoint.Max(orderQuoteQuantity.Div(newPrice), s.Market.MinQuantity)
|
||||
}
|
||||
|
||||
// calculate profit
|
||||
|
@ -322,7 +327,7 @@ func (s *Strategy) handleOrderFilled(o types.Order) {
|
|||
}
|
||||
|
||||
if s.EarnBase {
|
||||
newQuantity = orderQuoteQuantity.Div(newPrice).Sub(baseSellQuantityReduction)
|
||||
newQuantity = fixedpoint.Max(orderQuoteQuantity.Div(newPrice).Sub(baseSellQuantityReduction), s.Market.MinQuantity)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -359,7 +364,7 @@ func (s *Strategy) checkRequiredInvestmentByQuantity(baseBalance, quoteBalance,
|
|||
requiredQuote = fixedpoint.Zero
|
||||
|
||||
// when we need to place a buy-to-sell conversion order, we need to mark the price
|
||||
si := len(pins) - 1
|
||||
si := -1
|
||||
for i := len(pins) - 1; i >= 0; i-- {
|
||||
pin := pins[i]
|
||||
price := fixedpoint.Value(pin)
|
||||
|
@ -418,7 +423,7 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am
|
|||
requiredQuote = fixedpoint.Zero
|
||||
|
||||
// when we need to place a buy-to-sell conversion order, we need to mark the price
|
||||
si := len(pins) - 1
|
||||
si := -1
|
||||
for i := len(pins) - 1; i >= 0; i-- {
|
||||
pin := pins[i]
|
||||
price := fixedpoint.Value(pin)
|
||||
|
@ -439,9 +444,10 @@ func (s *Strategy) checkRequiredInvestmentByAmount(baseBalance, quoteBalance, am
|
|||
}
|
||||
} else {
|
||||
// for orders that buy
|
||||
if i+1 == si {
|
||||
if s.ProfitSpread.IsZero() && i+1 == si {
|
||||
continue
|
||||
}
|
||||
|
||||
requiredQuote = requiredQuote.Add(amount)
|
||||
}
|
||||
}
|
||||
|
@ -478,7 +484,7 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f
|
|||
// quoteInvestment = (p1 + p2 + p3) * q
|
||||
// q = quoteInvestment / (p1 + p2 + p3)
|
||||
totalQuotePrice := fixedpoint.Zero
|
||||
si := len(pins) - 1
|
||||
si := -1
|
||||
for i := len(pins) - 1; i >= 0; i-- {
|
||||
pin := pins[i]
|
||||
price := fixedpoint.Value(pin)
|
||||
|
@ -488,16 +494,17 @@ func (s *Strategy) calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice f
|
|||
// 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
|
||||
if s.ProfitSpread.Sign() > 0 {
|
||||
totalQuotePrice = totalQuotePrice.Add(price)
|
||||
} 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))
|
||||
totalQuotePrice = totalQuotePrice.Add(nextLowerPrice)
|
||||
}
|
||||
} else {
|
||||
// for orders that buy
|
||||
if i+1 == si {
|
||||
if s.ProfitSpread.IsZero() && i+1 == si {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -520,9 +527,15 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv
|
|||
for i := len(pins) - 1; i >= 0; i-- {
|
||||
pin := pins[i]
|
||||
price := fixedpoint.Value(pin)
|
||||
if price.Compare(lastPrice) < 0 {
|
||||
sellPrice := price
|
||||
if s.ProfitSpread.Sign() > 0 {
|
||||
sellPrice = sellPrice.Add(s.ProfitSpread)
|
||||
}
|
||||
|
||||
if sellPrice.Compare(lastPrice) < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
numberOfSellOrders++
|
||||
}
|
||||
|
||||
|
@ -540,31 +553,36 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv
|
|||
s.logger.Infof("grid base investment quantity range: %f <=> %f", minBaseQuantity.Float64(), maxBaseQuantity.Float64())
|
||||
}
|
||||
|
||||
buyPlacedPrice := fixedpoint.Zero
|
||||
// calculate quantity with quote investment
|
||||
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-- {
|
||||
si := -1
|
||||
for i := len(pins) - 1 - maxNumberOfSellOrders; i >= 0; i-- {
|
||||
pin := pins[i]
|
||||
price := fixedpoint.Value(pin)
|
||||
|
||||
// buy price greater than the last price will trigger taker order.
|
||||
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
|
||||
si = i
|
||||
|
||||
// when profit spread is set, we count all the grid prices as buy prices
|
||||
if s.ProfitSpread.Sign() > 0 {
|
||||
totalQuotePrice = totalQuotePrice.Add(price)
|
||||
} else if i > 0 {
|
||||
// when profit spread is not set
|
||||
// we do not want to place sell order at i == 0
|
||||
// here we submit an order to convert a buy order into a sell order
|
||||
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 {
|
||||
if s.ProfitSpread.IsZero() && i+1 == si {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -626,7 +644,7 @@ func (s *Strategy) newStopLossPriceHandler(ctx context.Context, session *bbgo.Ex
|
|||
|
||||
func (s *Strategy) newTakeProfitHandler(ctx context.Context, session *bbgo.ExchangeSession) types.KLineCallback {
|
||||
return types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) {
|
||||
if s.TakeProfitPrice.Compare(k.High) < 0 {
|
||||
if s.TakeProfitPrice.Compare(k.High) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -786,6 +804,13 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin
|
|||
for i := len(pins) - 1; i >= 0; i-- {
|
||||
pin := pins[i]
|
||||
price := fixedpoint.Value(pin)
|
||||
sellPrice := price
|
||||
|
||||
// when profitSpread is set, the sell price is shift upper with the given spread
|
||||
if s.ProfitSpread.Sign() > 0 {
|
||||
sellPrice = sellPrice.Add(s.ProfitSpread)
|
||||
}
|
||||
|
||||
quantity := s.QuantityOrAmount.Quantity
|
||||
if quantity.IsZero() {
|
||||
quantity = s.QuantityOrAmount.Amount.Div(price)
|
||||
|
@ -799,7 +824,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin
|
|||
Symbol: s.Symbol,
|
||||
Type: types.OrderTypeLimit,
|
||||
Side: types.SideTypeSell,
|
||||
Price: price,
|
||||
Price: sellPrice,
|
||||
Quantity: quantity,
|
||||
Market: s.Market,
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
|
@ -826,7 +851,7 @@ func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoin
|
|||
// skip i == 0
|
||||
}
|
||||
} else {
|
||||
if i+1 == si {
|
||||
if s.ProfitSpread.IsZero() && i+1 == si {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -906,6 +931,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
s.groupID = util.FNV32(instanceID)
|
||||
s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID)
|
||||
|
||||
if s.ProfitSpread.Sign() > 0 {
|
||||
s.ProfitSpread = s.Market.TruncatePrice(s.ProfitSpread)
|
||||
}
|
||||
|
||||
if s.GridProfitStats == nil {
|
||||
s.GridProfitStats = newGridProfitStats(s.Market)
|
||||
}
|
||||
|
@ -923,6 +952,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
}
|
||||
|
||||
s.historicalTrades = bbgo.NewTradeStore()
|
||||
s.historicalTrades.EnablePrune = true
|
||||
s.historicalTrades.BindStream(session.UserDataStream)
|
||||
|
||||
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
|
||||
|
|
|
@ -3,12 +3,17 @@
|
|||
package grid2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/backtest"
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/exchange"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/service"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
|
@ -56,8 +61,8 @@ type PriceSideAssert struct {
|
|||
|
||||
func assertPriceSide(t *testing.T, priceSideAsserts []PriceSideAssert, orders []types.SubmitOrder) {
|
||||
for i, a := range priceSideAsserts {
|
||||
assert.Equal(t, a.Side, orders[i].Side)
|
||||
assert.Equal(t, a.Price, orders[i].Price)
|
||||
assert.Equalf(t, a.Price, orders[i].Price, "order #%d price should be %f", i+1, a.Price.Float64())
|
||||
assert.Equalf(t, a.Side, orders[i].Side, "order at price %f should be %s", a.Price.Float64(), a.Side)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,6 +154,38 @@ func TestStrategy_generateGridOrders(t *testing.T) {
|
|||
}, orders)
|
||||
})
|
||||
|
||||
t.Run("enough base + quote + profitSpread", func(t *testing.T) {
|
||||
s := newTestStrategy()
|
||||
s.ProfitSpread = number(1_000)
|
||||
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
|
||||
s.grid.CalculateArithmeticPins()
|
||||
s.QuantityOrAmount.Quantity = number(0.01)
|
||||
|
||||
lastPrice := number(15300)
|
||||
orders, err := s.generateGridOrders(number(10000.0), number(1.0), lastPrice)
|
||||
assert.NoError(t, err)
|
||||
if !assert.Equal(t, 11, len(orders)) {
|
||||
for _, o := range orders {
|
||||
t.Logf("- %s %s", o.Price.String(), o.Side)
|
||||
}
|
||||
}
|
||||
|
||||
assertPriceSide(t, []PriceSideAssert{
|
||||
{number(21000.0), types.SideTypeSell},
|
||||
{number(20000.0), types.SideTypeSell},
|
||||
{number(19000.0), types.SideTypeSell},
|
||||
{number(18000.0), types.SideTypeSell},
|
||||
{number(17000.0), types.SideTypeSell},
|
||||
|
||||
{number(15000.0), types.SideTypeBuy},
|
||||
{number(14000.0), types.SideTypeBuy},
|
||||
{number(13000.0), types.SideTypeBuy},
|
||||
{number(12000.0), types.SideTypeBuy},
|
||||
{number(11000.0), types.SideTypeBuy},
|
||||
{number(10000.0), types.SideTypeBuy},
|
||||
}, orders)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) {
|
||||
|
@ -178,20 +215,14 @@ 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",
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("calculate quote quantity from quote investment", func(t *testing.T) {
|
||||
t.Run("quote quantity", 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{
|
||||
s := newTestStrategy()
|
||||
lastPrice := number(13_500.0)
|
||||
quantity, err := s.calculateQuoteInvestmentQuantity(number(12_000.0), lastPrice, []Pin{
|
||||
Pin(number(10_000.0)), // buy
|
||||
Pin(number(11_000.0)), // buy
|
||||
Pin(number(12_000.0)), // buy
|
||||
|
@ -202,13 +233,36 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, number(0.2).String(), quantity.String())
|
||||
})
|
||||
|
||||
t.Run("profit spread", func(t *testing.T) {
|
||||
// quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000) * q
|
||||
// q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000)
|
||||
// q = 7500 / (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000)
|
||||
// q = 0.1
|
||||
s := newTestStrategy()
|
||||
s.ProfitSpread = number(2000.0)
|
||||
lastPrice := number(13_500.0)
|
||||
quantity, err := s.calculateQuoteInvestmentQuantity(number(7500.0), lastPrice, []Pin{
|
||||
Pin(number(10_000.0)), // sell order @ 12_000
|
||||
Pin(number(11_000.0)), // sell order @ 13_000
|
||||
Pin(number(12_000.0)), // sell order @ 14_000
|
||||
Pin(number(13_000.0)), // sell order @ 15_000
|
||||
Pin(number(14_000.0)), // sell order @ 16_000
|
||||
Pin(number(15_000.0)), // sell order @ 17_000
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, number(0.1).String(), quantity.String())
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func newTestStrategy() *Strategy {
|
||||
market := types.Market{
|
||||
BaseCurrency: "BTC",
|
||||
QuoteCurrency: "USDT",
|
||||
TickSize: number(0.01),
|
||||
BaseCurrency: "BTC",
|
||||
QuoteCurrency: "USDT",
|
||||
TickSize: number(0.01),
|
||||
PricePrecision: 2,
|
||||
VolumePrecision: 8,
|
||||
}
|
||||
|
||||
s := &Strategy{
|
||||
|
@ -218,6 +272,8 @@ func newTestStrategy() *Strategy {
|
|||
UpperPrice: number(20_000),
|
||||
LowerPrice: number(10_000),
|
||||
GridNum: 10,
|
||||
|
||||
// QuoteInvestment: number(9000.0),
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
@ -275,3 +331,122 @@ func TestStrategy_calculateProfit(t *testing.T) {
|
|||
assert.InDelta(t, sellQuantity.Float64()-buyOrder.Quantity.Float64(), profit.Profit.Float64(), 0.001)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBacktestStrategy(t *testing.T) {
|
||||
market := types.Market{
|
||||
BaseCurrency: "BTC",
|
||||
QuoteCurrency: "USDT",
|
||||
TickSize: number(0.01),
|
||||
PricePrecision: 2,
|
||||
VolumePrecision: 8,
|
||||
}
|
||||
strategy := &Strategy{
|
||||
logger: logrus.NewEntry(logrus.New()),
|
||||
Symbol: "BTCUSDT",
|
||||
Market: market,
|
||||
GridProfitStats: newGridProfitStats(market),
|
||||
UpperPrice: number(60_000),
|
||||
LowerPrice: number(28_000),
|
||||
GridNum: 100,
|
||||
QuoteInvestment: number(9000.0),
|
||||
}
|
||||
|
||||
// TEMPLATE {{{ start backtest
|
||||
startTime, err := types.ParseLooseFormatTime("2021-06-01")
|
||||
assert.NoError(t, err)
|
||||
|
||||
endTime, err := types.ParseLooseFormatTime("2021-06-30")
|
||||
assert.NoError(t, err)
|
||||
|
||||
backtestConfig := &bbgo.Backtest{
|
||||
StartTime: startTime,
|
||||
EndTime: &endTime,
|
||||
RecordTrades: false,
|
||||
FeeMode: bbgo.BacktestFeeModeToken,
|
||||
Accounts: map[string]bbgo.BacktestAccount{
|
||||
"binance": {
|
||||
MakerFeeRate: number(0.075 * 0.01),
|
||||
TakerFeeRate: number(0.075 * 0.01),
|
||||
Balances: bbgo.BacktestAccountBalanceMap{
|
||||
"USDT": number(10_000.0),
|
||||
"BTC": number(1.0),
|
||||
},
|
||||
},
|
||||
},
|
||||
Symbols: []string{"BTCUSDT"},
|
||||
Sessions: []string{"binance"},
|
||||
SyncSecKLines: false,
|
||||
}
|
||||
|
||||
t.Logf("backtestConfig: %+v", backtestConfig)
|
||||
|
||||
ctx := context.Background()
|
||||
environ := bbgo.NewEnvironment()
|
||||
environ.SetStartTime(startTime.Time())
|
||||
|
||||
err = environ.ConfigureDatabaseDriver(ctx, "sqlite3", "../../../data/bbgo_test.sqlite3")
|
||||
assert.NoError(t, err)
|
||||
|
||||
backtestService := &service.BacktestService{DB: environ.DatabaseService.DB}
|
||||
defer func() {
|
||||
err := environ.DatabaseService.DB.Close()
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
environ.BacktestService = backtestService
|
||||
bbgo.SetBackTesting(backtestService)
|
||||
defer bbgo.SetBackTesting(nil)
|
||||
|
||||
exName, err := types.ValidExchangeName("binance")
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
publicExchange, err := exchange.NewPublic(exName)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
backtestExchange, err := backtest.NewExchange(publicExchange.Name(), publicExchange, backtestService, backtestConfig)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
session := environ.AddExchange(exName.String(), backtestExchange)
|
||||
assert.NotNil(t, session)
|
||||
|
||||
err = environ.Init(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, ses := range environ.Sessions() {
|
||||
userDataStream := ses.UserDataStream.(types.StandardStreamEmitter)
|
||||
backtestEx := ses.Exchange.(*backtest.Exchange)
|
||||
backtestEx.MarketDataStream = ses.MarketDataStream.(types.StandardStreamEmitter)
|
||||
backtestEx.BindUserData(userDataStream)
|
||||
}
|
||||
|
||||
trader := bbgo.NewTrader(environ)
|
||||
if assert.NotNil(t, trader) {
|
||||
trader.DisableLogging()
|
||||
}
|
||||
|
||||
// TODO: add grid2 to the user config and run backtest
|
||||
userConfig := &bbgo.Config{
|
||||
ExchangeStrategies: []bbgo.ExchangeStrategyMount{
|
||||
{
|
||||
Mounts: []string{"binance"},
|
||||
Strategy: strategy,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = trader.Configure(userConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = trader.Run(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// TODO: feed data
|
||||
|
||||
// }}}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user