qbtrade/pkg/strategy/wall/strategy.go

353 lines
8.4 KiB
Go
Raw Normal View History

2024-06-27 14:42:38 +00:00
package wall
import (
"context"
"fmt"
"sync"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/common"
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
"git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade"
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
)
const ID = "wall"
const stateKey = "state-v1"
var defaultFeeRate = fixedpoint.NewFromFloat(0.001)
var two = fixedpoint.NewFromInt(2)
var log = logrus.WithField("strategy", ID)
func init() {
qbtrade.RegisterStrategy(ID, &Strategy{})
}
type Strategy struct {
*common.Strategy
Environment *qbtrade.Environment
Market types.Market
// Symbol is the market symbol you want to trade
Symbol string `json:"symbol"`
Side types.SideType `json:"side"`
// Interval is how long do you want to update your order price and quantity
Interval types.Interval `json:"interval"`
FixedPrice fixedpoint.Value `json:"fixedPrice"`
qbtrade.QuantityOrAmount
NumLayers int `json:"numLayers"`
// LayerSpread is the price spread between each layer
LayerSpread fixedpoint.Value `json:"layerSpread"`
// QuantityScale helps user to define the quantity by layer scale
QuantityScale *qbtrade.LayerScale `json:"quantityScale,omitempty"`
AdjustmentMinSpread fixedpoint.Value `json:"adjustmentMinSpread"`
AdjustmentQuantity fixedpoint.Value `json:"adjustmentQuantity"`
session *qbtrade.ExchangeSession
activeAdjustmentOrders *qbtrade.ActiveOrderBook
activeWallOrders *qbtrade.ActiveOrderBook
}
func (s *Strategy) Initialize() error {
if s.Strategy == nil {
s.Strategy = &common.Strategy{}
}
return nil
}
func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) InstanceID() string {
return fmt.Sprintf("%s:%s", ID, s.Symbol)
}
func (s *Strategy) Subscribe(session *qbtrade.ExchangeSession) {
session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{
Depth: types.DepthLevelFull,
})
}
func (s *Strategy) Validate() error {
if len(s.Symbol) == 0 {
return errors.New("symbol is required")
}
if len(s.Side) == 0 {
return errors.New("side is required")
}
if s.FixedPrice.IsZero() {
return errors.New("fixedPrice can not be zero")
}
return nil
}
func (s *Strategy) CurrentPosition() *types.Position {
return s.Position
}
func (s *Strategy) placeAdjustmentOrders(ctx context.Context, orderExecutor qbtrade.OrderExecutor) error {
var submitOrders []types.SubmitOrder
// position adjustment orders
base := s.Position.GetBase()
if base.IsZero() {
return nil
}
ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol)
if err != nil {
return err
}
if s.Market.IsDustQuantity(base, ticker.Last) {
return nil
}
switch s.Side {
case types.SideTypeBuy:
askPrice := ticker.Sell.Mul(s.AdjustmentMinSpread.Add(fixedpoint.One))
if s.Position.AverageCost.Compare(askPrice) <= 0 {
return nil
}
if base.Sign() < 0 {
return nil
}
quantity := base.Abs()
if quantity.Compare(s.AdjustmentQuantity) >= 0 {
quantity = s.AdjustmentQuantity
}
submitOrders = append(submitOrders, types.SubmitOrder{
Symbol: s.Symbol,
Side: s.Side.Reverse(),
Type: types.OrderTypeLimitMaker,
Price: askPrice,
Quantity: quantity,
Market: s.Market,
})
case types.SideTypeSell:
bidPrice := ticker.Sell.Mul(fixedpoint.One.Sub(s.AdjustmentMinSpread))
if s.Position.AverageCost.Compare(bidPrice) >= 0 {
return nil
}
if base.Sign() > 0 {
return nil
}
quantity := base.Abs()
if quantity.Compare(s.AdjustmentQuantity) >= 0 {
quantity = s.AdjustmentQuantity
}
submitOrders = append(submitOrders, types.SubmitOrder{
Symbol: s.Symbol,
Side: s.Side.Reverse(),
Type: types.OrderTypeLimitMaker,
Price: bidPrice,
Quantity: quantity,
Market: s.Market,
})
}
// condition for lower the average cost
if len(submitOrders) == 0 {
return nil
}
createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrders...)
if err != nil {
return err
}
s.activeAdjustmentOrders.Add(createdOrders...)
return nil
}
func (s *Strategy) placeWallOrders(ctx context.Context, orderExecutor qbtrade.OrderExecutor) error {
log.Infof("placing wall orders...")
var submitOrders []types.SubmitOrder
var startPrice = s.FixedPrice
for i := 0; i < s.NumLayers; i++ {
var price = startPrice
var quantity fixedpoint.Value
if s.QuantityOrAmount.IsSet() {
quantity = s.QuantityOrAmount.CalculateQuantity(price)
} else if s.QuantityScale != nil {
qf, err := s.QuantityScale.Scale(i + 1)
if err != nil {
return err
}
quantity = fixedpoint.NewFromFloat(qf)
}
order := types.SubmitOrder{
Symbol: s.Symbol,
Side: s.Side,
Type: types.OrderTypeLimitMaker,
Price: price,
Quantity: quantity,
Market: s.Market,
}
submitOrders = append(submitOrders, order)
switch s.Side {
case types.SideTypeSell:
startPrice = startPrice.Add(s.LayerSpread)
case types.SideTypeBuy:
startPrice = startPrice.Sub(s.LayerSpread)
}
}
// condition for lower the average cost
if len(submitOrders) == 0 {
return nil
}
createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrders...)
if err != nil {
return err
}
log.Infof("wall orders placed: %+v", createdOrders)
s.activeWallOrders.Add(createdOrders...)
return err
}
func (s *Strategy) Run(ctx context.Context, _ qbtrade.OrderExecutor, session *qbtrade.ExchangeSession) error {
s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID())
// initial required information
s.session = session
s.activeWallOrders = qbtrade.NewActiveOrderBook(s.Symbol)
s.activeWallOrders.BindStream(session.UserDataStream)
s.activeAdjustmentOrders = qbtrade.NewActiveOrderBook(s.Symbol)
s.activeAdjustmentOrders.BindStream(session.UserDataStream)
session.UserDataStream.OnStart(func() {
if err := s.placeWallOrders(ctx, s.OrderExecutor); err != nil {
log.WithError(err).Errorf("can not place order")
}
})
s.activeAdjustmentOrders.OnFilled(func(o types.Order) {
if err := s.activeAdjustmentOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
// check if there is a canceled order had partially filled.
s.OrderExecutor.TradeCollector().Process()
if err := s.placeAdjustmentOrders(ctx, s.OrderExecutor); err != nil {
log.WithError(err).Errorf("can not place order")
}
})
s.activeWallOrders.OnFilled(func(o types.Order) {
if err := s.activeWallOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
// check if there is a canceled order had partially filled.
s.OrderExecutor.TradeCollector().Process()
if err := s.placeWallOrders(ctx, s.OrderExecutor); err != nil {
log.WithError(err).Errorf("can not place order")
}
if err := s.activeAdjustmentOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
// check if there is a canceled order had partially filled.
s.OrderExecutor.TradeCollector().Process()
if err := s.placeAdjustmentOrders(ctx, s.OrderExecutor); err != nil {
log.WithError(err).Errorf("can not place order")
}
})
ticker := time.NewTicker(s.Interval.Duration())
go func() {
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
orders := s.activeWallOrders.Orders()
if anyOrderFilled(orders) {
if err := s.activeWallOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
// check if there is a canceled order had partially filled.
s.OrderExecutor.TradeCollector().Process()
if err := s.placeWallOrders(ctx, s.OrderExecutor); err != nil {
log.WithError(err).Errorf("can not place order")
}
}
}
}
}()
qbtrade.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
if err := s.activeWallOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
if err := s.activeAdjustmentOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
// check if there is a canceled order had partially filled.
s.OrderExecutor.TradeCollector().Process()
qbtrade.Sync(ctx, s)
})
return nil
}
func anyOrderFilled(orders []types.Order) bool {
for _, o := range orders {
if o.ExecutedQuantity.Sign() > 0 {
return true
}
}
return false
}