2020-10-24 10:22:23 +00:00
|
|
|
package xpuremaker
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"math"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
|
|
|
|
"github.com/c9s/bbgo/pkg/bbgo"
|
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
|
|
|
|
2021-02-03 01:08:05 +00:00
|
|
|
const ID = "xpuremaker"
|
|
|
|
|
2020-10-24 10:22:23 +00:00
|
|
|
func init() {
|
2021-02-03 01:08:05 +00:00
|
|
|
bbgo.RegisterStrategy(ID, &Strategy{})
|
2020-10-24 10:22:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Strategy struct {
|
|
|
|
Symbol string `json:"symbol"`
|
|
|
|
Side string `json:"side"`
|
|
|
|
NumOrders int `json:"numOrders"`
|
|
|
|
BehindVolume fixedpoint.Value `json:"behindVolume"`
|
|
|
|
PriceTick fixedpoint.Value `json:"priceTick"`
|
|
|
|
BaseQuantity fixedpoint.Value `json:"baseQuantity"`
|
|
|
|
BuySellRatio float64 `json:"buySellRatio"`
|
|
|
|
|
2020-10-25 16:26:17 +00:00
|
|
|
book *types.StreamOrderBook
|
|
|
|
activeOrders map[string]types.Order
|
2020-10-24 10:22:23 +00:00
|
|
|
}
|
|
|
|
|
2021-02-03 01:08:05 +00:00
|
|
|
func (s *Strategy) ID() string {
|
|
|
|
return ID
|
|
|
|
}
|
|
|
|
|
2020-10-28 09:49:49 +00:00
|
|
|
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
2020-10-24 10:22:23 +00:00
|
|
|
session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
|
2020-10-28 09:49:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
2020-10-24 10:22:23 +00:00
|
|
|
|
|
|
|
s.book = types.NewStreamBook(s.Symbol)
|
|
|
|
s.book.BindStream(session.Stream)
|
|
|
|
|
2020-10-25 16:26:17 +00:00
|
|
|
s.activeOrders = make(map[string]types.Order)
|
|
|
|
|
2020-10-24 10:22:23 +00:00
|
|
|
// We can move the go routine to the parent level.
|
|
|
|
go func() {
|
|
|
|
ticker := time.NewTicker(1 * time.Minute)
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
2020-10-26 02:01:18 +00:00
|
|
|
s.update(orderExecutor, session)
|
2020-10-25 10:26:10 +00:00
|
|
|
|
2020-10-24 10:22:23 +00:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
|
|
|
|
case <-s.book.C:
|
2020-10-26 02:01:18 +00:00
|
|
|
s.update(orderExecutor, session)
|
2020-10-24 10:22:23 +00:00
|
|
|
|
|
|
|
case <-ticker.C:
|
2020-10-26 02:01:18 +00:00
|
|
|
s.update(orderExecutor, session)
|
2020-10-24 10:22:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-26 02:01:18 +00:00
|
|
|
func (s *Strategy) cancelOrders(session *bbgo.ExchangeSession) {
|
2020-10-25 16:26:17 +00:00
|
|
|
var deletedIDs []string
|
|
|
|
for clientOrderID, o := range s.activeOrders {
|
|
|
|
log.Infof("canceling order: %+v", o)
|
|
|
|
|
2020-10-26 02:01:18 +00:00
|
|
|
if err := session.Exchange.CancelOrders(context.Background(), o); err != nil {
|
2020-10-25 16:26:17 +00:00
|
|
|
log.WithError(err).Error("cancel order error")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
deletedIDs = append(deletedIDs, clientOrderID)
|
|
|
|
}
|
|
|
|
s.book.C.Drain(1*time.Second, 3*time.Second)
|
|
|
|
|
|
|
|
for _, id := range deletedIDs {
|
|
|
|
delete(s.activeOrders, id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-26 02:01:18 +00:00
|
|
|
func (s *Strategy) update(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) {
|
|
|
|
s.cancelOrders(session)
|
2020-10-25 16:26:17 +00:00
|
|
|
|
2020-10-24 10:22:23 +00:00
|
|
|
switch s.Side {
|
|
|
|
case "buy":
|
2020-10-26 02:08:58 +00:00
|
|
|
s.updateOrders(orderExecutor, session, types.SideTypeBuy)
|
2020-10-24 10:22:23 +00:00
|
|
|
case "sell":
|
2020-10-26 02:08:58 +00:00
|
|
|
s.updateOrders(orderExecutor, session, types.SideTypeSell)
|
2020-10-24 10:22:23 +00:00
|
|
|
case "both":
|
2020-10-26 02:08:58 +00:00
|
|
|
s.updateOrders(orderExecutor, session, types.SideTypeBuy)
|
|
|
|
s.updateOrders(orderExecutor, session, types.SideTypeSell)
|
2020-10-25 10:26:10 +00:00
|
|
|
|
|
|
|
default:
|
|
|
|
log.Panicf("undefined side: %s", s.Side)
|
2020-10-24 10:22:23 +00:00
|
|
|
}
|
2020-10-25 16:26:17 +00:00
|
|
|
|
|
|
|
s.book.C.Drain(1*time.Second, 3*time.Second)
|
2020-10-24 10:22:23 +00:00
|
|
|
}
|
|
|
|
|
2020-10-26 02:08:58 +00:00
|
|
|
func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession, side types.SideType) {
|
|
|
|
var book = s.book.Copy()
|
2021-05-22 08:32:29 +00:00
|
|
|
var pvs = book.SideBook(side)
|
2020-10-24 10:22:23 +00:00
|
|
|
if pvs == nil || len(pvs) == 0 {
|
2020-10-26 02:08:58 +00:00
|
|
|
log.Warnf("empty side: %s", side)
|
2020-10-24 10:22:23 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-10-25 10:26:10 +00:00
|
|
|
log.Infof("placing order behind volume: %f", s.BehindVolume.Float64())
|
|
|
|
|
2020-10-26 02:01:18 +00:00
|
|
|
idx := pvs.IndexByVolumeDepth(s.BehindVolume)
|
2020-10-26 02:08:58 +00:00
|
|
|
if idx == -1 || idx > len(pvs)-1 {
|
2020-10-24 10:22:23 +00:00
|
|
|
// do not place orders
|
|
|
|
log.Warn("depth is not enough")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-10-26 02:01:18 +00:00
|
|
|
var depthPrice = pvs[idx].Price
|
|
|
|
var orders = s.generateOrders(s.Symbol, side, depthPrice, s.PriceTick, s.BaseQuantity, s.NumOrders)
|
2020-10-24 10:22:23 +00:00
|
|
|
if len(orders) == 0 {
|
|
|
|
log.Warn("empty orders")
|
|
|
|
return
|
|
|
|
}
|
2020-10-25 16:26:17 +00:00
|
|
|
|
|
|
|
createdOrders, err := orderExecutor.SubmitOrders(context.Background(), orders...)
|
|
|
|
if err != nil {
|
2020-10-25 10:26:10 +00:00
|
|
|
log.WithError(err).Errorf("order submit error")
|
|
|
|
return
|
|
|
|
}
|
2020-10-25 16:26:17 +00:00
|
|
|
|
|
|
|
// add created orders to the list
|
|
|
|
for i, o := range createdOrders {
|
|
|
|
s.activeOrders[o.ClientOrderID] = createdOrders[i]
|
|
|
|
}
|
2020-10-24 10:22:23 +00:00
|
|
|
}
|
|
|
|
|
2020-10-26 02:08:58 +00:00
|
|
|
func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseQuantity fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) {
|
2020-10-24 10:22:23 +00:00
|
|
|
var expBase = fixedpoint.NewFromFloat(0.0)
|
|
|
|
|
|
|
|
switch side {
|
|
|
|
case types.SideTypeBuy:
|
|
|
|
if priceTick > 0 {
|
|
|
|
priceTick = -priceTick
|
|
|
|
}
|
|
|
|
|
|
|
|
case types.SideTypeSell:
|
|
|
|
if priceTick < 0 {
|
|
|
|
priceTick = -priceTick
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := 0; i < numOrders; i++ {
|
2020-10-26 02:08:58 +00:00
|
|
|
volume := math.Exp(expBase.Float64()) * baseQuantity.Float64()
|
2020-10-24 10:22:23 +00:00
|
|
|
|
|
|
|
// skip order less than 10usd
|
|
|
|
if volume*price.Float64() < 10.0 {
|
|
|
|
log.Warnf("amount too small (< 10usd). price=%f volume=%f amount=%f", price.Float64(), volume, volume*price.Float64())
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
orders = append(orders, types.SubmitOrder{
|
|
|
|
Symbol: symbol,
|
|
|
|
Side: side,
|
|
|
|
Type: types.OrderTypeLimit,
|
|
|
|
Price: price.Float64(),
|
|
|
|
Quantity: volume,
|
|
|
|
})
|
|
|
|
|
2020-10-25 10:26:10 +00:00
|
|
|
log.Infof("%s order: %.2f @ %f", side, volume, price.Float64())
|
2020-10-24 10:22:23 +00:00
|
|
|
|
|
|
|
if len(orders) >= numOrders {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
price = price + priceTick
|
|
|
|
declog := math.Log10(math.Abs(priceTick.Float64()))
|
|
|
|
expBase += fixedpoint.NewFromFloat(math.Pow10(-int(declog)) * math.Abs(priceTick.Float64()))
|
|
|
|
}
|
|
|
|
|
|
|
|
return orders
|
|
|
|
}
|