mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 06:53:52 +00:00
twap: add v2 fixed quantity executor
This commit is contained in:
parent
0a83c26fd5
commit
51c1b995c2
|
@ -84,6 +84,7 @@ func NewGeneralOrderExecutor(
|
|||
executor := &GeneralOrderExecutor{
|
||||
BaseOrderExecutor: BaseOrderExecutor{
|
||||
session: session,
|
||||
exchange: session.Exchange,
|
||||
activeMakerOrders: NewActiveOrderBook(symbol),
|
||||
orderStore: orderStore,
|
||||
},
|
||||
|
|
|
@ -22,6 +22,7 @@ func NewSimpleOrderExecutor(session *ExchangeSession) *SimpleOrderExecutor {
|
|||
return &SimpleOrderExecutor{
|
||||
BaseOrderExecutor: BaseOrderExecutor{
|
||||
session: session,
|
||||
exchange: session.Exchange,
|
||||
activeMakerOrders: NewActiveOrderBook(""),
|
||||
orderStore: core.NewOrderStore(""),
|
||||
},
|
||||
|
|
|
@ -6,9 +6,10 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func generateTestOrder(side types.SideType, status types.OrderStatus, createdAt time.Time) types.Order {
|
||||
|
@ -29,7 +30,7 @@ func Test_RecoverState(t *testing.T) {
|
|||
t.Run("new strategy", func(t *testing.T) {
|
||||
currentRound := Round{}
|
||||
position := types.NewPositionFromMarket(strategy.Market)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(&bbgo.ExchangeSession{}, strategy.Symbol, ID, "", position)
|
||||
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, WaitToOpenPosition, state)
|
||||
|
@ -47,7 +48,7 @@ func Test_RecoverState(t *testing.T) {
|
|||
},
|
||||
}
|
||||
position := types.NewPositionFromMarket(strategy.Market)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(&bbgo.ExchangeSession{}, strategy.Symbol, ID, "", position)
|
||||
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, OpenPositionReady, state)
|
||||
|
@ -65,7 +66,7 @@ func Test_RecoverState(t *testing.T) {
|
|||
},
|
||||
}
|
||||
position := types.NewPositionFromMarket(strategy.Market)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(&bbgo.ExchangeSession{}, strategy.Symbol, ID, "", position)
|
||||
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, OpenPositionOrderFilled, state)
|
||||
|
@ -83,7 +84,7 @@ func Test_RecoverState(t *testing.T) {
|
|||
},
|
||||
}
|
||||
position := types.NewPositionFromMarket(strategy.Market)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(&bbgo.ExchangeSession{}, strategy.Symbol, ID, "", position)
|
||||
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, OpenPositionOrdersCancelling, state)
|
||||
|
@ -101,7 +102,7 @@ func Test_RecoverState(t *testing.T) {
|
|||
},
|
||||
}
|
||||
position := types.NewPositionFromMarket(strategy.Market)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(&bbgo.ExchangeSession{}, strategy.Symbol, ID, "", position)
|
||||
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, OpenPositionOrdersCancelling, state)
|
||||
|
@ -122,7 +123,7 @@ func Test_RecoverState(t *testing.T) {
|
|||
},
|
||||
}
|
||||
position := types.NewPositionFromMarket(strategy.Market)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(&bbgo.ExchangeSession{}, strategy.Symbol, ID, "", position)
|
||||
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, TakeProfitReady, state)
|
||||
|
@ -143,7 +144,7 @@ func Test_RecoverState(t *testing.T) {
|
|||
},
|
||||
}
|
||||
position := types.NewPositionFromMarket(strategy.Market)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
|
||||
orderExecutor := bbgo.NewGeneralOrderExecutor(&bbgo.ExchangeSession{}, strategy.Symbol, ID, "", position)
|
||||
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, WaitToOpenPosition, state)
|
||||
|
|
|
@ -48,8 +48,6 @@ type StreamExecutor struct {
|
|||
|
||||
stoppedC chan struct{}
|
||||
|
||||
state int
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
|
@ -349,6 +347,7 @@ func (e *StreamExecutor) cancelContextIfTargetQuantityFilled() bool {
|
|||
e.cancelExecution()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -399,10 +398,10 @@ func (e *StreamExecutor) Run(parentCtx context.Context) error {
|
|||
|
||||
e.orderBook = types.NewStreamBook(e.Symbol)
|
||||
e.orderBook.BindStream(e.marketDataStream)
|
||||
go e.connectMarketData(e.executionCtx)
|
||||
|
||||
e.userDataStream = e.Session.Exchange.NewStream()
|
||||
e.userDataStream.OnTradeUpdate(e.handleTradeUpdate)
|
||||
|
||||
e.position = &types.Position{
|
||||
Symbol: e.Symbol,
|
||||
BaseCurrency: e.market.BaseCurrency,
|
||||
|
@ -415,6 +414,7 @@ func (e *StreamExecutor) Run(parentCtx context.Context) error {
|
|||
e.activeMakerOrders.OnFilled(e.handleFilledOrder)
|
||||
e.activeMakerOrders.BindStream(e.userDataStream)
|
||||
|
||||
go e.connectMarketData(e.executionCtx)
|
||||
go e.connectUserData(e.userDataStreamCtx)
|
||||
go e.orderUpdater(e.executionCtx)
|
||||
return nil
|
||||
|
|
291
pkg/twap/v2/stream_executor.go
Normal file
291
pkg/twap/v2/stream_executor.go
Normal file
|
@ -0,0 +1,291 @@
|
|||
package twap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/core"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
var defaultUpdateInterval = time.Minute
|
||||
|
||||
type DoneSignal struct {
|
||||
doneC chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewDoneSignal() *DoneSignal {
|
||||
return &DoneSignal{
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DoneSignal) Emit() {
|
||||
e.mu.Lock()
|
||||
if e.doneC == nil {
|
||||
e.doneC = make(chan struct{})
|
||||
}
|
||||
|
||||
close(e.doneC)
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// Chan returns a channel that emits a signal when the execution is done.
|
||||
func (e *DoneSignal) Chan() (c <-chan struct{}) {
|
||||
// if the channel is not allocated, it means it's not started yet, we need to return a closed channel
|
||||
e.mu.Lock()
|
||||
if e.doneC == nil {
|
||||
e.doneC = make(chan struct{})
|
||||
c = e.doneC
|
||||
} else {
|
||||
c = e.doneC
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// FixedQuantityExecutor is a TWAP executor that places orders on the exchange using the exchange's stream API.
|
||||
// It uses a fixed target quantity to place orders.
|
||||
type FixedQuantityExecutor struct {
|
||||
exchange types.Exchange
|
||||
|
||||
// configuration fields
|
||||
|
||||
symbol string
|
||||
side types.SideType
|
||||
targetQuantity fixedpoint.Value
|
||||
|
||||
// updateInterval is a fixed update interval for placing new order
|
||||
updateInterval time.Duration
|
||||
|
||||
// delayInterval is the delay interval between each order placement
|
||||
delayInterval time.Duration
|
||||
|
||||
// priceLimit is the price limit for the order
|
||||
// for buy-orders, the price limit is the maximum price
|
||||
// for sell-orders, the price limit is the minimum price
|
||||
priceLimit fixedpoint.Value
|
||||
|
||||
// deadlineTime is the deadline time for the order execution
|
||||
deadlineTime *time.Time
|
||||
|
||||
executionCtx context.Context
|
||||
cancelExecution context.CancelFunc
|
||||
|
||||
userDataStreamCtx context.Context
|
||||
cancelUserDataStream context.CancelFunc
|
||||
|
||||
market types.Market
|
||||
marketDataStream types.Stream
|
||||
orderBook *types.StreamOrderBook
|
||||
|
||||
userDataStream types.Stream
|
||||
activeMakerOrders *bbgo.ActiveOrderBook
|
||||
orderStore *core.OrderStore
|
||||
position *types.Position
|
||||
|
||||
logger logrus.FieldLogger
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
done *DoneSignal
|
||||
}
|
||||
|
||||
func NewStreamExecutor(
|
||||
exchange types.Exchange,
|
||||
symbol string,
|
||||
market types.Market,
|
||||
side types.SideType,
|
||||
targetQuantity fixedpoint.Value,
|
||||
) *FixedQuantityExecutor {
|
||||
return &FixedQuantityExecutor{
|
||||
exchange: exchange,
|
||||
symbol: symbol,
|
||||
side: side,
|
||||
market: market,
|
||||
position: types.NewPositionFromMarket(market),
|
||||
targetQuantity: targetQuantity,
|
||||
updateInterval: defaultUpdateInterval,
|
||||
logger: logrus.WithFields(logrus.Fields{
|
||||
"executor": "twapStream",
|
||||
"symbol": symbol,
|
||||
}),
|
||||
|
||||
done: NewDoneSignal(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *FixedQuantityExecutor) SetDeadlineTime(t time.Time) {
|
||||
e.deadlineTime = &t
|
||||
}
|
||||
|
||||
func (e *FixedQuantityExecutor) SetDelayInterval(delayInterval time.Duration) {
|
||||
e.delayInterval = delayInterval
|
||||
}
|
||||
|
||||
func (e *FixedQuantityExecutor) SetUpdateInterval(updateInterval time.Duration) {
|
||||
e.updateInterval = updateInterval
|
||||
}
|
||||
|
||||
func (e *FixedQuantityExecutor) connectMarketData(ctx context.Context) {
|
||||
e.logger.Infof("connecting market data stream...")
|
||||
if err := e.marketDataStream.Connect(ctx); err != nil {
|
||||
e.logger.WithError(err).Errorf("market data stream connect error")
|
||||
}
|
||||
}
|
||||
|
||||
func (e *FixedQuantityExecutor) connectUserData(ctx context.Context) {
|
||||
e.logger.Infof("connecting user data stream...")
|
||||
if err := e.userDataStream.Connect(ctx); err != nil {
|
||||
e.logger.WithError(err).Errorf("user data stream connect error")
|
||||
}
|
||||
}
|
||||
|
||||
func (e *FixedQuantityExecutor) handleFilledOrder(order types.Order) {
|
||||
e.logger.Info(order.String())
|
||||
|
||||
// filled event triggers the order removal from the active order store
|
||||
// we need to ensure we received every order update event before the execution is done.
|
||||
e.cancelContextIfTargetQuantityFilled()
|
||||
}
|
||||
|
||||
func (e *FixedQuantityExecutor) cancelContextIfTargetQuantityFilled() bool {
|
||||
base := e.position.GetBase()
|
||||
|
||||
if base.Abs().Compare(e.targetQuantity) >= 0 {
|
||||
e.logger.Infof("filled target quantity, canceling the order execution context")
|
||||
e.cancelExecution()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *FixedQuantityExecutor) cancelActiveOrders(ctx context.Context) error {
|
||||
gracefulCtx, gracefulCancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer gracefulCancel()
|
||||
return e.activeMakerOrders.GracefulCancel(gracefulCtx, e.exchange)
|
||||
}
|
||||
|
||||
func (e *FixedQuantityExecutor) orderUpdater(ctx context.Context) {
|
||||
updateLimiter := rate.NewLimiter(rate.Every(3*time.Second), 1)
|
||||
_ = updateLimiter
|
||||
|
||||
defer func() {
|
||||
if err := e.cancelActiveOrders(ctx); err != nil {
|
||||
e.logger.WithError(err).Error("cancel active orders error")
|
||||
}
|
||||
|
||||
e.cancelUserDataStream()
|
||||
e.done.Emit()
|
||||
}()
|
||||
|
||||
ticker := time.NewTimer(e.updateInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-e.orderBook.C:
|
||||
if !updateLimiter.Allow() {
|
||||
break
|
||||
}
|
||||
|
||||
if e.cancelContextIfTargetQuantityFilled() {
|
||||
return
|
||||
}
|
||||
|
||||
e.logger.Infof("%s order book changed, checking order...", e.symbol)
|
||||
|
||||
/*
|
||||
if err := e.updateOrder(ctx); err != nil {
|
||||
e.logger.WithError(err).Errorf("order update failed")
|
||||
}
|
||||
*/
|
||||
|
||||
case <-ticker.C:
|
||||
if !updateLimiter.Allow() {
|
||||
break
|
||||
}
|
||||
|
||||
if e.cancelContextIfTargetQuantityFilled() {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
if err := e.updateOrder(ctx); err != nil {
|
||||
e.logger.WithError(err).Errorf("order update failed")
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *FixedQuantityExecutor) Start(ctx context.Context) error {
|
||||
if e.marketDataStream != nil {
|
||||
return errors.New("market data stream is not nil, you can't start the executor twice")
|
||||
}
|
||||
|
||||
e.executionCtx, e.cancelExecution = context.WithCancel(ctx)
|
||||
e.userDataStreamCtx, e.cancelUserDataStream = context.WithCancel(ctx)
|
||||
|
||||
e.marketDataStream = e.exchange.NewStream()
|
||||
e.marketDataStream.SetPublicOnly()
|
||||
e.marketDataStream.Subscribe(types.BookChannel, e.symbol, types.SubscribeOptions{
|
||||
Depth: types.DepthLevelMedium,
|
||||
})
|
||||
|
||||
e.orderBook = types.NewStreamBook(e.symbol)
|
||||
e.orderBook.BindStream(e.marketDataStream)
|
||||
|
||||
e.orderStore = core.NewOrderStore(e.symbol)
|
||||
e.orderStore.BindStream(e.userDataStream)
|
||||
e.activeMakerOrders = bbgo.NewActiveOrderBook(e.symbol)
|
||||
e.activeMakerOrders.OnFilled(e.handleFilledOrder)
|
||||
e.activeMakerOrders.BindStream(e.userDataStream)
|
||||
|
||||
go e.connectMarketData(e.executionCtx)
|
||||
go e.connectUserData(e.userDataStreamCtx)
|
||||
go e.orderUpdater(e.executionCtx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Done returns a channel that emits a signal when the execution is done.
|
||||
func (e *FixedQuantityExecutor) Done() <-chan struct{} {
|
||||
return e.done.Chan()
|
||||
}
|
||||
|
||||
// Shutdown stops the execution
|
||||
// If we call this method, it means the execution is still running,
|
||||
// We need it to:
|
||||
// 1. Stop the order updater (by using the execution context)
|
||||
// 2. The order updater cancels all open orders and closes the user data stream
|
||||
func (e *FixedQuantityExecutor) Shutdown(shutdownCtx context.Context) {
|
||||
e.mu.Lock()
|
||||
if e.cancelExecution != nil {
|
||||
e.cancelExecution()
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
case <-shutdownCtx.Done():
|
||||
return
|
||||
|
||||
case <-e.done.Chan():
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
}
|
9
pkg/twap/v2/stream_executor_test.go
Normal file
9
pkg/twap/v2/stream_executor_test.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package twap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewStreamExecutorV2(t *testing.T) {
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user