Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REFACTOR: [xgap] refactor with common strategy #1480

Merged
merged 1 commit into from
Jan 6, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 151 additions & 133 deletions pkg/strategy/xgap/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/strategy/common"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
)
Expand All @@ -32,6 +33,10 @@ func (s *Strategy) ID() string {
return ID
}

func (s *Strategy) InstanceID() string {
return fmt.Sprintf("%s:%s", ID, s.Symbol)
}

type State struct {
AccumulatedFeeStartedAt time.Time `json:"accumulatedFeeStartedAt,omitempty"`
AccumulatedFees map[string]fixedpoint.Value `json:"accumulatedFees,omitempty"`
Expand All @@ -54,6 +59,10 @@ func (s *State) Reset() {
}

type Strategy struct {
*common.Strategy

Environment *bbgo.Environment

Symbol string `json:"symbol"`
SourceExchange string `json:"sourceExchange"`
TradingExchange string `json:"tradingExchange"`
Expand All @@ -73,13 +82,28 @@ type Strategy struct {
mu sync.Mutex
lastSourceKLine, lastTradingKLine types.KLine
sourceBook, tradingBook *types.StreamOrderBook
groupID uint32

activeOrderBook *bbgo.ActiveOrderBook

stopC chan struct{}
}

func (s *Strategy) Initialize() error {
if s.Strategy == nil {
s.Strategy = &common.Strategy{}
}
return nil
}

func (s *Strategy) Validate() error {
return nil
}

func (s *Strategy) Defaults() error {
if s.UpdateInterval == 0 {
s.UpdateInterval = types.Duration(time.Second)
}
return nil
}

func (s *Strategy) isBudgetAllowed() bool {
if s.DailyFeeBudgets == nil {
return true
Expand Down Expand Up @@ -141,10 +165,6 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {
}

func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error {
if s.UpdateInterval == 0 {
s.UpdateInterval = types.Duration(time.Second)
}

sourceSession, ok := sessions[s.SourceExchange]
if !ok {
return fmt.Errorf("source session %s is not defined", s.SourceExchange)
Expand All @@ -167,6 +187,8 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
return fmt.Errorf("trading session market %s is not defined", s.Symbol)
}

s.Strategy.Initialize(ctx, s.Environment, tradingSession, s.tradingMarket, ID, s.InstanceID())

s.stopC = make(chan struct{})

if s.State == nil {
Expand Down Expand Up @@ -209,13 +231,6 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se

s.tradingSession.UserDataStream.OnTradeUpdate(s.handleTradeUpdate)

instanceID := fmt.Sprintf("%s-%s", ID, s.Symbol)
s.groupID = util.FNV32(instanceID) % math.MaxInt32
log.Infof("using group id %d from fnv32(%s)", s.groupID, instanceID)

s.activeOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
s.activeOrderBook.BindStream(s.tradingSession.UserDataStream)

go func() {
ticker := time.NewTicker(
util.MillisecondsJitter(s.UpdateInterval.Duration(), 1000),
Expand All @@ -241,133 +256,136 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
time.Sleep(delay)
}

bestBid, hasBid := s.tradingBook.BestBid()
bestAsk, hasAsk := s.tradingBook.BestAsk()

// try to use the bid/ask price from the trading book
if hasBid && hasAsk {
var spread = bestAsk.Price.Sub(bestBid.Price)
var spreadPercentage = spread.Div(bestAsk.Price)
log.Infof("trading book spread=%s %s",
spread.String(), spreadPercentage.Percentage())

// use the source book price if the spread percentage greater than 10%
if spreadPercentage.Compare(StepPercentageGap) > 0 {
log.Warnf("spread too large (%s %s), using source book",
spread.String(), spreadPercentage.Percentage())
bestBid, hasBid = s.sourceBook.BestBid()
bestAsk, hasAsk = s.sourceBook.BestAsk()
}

if s.MinSpread.Sign() > 0 {
if spread.Compare(s.MinSpread) < 0 {
log.Warnf("spread < min spread, spread=%s minSpread=%s bid=%s ask=%s",
spread.String(), s.MinSpread.String(),
bestBid.Price.String(), bestAsk.Price.String())
continue
}
}

// if the spread is less than 100 ticks (100 pips), skip
if spread.Compare(s.tradingMarket.TickSize.MulExp(2)) < 0 {
log.Warnf("spread too small, we can't place orders: spread=%v bid=%v ask=%v",
spread, bestBid.Price, bestAsk.Price)
continue
}

} else {
bestBid, hasBid = s.sourceBook.BestBid()
bestAsk, hasAsk = s.sourceBook.BestAsk()
}
s.placeOrders(ctx)

if !hasBid || !hasAsk {
log.Warn("no bids or asks on the source book or the trading book")
continue
}
s.cancelOrders(ctx)
}
}
}()

var spread = bestAsk.Price.Sub(bestBid.Price)
var spreadPercentage = spread.Div(bestAsk.Price)
log.Infof("spread=%v %s ask=%v bid=%v",
spread, spreadPercentage.Percentage(),
bestAsk.Price, bestBid.Price)
// var spreadPercentage = spread.Float64() / bestBid.Price.Float64()

var midPrice = bestAsk.Price.Add(bestBid.Price).Div(Two)
var price = midPrice

log.Infof("mid price %v", midPrice)

var balances = s.tradingSession.GetAccount().Balances()
var quantity = s.tradingMarket.MinQuantity

if s.Quantity.Sign() > 0 {
quantity = fixedpoint.Min(s.Quantity, s.tradingMarket.MinQuantity)
} else if s.SimulateVolume {
s.mu.Lock()
if s.lastTradingKLine.Volume.Sign() > 0 && s.lastSourceKLine.Volume.Sign() > 0 {
volumeDiff := s.lastSourceKLine.Volume.Sub(s.lastTradingKLine.Volume)
// change the current quantity only diff is positive
if volumeDiff.Sign() > 0 {
quantity = volumeDiff
}

if baseBalance, ok := balances[s.tradingMarket.BaseCurrency]; ok {
quantity = fixedpoint.Min(quantity, baseBalance.Available)
}

if quoteBalance, ok := balances[s.tradingMarket.QuoteCurrency]; ok {
maxQuantity := quoteBalance.Available.Div(price)
quantity = fixedpoint.Min(quantity, maxQuantity)
}
}
s.mu.Unlock()
} else {
// plus a 2% quantity jitter
jitter := 1.0 + math.Max(0.02, rand.Float64())
quantity = quantity.Mul(fixedpoint.NewFromFloat(jitter))
}
return nil
}

var quoteAmount = price.Mul(quantity)
if quoteAmount.Compare(s.tradingMarket.MinNotional) <= 0 {
quantity = fixedpoint.Max(
s.tradingMarket.MinQuantity,
s.tradingMarket.MinNotional.Mul(NotionModifier).Div(price))
}
func (s *Strategy) placeOrders(ctx context.Context) {
bestBid, hasBid := s.tradingBook.BestBid()
bestAsk, hasAsk := s.tradingBook.BestAsk()

// try to use the bid/ask price from the trading book
if hasBid && hasAsk {
var spread = bestAsk.Price.Sub(bestBid.Price)
var spreadPercentage = spread.Div(bestAsk.Price)
log.Infof("trading book spread=%s %s",
spread.String(), spreadPercentage.Percentage())

// use the source book price if the spread percentage greater than 10%
if spreadPercentage.Compare(StepPercentageGap) > 0 {
log.Warnf("spread too large (%s %s), using source book",
spread.String(), spreadPercentage.Percentage())
bestBid, hasBid = s.sourceBook.BestBid()
bestAsk, hasAsk = s.sourceBook.BestAsk()
}

createdOrders, _, err := bbgo.BatchPlaceOrder(ctx, tradingSession.Exchange, nil, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
Market: s.tradingMarket,
// TimeInForce: types.TimeInForceGTC,
GroupID: s.groupID,
}, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
Market: s.tradingMarket,
// TimeInForce: types.TimeInForceGTC,
GroupID: s.groupID,
})

if err != nil {
log.WithError(err).Error("order submit error")
}
if s.MinSpread.Sign() > 0 {
if spread.Compare(s.MinSpread) < 0 {
log.Warnf("spread < min spread, spread=%s minSpread=%s bid=%s ask=%s",
spread.String(), s.MinSpread.String(),
bestBid.Price.String(), bestAsk.Price.String())
return
}
}

s.activeOrderBook.Add(createdOrders...)
// if the spread is less than 100 ticks (100 pips), skip
if spread.Compare(s.tradingMarket.TickSize.MulExp(2)) < 0 {
log.Warnf("spread too small, we can't place orders: spread=%v bid=%v ask=%v",
spread, bestBid.Price, bestAsk.Price)
return
}

time.Sleep(time.Second)
} else {
bestBid, hasBid = s.sourceBook.BestBid()
bestAsk, hasAsk = s.sourceBook.BestAsk()
}

if err := s.activeOrderBook.GracefulCancel(ctx, s.tradingSession.Exchange); err != nil {
log.WithError(err).Error("cancel order error")
}
if !hasBid || !hasAsk {
log.Warn("no bids or asks on the source book or the trading book")
return
}

var spread = bestAsk.Price.Sub(bestBid.Price)
var spreadPercentage = spread.Div(bestAsk.Price)
log.Infof("spread=%v %s ask=%v bid=%v",
spread, spreadPercentage.Percentage(),
bestAsk.Price, bestBid.Price)
// var spreadPercentage = spread.Float64() / bestBid.Price.Float64()

var midPrice = bestAsk.Price.Add(bestBid.Price).Div(Two)
var price = midPrice

log.Infof("mid price %v", midPrice)

var balances = s.tradingSession.GetAccount().Balances()
var quantity = s.tradingMarket.MinQuantity

if s.Quantity.Sign() > 0 {
quantity = fixedpoint.Min(s.Quantity, s.tradingMarket.MinQuantity)
} else if s.SimulateVolume {
s.mu.Lock()
if s.lastTradingKLine.Volume.Sign() > 0 && s.lastSourceKLine.Volume.Sign() > 0 {
volumeDiff := s.lastSourceKLine.Volume.Sub(s.lastTradingKLine.Volume)
// change the current quantity only diff is positive
if volumeDiff.Sign() > 0 {
quantity = volumeDiff
}

if baseBalance, ok := balances[s.tradingMarket.BaseCurrency]; ok {
quantity = fixedpoint.Min(quantity, baseBalance.Available)
}

if quoteBalance, ok := balances[s.tradingMarket.QuoteCurrency]; ok {
maxQuantity := quoteBalance.Available.Div(price)
quantity = fixedpoint.Min(quantity, maxQuantity)
}
}
}()
s.mu.Unlock()
} else {
// plus a 2% quantity jitter
jitter := 1.0 + math.Max(0.02, rand.Float64())
quantity = quantity.Mul(fixedpoint.NewFromFloat(jitter))
}

return nil
var quoteAmount = price.Mul(quantity)
if quoteAmount.Compare(s.tradingMarket.MinNotional) <= 0 {
quantity = fixedpoint.Max(
s.tradingMarket.MinQuantity,
s.tradingMarket.MinNotional.Mul(NotionModifier).Div(price))
}

orderForm := []types.SubmitOrder{{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
Market: s.tradingMarket,
}, {
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
Market: s.tradingMarket,
}}
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orderForm...)
if err != nil {
log.WithError(err).Error("order submit error")
}
log.Infof("created orders: %+v", createdOrders)

time.Sleep(time.Second)
}

func (s *Strategy) cancelOrders(ctx context.Context) {
if err := s.OrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Error("cancel order error")
}
}
Loading