grid2: add profitFixer and tests

This commit is contained in:
c9s 2023-04-26 16:14:53 +08:00
parent d0ea7d8b47
commit e8a761e331
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
6 changed files with 265 additions and 0 deletions

View File

@ -0,0 +1,78 @@
package grid2
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/exchange/batch"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
type ProfitFixer struct {
symbol string
grid *Grid
historyService types.ExchangeTradeHistoryService
}
func newProfitFixer(grid *Grid, symbol string, historyService types.ExchangeTradeHistoryService) *ProfitFixer {
return &ProfitFixer{
symbol: symbol,
grid: grid,
historyService: historyService,
}
}
// Fix fixes the total quote profit of the given grid
func (f *ProfitFixer) Fix(ctx context.Context, since, until time.Time, initialOrderID uint64, profitStats *GridProfitStats) error {
profitStats.TotalQuoteProfit = fixedpoint.Zero
profitStats.ArbitrageCount = 0
q := &batch.ClosedOrderBatchQuery{ExchangeTradeHistoryService: f.historyService}
orderC, errC := q.Query(ctx, f.symbol, since, until, initialOrderID)
for {
select {
case <-ctx.Done():
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return ctx.Err()
case order, ok := <-orderC:
if !ok {
return <-errC
}
if !f.grid.HasPrice(order.Price) {
continue
}
if profitStats.InitialOrderID == 0 {
profitStats.InitialOrderID = order.OrderID
}
if order.Status != types.OrderStatusFilled {
continue
}
if order.Type != types.OrderTypeLimit {
continue
}
if order.Side != types.SideTypeSell {
continue
}
quoteProfit := order.Quantity.Mul(f.grid.Spread)
profitStats.TotalQuoteProfit = profitStats.TotalQuoteProfit.Add(quoteProfit)
profitStats.ArbitrageCount++
log.Debugf("profitFixer: filledSellOrder=%#v", order)
log.Debugf("profitFixer: profitStats=%#v", profitStats)
}
}
}

View File

@ -0,0 +1,102 @@
package grid2
import (
"context"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/types/mocks"
)
func mustNewTime(v string) time.Time {
t, err := time.Parse(time.RFC3339, v)
if err != nil {
panic(err)
}
return t
}
var testClosedOrderID = uint64(0)
func newClosedLimitOrder(symbol string, side types.SideType, price, quantity fixedpoint.Value, ta ...time.Time) types.Order {
testClosedOrderID++
creationTime := time.Now()
updateTime := creationTime
if len(ta) > 0 {
creationTime = ta[0]
if len(ta) > 1 {
updateTime = ta[1]
}
}
return types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: symbol,
Side: side,
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
},
Exchange: types.ExchangeBinance,
OrderID: testClosedOrderID,
Status: types.OrderStatusFilled,
ExecutedQuantity: quantity,
CreationTime: types.Time(creationTime),
UpdateTime: types.Time(updateTime),
}
}
func TestProfitFixer(t *testing.T) {
testOrderID = 0
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ctx := context.Background()
mockHistoryService := mocks.NewMockExchangeTradeHistoryService(mockCtrl)
mockHistoryService.EXPECT().QueryClosedOrders(ctx, "ETHUSDT", mustNewTime("2022-01-01T00:00:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(0)).
Return([]types.Order{
newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1800.0), number(0.1), mustNewTime("2022-01-01T00:01:00Z")),
newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1700.0), number(0.1), mustNewTime("2022-01-01T00:01:00Z")),
newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1800.0), number(0.1), mustNewTime("2022-01-01T00:01:00Z")),
newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1900.0), number(0.1), mustNewTime("2022-01-01T00:03:00Z")),
newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1905.0), number(0.1), mustNewTime("2022-01-01T00:03:00Z")),
}, nil)
mockHistoryService.EXPECT().QueryClosedOrders(ctx, "ETHUSDT", mustNewTime("2022-01-01T00:03:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(5)).
Return([]types.Order{
newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1900.0), number(0.1), mustNewTime("2022-01-01T00:04:00Z")),
newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1800.0), number(0.1), mustNewTime("2022-01-01T00:04:00Z")),
newClosedLimitOrder("ETHUSDT", types.SideTypeBuy, number(1700.0), number(0.1), mustNewTime("2022-01-01T00:04:00Z")),
newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1800.0), number(0.1), mustNewTime("2022-01-01T00:04:00Z")),
newClosedLimitOrder("ETHUSDT", types.SideTypeSell, number(1900.0), number(0.1), mustNewTime("2022-01-01T00:08:00Z")),
}, nil)
mockHistoryService.EXPECT().QueryClosedOrders(ctx, "ETHUSDT", mustNewTime("2022-01-01T00:08:00Z"), mustNewTime("2022-01-07T00:00:00Z"), uint64(10)).
Return([]types.Order{}, nil)
grid := NewGrid(number(1000.0), number(2000.0), number(11), number(0.01))
grid.CalculateArithmeticPins()
since, err := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z")
assert.NoError(t, err)
until, err := time.Parse(time.RFC3339, "2022-01-07T00:00:00Z")
assert.NoError(t, err)
stats := &GridProfitStats{}
fixer := newProfitFixer(grid, "ETHUSDT", mockHistoryService)
err = fixer.Fix(ctx, since, until, 0, stats)
assert.NoError(t, err)
assert.Equal(t, "40", stats.TotalQuoteProfit.String())
assert.Equal(t, 4, stats.ArbitrageCount)
}

