mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +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{
|
executor := &GeneralOrderExecutor{
|
||||||
BaseOrderExecutor: BaseOrderExecutor{
|
BaseOrderExecutor: BaseOrderExecutor{
|
||||||
session: session,
|
session: session,
|
||||||
|
exchange: session.Exchange,
|
||||||
activeMakerOrders: NewActiveOrderBook(symbol),
|
activeMakerOrders: NewActiveOrderBook(symbol),
|
||||||
orderStore: orderStore,
|
orderStore: orderStore,
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,6 +22,7 @@ func NewSimpleOrderExecutor(session *ExchangeSession) *SimpleOrderExecutor {
|
||||||
return &SimpleOrderExecutor{
|
return &SimpleOrderExecutor{
|
||||||
BaseOrderExecutor: BaseOrderExecutor{
|
BaseOrderExecutor: BaseOrderExecutor{
|
||||||
session: session,
|
session: session,
|
||||||
|
exchange: session.Exchange,
|
||||||
activeMakerOrders: NewActiveOrderBook(""),
|
activeMakerOrders: NewActiveOrderBook(""),
|
||||||
orderStore: core.NewOrderStore(""),
|
orderStore: core.NewOrderStore(""),
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,9 +6,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateTestOrder(side types.SideType, status types.OrderStatus, createdAt time.Time) types.Order {
|
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) {
|
t.Run("new strategy", func(t *testing.T) {
|
||||||
currentRound := Round{}
|
currentRound := Round{}
|
||||||
position := types.NewPositionFromMarket(strategy.Market)
|
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)
|
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, WaitToOpenPosition, state)
|
assert.Equal(t, WaitToOpenPosition, state)
|
||||||
|
@ -47,7 +48,7 @@ func Test_RecoverState(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
position := types.NewPositionFromMarket(strategy.Market)
|
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)
|
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, OpenPositionReady, state)
|
assert.Equal(t, OpenPositionReady, state)
|
||||||
|
@ -65,7 +66,7 @@ func Test_RecoverState(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
position := types.NewPositionFromMarket(strategy.Market)
|
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)
|
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, OpenPositionOrderFilled, state)
|
assert.Equal(t, OpenPositionOrderFilled, state)
|
||||||
|
@ -83,7 +84,7 @@ func Test_RecoverState(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
position := types.NewPositionFromMarket(strategy.Market)
|
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)
|
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, OpenPositionOrdersCancelling, state)
|
assert.Equal(t, OpenPositionOrdersCancelling, state)
|
||||||
|
@ -101,7 +102,7 @@ func Test_RecoverState(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
position := types.NewPositionFromMarket(strategy.Market)
|
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)
|
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, OpenPositionOrdersCancelling, state)
|
assert.Equal(t, OpenPositionOrdersCancelling, state)
|
||||||
|
@ -122,7 +123,7 @@ func Test_RecoverState(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
position := types.NewPositionFromMarket(strategy.Market)
|
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)
|
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, TakeProfitReady, state)
|
assert.Equal(t, TakeProfitReady, state)
|
||||||
|
@ -143,7 +144,7 @@ func Test_RecoverState(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
position := types.NewPositionFromMarket(strategy.Market)
|
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)
|
state, err := recoverState(context.Background(), 5, currentRound, orderExecutor)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, WaitToOpenPosition, state)
|
assert.Equal(t, WaitToOpenPosition, state)
|
||||||
|
|
|
@ -48,8 +48,6 @@ type StreamExecutor struct {
|
||||||
|
|
||||||
stoppedC chan struct{}
|
stoppedC chan struct{}
|
||||||
|
|
||||||
state int
|
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,6 +347,7 @@ func (e *StreamExecutor) cancelContextIfTargetQuantityFilled() bool {
|
||||||
e.cancelExecution()
|
e.cancelExecution()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,10 +398,10 @@ func (e *StreamExecutor) Run(parentCtx context.Context) error {
|
||||||
|
|
||||||
e.orderBook = types.NewStreamBook(e.Symbol)
|
e.orderBook = types.NewStreamBook(e.Symbol)
|
||||||
e.orderBook.BindStream(e.marketDataStream)
|
e.orderBook.BindStream(e.marketDataStream)
|
||||||
go e.connectMarketData(e.executionCtx)
|
|
||||||
|
|
||||||
e.userDataStream = e.Session.Exchange.NewStream()
|
e.userDataStream = e.Session.Exchange.NewStream()
|
||||||
e.userDataStream.OnTradeUpdate(e.handleTradeUpdate)
|
e.userDataStream.OnTradeUpdate(e.handleTradeUpdate)
|
||||||
|
|
||||||
e.position = &types.Position{
|
e.position = &types.Position{
|
||||||
Symbol: e.Symbol,
|
Symbol: e.Symbol,
|
||||||
BaseCurrency: e.market.BaseCurrency,
|
BaseCurrency: e.market.BaseCurrency,
|
||||||
|
@ -415,6 +414,7 @@ func (e *StreamExecutor) Run(parentCtx context.Context) error {
|
||||||
e.activeMakerOrders.OnFilled(e.handleFilledOrder)
|
e.activeMakerOrders.OnFilled(e.handleFilledOrder)
|
||||||
e.activeMakerOrders.BindStream(e.userDataStream)
|
e.activeMakerOrders.BindStream(e.userDataStream)
|
||||||
|
|
||||||
|
go e.connectMarketData(e.executionCtx)
|
||||||
go e.connectUserData(e.userDataStreamCtx)
|
go e.connectUserData(e.userDataStreamCtx)
|
||||||
go e.orderUpdater(e.executionCtx)
|
go e.orderUpdater(e.executionCtx)
|
||||||
return nil
|
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