add wall strategy

This commit is contained in:
c9s 2022-05-12 22:51:39 +08:00
parent 668328dd16
commit e950ee9559
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
3 changed files with 312 additions and 0 deletions

38
config/wall.yaml Normal file
View File

@ -0,0 +1,38 @@
---
persistence:
redis:
host: 127.0.0.1
port: 6379
db: 0
sessions:
max:
exchange: max
envVarPrefix: MAX
exchangeStrategies:
- on: max
wall:
symbol: DOTUSDT
# interval is how long do you want to update your order price and quantity
interval: 1m
fixedPrice: 2.0
side: buy
# quantity is the base order quantity for your buy/sell order.
# quantity: 0.05
numLayers: 3
layerSpread: 0.1
quantityScale:
byLayer:
linear:
domain: [ 1, 3 ]
range: [ 10.0, 30.0 ]

View File

@ -21,6 +21,7 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/support"
_ "github.com/c9s/bbgo/pkg/strategy/swing"
_ "github.com/c9s/bbgo/pkg/strategy/techsignal"
_ "github.com/c9s/bbgo/pkg/strategy/wall"
_ "github.com/c9s/bbgo/pkg/strategy/xbalance"
_ "github.com/c9s/bbgo/pkg/strategy/xgap"
_ "github.com/c9s/bbgo/pkg/strategy/xmaker"

View File

@ -0,0 +1,273 @@
package wall
import (
"context"
"fmt"
"sync"
"time"
"github.com/c9s/bbgo/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/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() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
type Strategy struct {
*bbgo.Graceful
*bbgo.Notifiability
*bbgo.Persistence
Environment *bbgo.Environment
StandardIndicatorSet *bbgo.StandardIndicatorSet
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"`
bbgo.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 *bbgo.LayerScale `json:"quantityScale,omitempty"`
session *bbgo.ExchangeSession
// persistence fields
Position *types.Position `json:"position,omitempty" persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
activeMakerOrders *bbgo.LocalActiveOrderBook
orderStore *bbgo.OrderStore
tradeCollector *bbgo.TradeCollector
groupID uint32
stopC chan struct{}
}
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 *bbgo.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) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor) error {
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,
GroupID: s.groupID,
}
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
}
s.orderStore.Add(createdOrders...)
s.activeMakerOrders.Add(createdOrders...)
return err
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
// initial required information
s.session = session
// calculate group id for orders
instanceID := s.InstanceID()
s.groupID = util.FNV32(instanceID)
// If position is nil, we need to allocate a new position for calculation
if s.Position == nil {
s.Position = types.NewPositionFromMarket(s.Market)
}
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(s.Market)
}
// Always update the position fields
s.Position.Strategy = ID
s.Position.StrategyInstanceID = instanceID
s.stopC = make(chan struct{})
s.activeMakerOrders = bbgo.NewLocalActiveOrderBook(s.Symbol)
s.activeMakerOrders.BindStream(session.UserDataStream)
s.orderStore = bbgo.NewOrderStore(s.Symbol)
s.orderStore.BindStream(session.UserDataStream)
s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore)
s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) {
s.Notifiability.Notify(trade)
s.ProfitStats.AddTrade(trade)
if profit.Compare(fixedpoint.Zero) == 0 {
s.Environment.RecordPosition(s.Position, trade, nil)
} else {
log.Infof("%s generated profit: %v", s.Symbol, profit)
p := s.Position.NewProfit(trade, profit, netProfit)
p.Strategy = ID
p.StrategyInstanceID = instanceID
s.Notify(&p)
s.ProfitStats.AddProfit(p)
s.Notify(&s.ProfitStats)
s.Environment.RecordPosition(s.Position, trade, &p)
}
})
s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
log.Infof("position changed: %s", s.Position)
s.Notify(s.Position)
})
s.tradeCollector.BindStream(session.UserDataStream)
session.UserDataStream.OnStart(func() {
if err := s.placeOrders(ctx, 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.activeMakerOrders.Orders()
if anyOrderFilled(orders) {
if err := s.activeMakerOrders.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.tradeCollector.Process()
if err := s.placeOrders(ctx, orderExecutor); err != nil {
log.WithError(err).Errorf("can not place order")
}
}
}
}
}()
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
close(s.stopC)
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
s.tradeCollector.Process()
})
return nil
}
func anyOrderFilled(orders []types.Order) bool {
for _, o := range orders {
if o.ExecutedQuantity.Sign() > 0 {
return true
}
}
return false
}