View File

@ -70,6 +70,20 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex
// emit ready after recover
s.EmitGridReady()
defer bbgo.Sync(ctx, s)
if s.EnableProfitFixer {
until := time.Now()
since := until.Add(7 * 24 * time.Hour)
if s.FixProfitSince != nil {
since = s.FixProfitSince.Time()
}
fixer := newProfitFixer(s.grid, s.Symbol, historyService)
// set initial order ID = 0 instead of s.GridProfitStats.InitialOrderID because the order ID could be incorrect
return fixer.Fix(ctx, since, until, 0, s.GridProfitStats)
}
return nil
}

View File

@ -165,6 +165,9 @@ type Strategy struct {
SkipSpreadCheck bool `json:"skipSpreadCheck"`
RecoverGridByScanningTrades bool `json:"recoverGridByScanningTrades"`
EnableProfitFixer bool `json:"enableProfitFixer"`
FixProfitSince *types.Time `json:"fixProfitSince"`
// Debug enables the debug mode
Debug bool `json:"debug"`

View File

@ -108,6 +108,7 @@ type ExchangeAmountFeeProtect interface {
SetModifyOrderAmountForFee(ExchangeFee)
}
//go:generate mockgen -destination=mocks/mock_exchange_trade_history.go -package=mocks . ExchangeTradeHistoryService
type ExchangeTradeHistoryService interface {
QueryTrades(ctx context.Context, symbol string, options *TradeQueryOptions) ([]Trade, error)
QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []Order, err error)

View File

@ -0,0 +1,67 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/c9s/bbgo/pkg/types (interfaces: ExchangeTradeHistoryService)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
time "time"
types "github.com/c9s/bbgo/pkg/types"
gomock "github.com/golang/mock/gomock"
)
// MockExchangeTradeHistoryService is a mock of ExchangeTradeHistoryService interface.
type MockExchangeTradeHistoryService struct {
ctrl *gomock.Controller
recorder *MockExchangeTradeHistoryServiceMockRecorder
}
// MockExchangeTradeHistoryServiceMockRecorder is the mock recorder for MockExchangeTradeHistoryService.
type MockExchangeTradeHistoryServiceMockRecorder struct {
mock *MockExchangeTradeHistoryService
}
// NewMockExchangeTradeHistoryService creates a new mock instance.
func NewMockExchangeTradeHistoryService(ctrl *gomock.Controller) *MockExchangeTradeHistoryService {
mock := &MockExchangeTradeHistoryService{ctrl: ctrl}
mock.recorder = &MockExchangeTradeHistoryServiceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockExchangeTradeHistoryService) EXPECT() *MockExchangeTradeHistoryServiceMockRecorder {
return m.recorder
}
// QueryClosedOrders mocks base method.
func (m *MockExchangeTradeHistoryService) QueryClosedOrders(arg0 context.Context, arg1 string, arg2, arg3 time.Time, arg4 uint64) ([]types.Order, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryClosedOrders", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].([]types.Order)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryClosedOrders indicates an expected call of QueryClosedOrders.
func (mr *MockExchangeTradeHistoryServiceMockRecorder) QueryClosedOrders(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryClosedOrders", reflect.TypeOf((*MockExchangeTradeHistoryService)(nil).QueryClosedOrders), arg0, arg1, arg2, arg3, arg4)
}
// QueryTrades mocks base method.
func (m *MockExchangeTradeHistoryService) QueryTrades(arg0 context.Context, arg1 string, arg2 *types.TradeQueryOptions) ([]types.Trade, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryTrades", arg0, arg1, arg2)
ret0, _ := ret[0].([]types.Trade)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryTrades indicates an expected call of QueryTrades.
func (mr *MockExchangeTradeHistoryServiceMockRecorder) QueryTrades(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTrades", reflect.TypeOf((*MockExchangeTradeHistoryService)(nil).QueryTrades), arg0, arg1, arg2)
}