mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-21 22:43:52 +00:00
Merge pull request #1752 from c9s/c9s/bitget/improvements
IMPROVE: [biget] several improvements
This commit is contained in:
commit
03221a0a71
|
@ -197,7 +197,7 @@ func (e *Exchange) QueryKLines(
|
|||
|
||||
limit := uint64(options.Limit)
|
||||
if limit > defaultKLineLimit || limit <= 0 {
|
||||
log.Debugf("limtit is exceeded or zero, update to %d, got: %d", defaultKLineLimit, options.Limit)
|
||||
log.Debugf("the parameter limit exceeds the server boundary or is set to zero. changed to %d, original value: %d", defaultKLineLimit, options.Limit)
|
||||
limit = defaultKLineLimit
|
||||
}
|
||||
req.Limit(strconv.FormatUint(limit, 10))
|
||||
|
@ -568,7 +568,7 @@ func (e *Exchange) QueryTrades(
|
|||
|
||||
limit := options.Limit
|
||||
if limit > queryLimit || limit <= 0 {
|
||||
log.Debugf("limtit is exceeded or zero, update to %d, got: %d", queryLimit, options.Limit)
|
||||
log.Debugf("the parameter limit exceeds the server boundary or is set to zero. changed to %d, original value: %d", queryLimit, options.Limit)
|
||||
limit = queryLimit
|
||||
}
|
||||
req.Limit(strconv.FormatInt(limit, 10))
|
||||
|
|
|
@ -435,10 +435,14 @@ func (s *Stream) handleOrderTradeEvent(m OrderTradeEvent) {
|
|||
return
|
||||
}
|
||||
|
||||
debugf("received OrderTradeEvent: %+v", m)
|
||||
debugf("received OrderTradeEvent %s (%s): %+v", m.instId, m.actionType, m)
|
||||
|
||||
for _, order := range m.Orders {
|
||||
debugf("received Order: %+v", order)
|
||||
if order.TradeId == 0 {
|
||||
debugf("received %s order update #%d: %+v", m.instId, order.OrderId, order)
|
||||
} else {
|
||||
debugf("received %s order update #%d and trade update #%d: %+v", m.instId, order.OrderId, order.TradeId, order)
|
||||
}
|
||||
|
||||
globalOrder, err := order.toGlobalOrder()
|
||||
if err != nil {
|
||||
|
|
|
@ -395,7 +395,9 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err
|
|||
return errs
|
||||
}
|
||||
|
||||
func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, util time.Time, lastOrderID uint64) (orders []types.Order, err error) {
|
||||
func (e *Exchange) QueryClosedOrders(
|
||||
ctx context.Context, symbol string, since, util time.Time, lastOrderID uint64,
|
||||
) (orders []types.Order, err error) {
|
||||
if !since.IsZero() || !util.IsZero() {
|
||||
log.Warn("!!!BYBIT EXCHANGE API NOTICE!!! the since/until conditions will not be effected on SPOT account, bybit exchange does not support time-range-based query currently")
|
||||
}
|
||||
|
@ -465,7 +467,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
|||
|
||||
limit := uint64(options.Limit)
|
||||
if limit > defaultQueryLimit || limit <= 0 {
|
||||
log.Debugf("limtit is exceeded or zero, update to %d, got: %d", defaultQueryLimit, options.Limit)
|
||||
log.Debugf("the parameter limit exceeds the server boundary or is set to zero. changed to %d, original value: %d", defaultQueryLimit, options.Limit)
|
||||
limit = defaultQueryLimit
|
||||
}
|
||||
req.Limit(limit)
|
||||
|
@ -533,7 +535,9 @@ on the requested interval.
|
|||
A k-line's start time is inclusive, but end time is not(startTime + interval - 1 millisecond).
|
||||
e.q. 15m interval k line can be represented as 00:00:00.000 ~ 00:14:59.999
|
||||
*/
|
||||
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
|
||||
func (e *Exchange) QueryKLines(
|
||||
ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions,
|
||||
) ([]types.KLine, error) {
|
||||
req := e.client.NewGetKLinesRequest().Symbol(symbol)
|
||||
intervalStr, err := toLocalInterval(interval)
|
||||
if err != nil {
|
||||
|
@ -543,7 +547,7 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type
|
|||
|
||||
limit := uint64(options.Limit)
|
||||
if limit > defaultKLineLimit || limit <= 0 {
|
||||
log.Debugf("limtit is exceeded or zero, update to %d, got: %d", defaultKLineLimit, options.Limit)
|
||||
log.Debugf("the parameter limit exceeds the server boundary or is set to zero. changed to %d, original value: %d", defaultQueryLimit, options.Limit)
|
||||
limit = defaultKLineLimit
|
||||
}
|
||||
req.Limit(limit)
|
||||
|
|
37
pkg/fixedpoint/mutex.go
Normal file
37
pkg/fixedpoint/mutex.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package fixedpoint
|
||||
|
||||
import "sync"
|
||||
|
||||
type MutexValue struct {
|
||||
value Value
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (f *MutexValue) Add(v Value) Value {
|
||||
f.mu.Lock()
|
||||
f.value = f.value.Add(v)
|
||||
ret := f.value
|
||||
f.mu.Unlock()
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f *MutexValue) Sub(v Value) Value {
|
||||
f.mu.Lock()
|
||||
f.value = f.value.Sub(v)
|
||||
ret := f.value
|
||||
f.mu.Unlock()
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f *MutexValue) Set(v Value) {
|
||||
f.mu.Lock()
|
||||
f.value = v
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
func (f *MutexValue) Get() Value {
|
||||
f.mu.Lock()
|
||||
v := f.value
|
||||
f.mu.Unlock()
|
||||
return v
|
||||
}
|
|
@ -375,7 +375,7 @@ func (s *Strategy) Close(ctx context.Context) error {
|
|||
|
||||
var err error
|
||||
if s.UniversalCancelAllOrdersWhenClose {
|
||||
err = tradingutil.UniversalCancelAllOrders(ctx, s.ExchangeSession.Exchange, nil)
|
||||
err = tradingutil.UniversalCancelAllOrders(ctx, s.ExchangeSession.Exchange, s.Symbol, nil)
|
||||
} else {
|
||||
err = s.OrderExecutor.GracefulCancel(ctx)
|
||||
}
|
||||
|
@ -398,7 +398,7 @@ func (s *Strategy) CleanUp(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// ignore the first cancel error, this skips one open-orders query request
|
||||
if err := tradingutil.UniversalCancelAllOrders(ctx, session.Exchange, nil); err == nil {
|
||||
if err := tradingutil.UniversalCancelAllOrders(ctx, session.Exchange, s.Symbol, nil); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -418,7 +418,7 @@ func (s *Strategy) CleanUp(ctx context.Context) error {
|
|||
break
|
||||
}
|
||||
|
||||
if err := tradingutil.UniversalCancelAllOrders(ctx, session.Exchange, openOrders); err != nil {
|
||||
if err := tradingutil.UniversalCancelAllOrders(ctx, session.Exchange, s.Symbol, openOrders); err != nil {
|
||||
s.logger.WithError(err).Errorf("unable to cancel all orders")
|
||||
werr = multierr.Append(werr, err)
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
|
|||
util.LogErr(err, "unable to cancel adjustment orders")
|
||||
}
|
||||
|
||||
if err := tradingutil.UniversalCancelAllOrders(ctx, s.Session.Exchange, nil); err != nil {
|
||||
if err := tradingutil.UniversalCancelAllOrders(ctx, s.Session.Exchange, s.Symbol, nil); err != nil {
|
||||
util.LogErr(err, "unable to cancel all orders")
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -16,8 +17,10 @@ import (
|
|||
"github.com/c9s/bbgo/pkg/exchange/retry"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/strategy/common"
|
||||
"github.com/c9s/bbgo/pkg/strategy/xmaker"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
"github.com/c9s/bbgo/pkg/util/tradingutil"
|
||||
)
|
||||
|
||||
var lastPriceModifier = fixedpoint.NewFromFloat(1.001)
|
||||
|
@ -32,6 +35,10 @@ const ID = "xdepthmaker"
|
|||
|
||||
var log = logrus.WithField("strategy", ID)
|
||||
|
||||
var ErrZeroQuantity = stderrors.New("quantity is zero")
|
||||
var ErrDustQuantity = stderrors.New("quantity is dust")
|
||||
var ErrZeroPrice = stderrors.New("price is zero")
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||
}
|
||||
|
@ -46,9 +53,10 @@ type CrossExchangeMarketMakingStrategy struct {
|
|||
makerMarket, hedgeMarket types.Market
|
||||
|
||||
// persistence fields
|
||||
Position *types.Position `json:"position,omitempty" persistence:"position"`
|
||||
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
|
||||
CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty" persistence:"covered_position"`
|
||||
Position *types.Position `json:"position,omitempty" persistence:"position"`
|
||||
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
|
||||
|
||||
CoveredPosition fixedpoint.MutexValue
|
||||
|
||||
core.ConverterManager
|
||||
|
||||
|
@ -152,13 +160,19 @@ func (s *CrossExchangeMarketMakingStrategy) Initialize(
|
|||
// 2) short position -> increase short position
|
||||
|
||||
// TODO: make this atomic
|
||||
s.mu.Lock()
|
||||
s.CoveredPosition = s.CoveredPosition.Add(c)
|
||||
s.mu.Unlock()
|
||||
s.CoveredPosition.Add(c)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
type HedgeStrategy string
|
||||
|
||||
const (
|
||||
HedgeStrategyMarket HedgeStrategy = "market"
|
||||
HedgeStrategyBboCounterParty1 HedgeStrategy = "bbo-counter-party-1"
|
||||
HedgeStrategyBboQueue1 HedgeStrategy = "bbo-queue-1"
|
||||
)
|
||||
|
||||
type Strategy struct {
|
||||
*CrossExchangeMarketMakingStrategy
|
||||
|
||||
|
@ -181,6 +195,8 @@ type Strategy struct {
|
|||
|
||||
HedgeInterval types.Duration `json:"hedgeInterval"`
|
||||
|
||||
HedgeStrategy HedgeStrategy `json:"hedgeStrategy"`
|
||||
|
||||
FullReplenishInterval types.Duration `json:"fullReplenishInterval"`
|
||||
|
||||
OrderCancelWaitTime types.Duration `json:"orderCancelWaitTime"`
|
||||
|
@ -225,16 +241,18 @@ type Strategy struct {
|
|||
// --------------------------------
|
||||
|
||||
// pricingBook is the order book (depth) from the hedging session
|
||||
pricingBook *types.StreamOrderBook
|
||||
sourceBook *types.StreamOrderBook
|
||||
|
||||
hedgeErrorLimiter *rate.Limiter
|
||||
hedgeErrorRateReservation *rate.Reservation
|
||||
|
||||
askPriceHeartBeat, bidPriceHeartBeat *types.PriceHeartBeat
|
||||
|
||||
lastPrice fixedpoint.Value
|
||||
lastSourcePrice fixedpoint.MutexValue
|
||||
|
||||
stopC, authedC chan struct{}
|
||||
|
||||
logger logrus.FieldLogger
|
||||
}
|
||||
|
||||
func (s *Strategy) ID() string {
|
||||
|
@ -242,7 +260,14 @@ func (s *Strategy) ID() string {
|
|||
}
|
||||
|
||||
func (s *Strategy) InstanceID() string {
|
||||
return fmt.Sprintf("%s:%s:%s-%s", ID, s.Symbol, s.MakerExchange, s.HedgeExchange)
|
||||
// this generates a unique instance ID for the strategy
|
||||
return strings.Join([]string{
|
||||
ID,
|
||||
s.MakerExchange,
|
||||
s.Symbol,
|
||||
s.HedgeExchange,
|
||||
s.HedgeSymbol,
|
||||
}, "-")
|
||||
}
|
||||
|
||||
func (s *Strategy) Initialize() error {
|
||||
|
@ -252,6 +277,12 @@ func (s *Strategy) Initialize() error {
|
|||
|
||||
s.bidPriceHeartBeat = types.NewPriceHeartBeat(priceUpdateTimeout)
|
||||
s.askPriceHeartBeat = types.NewPriceHeartBeat(priceUpdateTimeout)
|
||||
s.logger = log.WithFields(logrus.Fields{
|
||||
"symbol": s.Symbol,
|
||||
"strategy": ID,
|
||||
"strategy_instance": s.InstanceID(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -277,7 +308,7 @@ func (s *Strategy) Validate() error {
|
|||
}
|
||||
|
||||
if s.HedgeExchange == "" {
|
||||
return errors.New("maker exchange is not configured")
|
||||
return errors.New("hedge exchange is not configured")
|
||||
}
|
||||
|
||||
if s.DepthScale == nil {
|
||||
|
@ -304,6 +335,10 @@ func (s *Strategy) Defaults() error {
|
|||
s.HedgeInterval = types.Duration(3 * time.Second)
|
||||
}
|
||||
|
||||
if s.HedgeStrategy == "" {
|
||||
s.HedgeStrategy = HedgeStrategyMarket
|
||||
}
|
||||
|
||||
if s.HedgeSymbol == "" {
|
||||
s.HedgeSymbol = s.Symbol
|
||||
}
|
||||
|
@ -393,8 +428,8 @@ func (s *Strategy) CrossRun(
|
|||
return err
|
||||
}
|
||||
|
||||
s.pricingBook = types.NewStreamBook(s.HedgeSymbol, s.hedgeSession.ExchangeName)
|
||||
s.pricingBook.BindStream(s.hedgeSession.MarketDataStream)
|
||||
s.sourceBook = types.NewStreamBook(s.HedgeSymbol, s.hedgeSession.ExchangeName)
|
||||
s.sourceBook.BindStream(s.hedgeSession.MarketDataStream)
|
||||
|
||||
s.stopC = make(chan struct{})
|
||||
|
||||
|
@ -451,7 +486,7 @@ func (s *Strategy) CrossRun(
|
|||
s.updateQuote(ctx, 0)
|
||||
lastOrderReplenishTime = time.Now()
|
||||
|
||||
case sig, ok := <-s.pricingBook.C:
|
||||
case sig, ok := <-s.sourceBook.C:
|
||||
// when any book change event happened
|
||||
if !ok {
|
||||
return
|
||||
|
@ -486,18 +521,28 @@ func (s *Strategy) CrossRun(
|
|||
s.MakerOrderExecutor.TradeCollector().Process()
|
||||
|
||||
position := s.Position.GetBase()
|
||||
uncoverPosition := position.Sub(s.CoveredPosition)
|
||||
|
||||
coveredPosition := s.CoveredPosition.Get()
|
||||
uncoverPosition := position.Sub(coveredPosition)
|
||||
|
||||
absPos := uncoverPosition.Abs()
|
||||
if absPos.Compare(s.hedgeMarket.MinQuantity) > 0 {
|
||||
log.Infof("%s base position %v coveredPosition: %v uncoverPosition: %v",
|
||||
s.Symbol,
|
||||
position,
|
||||
s.CoveredPosition,
|
||||
coveredPosition,
|
||||
uncoverPosition,
|
||||
)
|
||||
|
||||
if !s.DisableHedge {
|
||||
s.Hedge(ctx, uncoverPosition.Neg())
|
||||
if err := s.Hedge(ctx, uncoverPosition.Neg()); err != nil {
|
||||
//goland:noinspection GoDirectComparisonOfErrors
|
||||
switch err {
|
||||
case ErrZeroQuantity, ErrDustQuantity:
|
||||
default:
|
||||
s.logger.WithError(err).Errorf("unable to hedge position")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -520,6 +565,10 @@ func (s *Strategy) CrossRun(
|
|||
log.WithError(err).Errorf("graceful cancel %s order error", s.HedgeSymbol)
|
||||
}
|
||||
|
||||
if err := tradingutil.UniversalCancelAllOrders(ctx, s.makerSession.Exchange, s.Symbol, s.MakerOrderExecutor.ActiveMakerOrders().Orders()); err != nil {
|
||||
log.WithError(err).Errorf("unable to cancel all orders")
|
||||
}
|
||||
|
||||
bbgo.Sync(ctx, s)
|
||||
bbgo.Notify("%s: %s position", ID, s.Symbol, s.Position)
|
||||
})
|
||||
|
@ -527,112 +576,218 @@ func (s *Strategy) CrossRun(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
|
||||
side := types.SideTypeBuy
|
||||
func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) error {
|
||||
if pos.IsZero() {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
quantity := pos.Abs()
|
||||
|
||||
// the default side
|
||||
side := types.SideTypeBuy
|
||||
if pos.Sign() < 0 {
|
||||
side = types.SideTypeSell
|
||||
}
|
||||
|
||||
lastPrice := s.lastPrice
|
||||
sourceBook := s.pricingBook.CopyDepth(1)
|
||||
switch side {
|
||||
quantity := pos.Abs()
|
||||
|
||||
case types.SideTypeBuy:
|
||||
if bestAsk, ok := sourceBook.BestAsk(); ok {
|
||||
lastPrice = bestAsk.Price
|
||||
}
|
||||
switch s.HedgeStrategy {
|
||||
case HedgeStrategyMarket:
|
||||
return s.executeHedgeMarket(ctx, side, quantity)
|
||||
case HedgeStrategyBboCounterParty1:
|
||||
return s.executeHedgeBboCounterParty1(ctx, side, quantity)
|
||||
case HedgeStrategyBboQueue1:
|
||||
return s.executeHedgeBboQueue1(ctx, side, quantity)
|
||||
default:
|
||||
return fmt.Errorf("unsupported or invalid hedge strategy setup %q, please check your configuration", s.HedgeStrategy)
|
||||
}
|
||||
}
|
||||
|
||||
case types.SideTypeSell:
|
||||
if bestBid, ok := sourceBook.BestBid(); ok {
|
||||
lastPrice = bestBid.Price
|
||||
}
|
||||
func (s *Strategy) executeHedgeBboCounterParty1(
|
||||
ctx context.Context,
|
||||
side types.SideType,
|
||||
quantity fixedpoint.Value,
|
||||
) error {
|
||||
price := s.lastSourcePrice.Get()
|
||||
if sourcePrice := s.getSourceBboPrice(side.Reverse()); sourcePrice.Sign() > 0 {
|
||||
price = sourcePrice
|
||||
}
|
||||
|
||||
notional := quantity.Mul(lastPrice)
|
||||
if notional.Compare(s.hedgeMarket.MinNotional) <= 0 {
|
||||
log.Warnf("%s %v less than min notional, skipping hedge", s.Symbol, notional)
|
||||
return
|
||||
if price.IsZero() {
|
||||
return ErrZeroPrice
|
||||
}
|
||||
|
||||
// adjust quantity according to the balances
|
||||
account := s.hedgeSession.GetAccount()
|
||||
switch side {
|
||||
|
||||
case types.SideTypeBuy:
|
||||
// check quote quantity
|
||||
if quote, ok := account.Balance(s.hedgeMarket.QuoteCurrency); ok {
|
||||
if quote.Available.Compare(notional) < 0 {
|
||||
// adjust price to higher 0.1%, so that we can ensure that the order can be executed
|
||||
quantity = bbgo.AdjustQuantityByMaxAmount(quantity, lastPrice.Mul(lastPriceModifier), quote.Available)
|
||||
quantity = s.hedgeMarket.TruncateQuantity(quantity)
|
||||
}
|
||||
}
|
||||
|
||||
case types.SideTypeSell:
|
||||
// check quote quantity
|
||||
if base, ok := account.Balance(s.hedgeMarket.BaseCurrency); ok {
|
||||
if base.Available.Compare(quantity) < 0 {
|
||||
quantity = base.Available
|
||||
}
|
||||
}
|
||||
}
|
||||
quantity = xmaker.AdjustHedgeQuantityWithAvailableBalance(account,
|
||||
s.hedgeMarket,
|
||||
side,
|
||||
quantity,
|
||||
price)
|
||||
|
||||
// truncate quantity for the supported precision
|
||||
quantity = s.hedgeMarket.TruncateQuantity(quantity)
|
||||
|
||||
if notional.Compare(s.hedgeMarket.MinNotional.Mul(minGap)) <= 0 {
|
||||
log.Warnf("the adjusted amount %v is less than minimal notional %v, skipping hedge", notional, s.hedgeMarket.MinNotional)
|
||||
return
|
||||
if quantity.IsZero() {
|
||||
return ErrZeroQuantity
|
||||
}
|
||||
|
||||
if quantity.Compare(s.hedgeMarket.MinQuantity.Mul(minGap)) <= 0 {
|
||||
log.Warnf("the adjusted quantity %v is less than minimal quantity %v, skipping hedge", quantity, s.hedgeMarket.MinQuantity)
|
||||
return
|
||||
if s.hedgeMarket.IsDustQuantity(quantity, price) {
|
||||
return ErrDustQuantity
|
||||
}
|
||||
|
||||
if s.hedgeErrorRateReservation != nil {
|
||||
if !s.hedgeErrorRateReservation.OK() {
|
||||
return
|
||||
}
|
||||
bbgo.Notify("Hit hedge error rate limit, waiting...")
|
||||
time.Sleep(s.hedgeErrorRateReservation.Delay())
|
||||
s.hedgeErrorRateReservation = nil
|
||||
// submit order as limit taker
|
||||
return s.executeHedgeOrder(ctx, types.SubmitOrder{
|
||||
Market: s.hedgeMarket,
|
||||
Symbol: s.hedgeMarket.Symbol,
|
||||
Type: types.OrderTypeLimit,
|
||||
Price: price,
|
||||
Side: side,
|
||||
Quantity: quantity,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Strategy) executeHedgeBboQueue1(
|
||||
ctx context.Context,
|
||||
side types.SideType,
|
||||
quantity fixedpoint.Value,
|
||||
) error {
|
||||
price := s.lastSourcePrice.Get()
|
||||
if sourcePrice := s.getSourceBboPrice(side); sourcePrice.Sign() > 0 {
|
||||
price = sourcePrice
|
||||
}
|
||||
|
||||
log.Infof("submitting %s hedge order %s %v", s.HedgeSymbol, side.String(), quantity)
|
||||
bbgo.Notify("Submitting %s hedge order %s %v", s.HedgeSymbol, side.String(), quantity)
|
||||
if price.IsZero() {
|
||||
return ErrZeroPrice
|
||||
}
|
||||
|
||||
_, err := s.HedgeOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||
// adjust quantity according to the balances
|
||||
account := s.hedgeSession.GetAccount()
|
||||
|
||||
quantity = xmaker.AdjustHedgeQuantityWithAvailableBalance(account,
|
||||
s.hedgeMarket,
|
||||
side,
|
||||
quantity,
|
||||
price)
|
||||
|
||||
// truncate quantity for the supported precision
|
||||
quantity = s.hedgeMarket.TruncateQuantity(quantity)
|
||||
if quantity.IsZero() {
|
||||
return ErrZeroQuantity
|
||||
}
|
||||
|
||||
if s.hedgeMarket.IsDustQuantity(quantity, price) {
|
||||
return ErrDustQuantity
|
||||
}
|
||||
|
||||
// submit order as limit taker
|
||||
return s.executeHedgeOrder(ctx, types.SubmitOrder{
|
||||
Market: s.hedgeMarket,
|
||||
Symbol: s.hedgeMarket.Symbol,
|
||||
Type: types.OrderTypeLimit,
|
||||
Price: price,
|
||||
Side: side,
|
||||
Quantity: quantity,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Strategy) executeHedgeMarket(
|
||||
ctx context.Context,
|
||||
side types.SideType,
|
||||
quantity fixedpoint.Value,
|
||||
) error {
|
||||
price := s.lastSourcePrice.Get()
|
||||
if sourcePrice := s.getSourceBboPrice(side.Reverse()); sourcePrice.Sign() > 0 {
|
||||
price = sourcePrice
|
||||
}
|
||||
|
||||
if price.IsZero() {
|
||||
return ErrZeroPrice
|
||||
}
|
||||
|
||||
// adjust quantity according to the balances
|
||||
account := s.hedgeSession.GetAccount()
|
||||
|
||||
quantity = xmaker.AdjustHedgeQuantityWithAvailableBalance(account,
|
||||
s.hedgeMarket,
|
||||
side,
|
||||
quantity,
|
||||
price)
|
||||
|
||||
// truncate quantity for the supported precision
|
||||
quantity = s.hedgeMarket.TruncateQuantity(quantity)
|
||||
if quantity.IsZero() {
|
||||
return ErrZeroQuantity
|
||||
}
|
||||
|
||||
if s.hedgeMarket.IsDustQuantity(quantity, price) {
|
||||
return ErrDustQuantity
|
||||
}
|
||||
|
||||
return s.executeHedgeOrder(ctx, types.SubmitOrder{
|
||||
Market: s.hedgeMarket,
|
||||
Symbol: s.hedgeMarket.Symbol,
|
||||
Type: types.OrderTypeMarket,
|
||||
Side: side,
|
||||
Quantity: quantity,
|
||||
})
|
||||
}
|
||||
|
||||
// getSourceBboPrice returns the best bid offering price from the source order book
|
||||
func (s *Strategy) getSourceBboPrice(side types.SideType) fixedpoint.Value {
|
||||
bid, ask, ok := s.sourceBook.BestBidAndAsk()
|
||||
if !ok {
|
||||
return fixedpoint.Zero
|
||||
}
|
||||
|
||||
switch side {
|
||||
case types.SideTypeSell:
|
||||
return ask.Price
|
||||
case types.SideTypeBuy:
|
||||
return bid.Price
|
||||
}
|
||||
return fixedpoint.Zero
|
||||
}
|
||||
|
||||
func (s *Strategy) executeHedgeOrder(ctx context.Context, submitOrder types.SubmitOrder) error {
|
||||
if err := s.HedgeOrderExecutor.GracefulCancel(ctx); err != nil {
|
||||
s.logger.WithError(err).Warnf("graceful cancel order error")
|
||||
}
|
||||
|
||||
if s.hedgeErrorRateReservation != nil {
|
||||
if !s.hedgeErrorRateReservation.OK() {
|
||||
s.logger.Warnf("rate reservation hitted, skip executing hedge order")
|
||||
return nil
|
||||
}
|
||||
|
||||
bbgo.Notify("Hit hedge error rate limit, waiting...")
|
||||
time.Sleep(s.hedgeErrorRateReservation.Delay())
|
||||
|
||||
// reset reservation
|
||||
s.hedgeErrorRateReservation = nil
|
||||
}
|
||||
|
||||
bbgo.Notify("Submitting hedge %s order on %s %s %s %s @ %s",
|
||||
submitOrder.Type, s.HedgeSymbol, s.HedgeExchange,
|
||||
submitOrder.Side.String(),
|
||||
submitOrder.Quantity.String(),
|
||||
submitOrder.Price.String(),
|
||||
)
|
||||
|
||||
_, err := s.HedgeOrderExecutor.SubmitOrders(ctx, submitOrder)
|
||||
if err != nil {
|
||||
// allocate a new reservation
|
||||
s.hedgeErrorRateReservation = s.hedgeErrorLimiter.Reserve()
|
||||
log.WithError(err).Errorf("market order submit error: %s", err.Error())
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// if the hedge is on sell side, then we should add positive position
|
||||
switch side {
|
||||
switch submitOrder.Side {
|
||||
case types.SideTypeSell:
|
||||
s.mu.Lock()
|
||||
s.CoveredPosition = s.CoveredPosition.Add(quantity)
|
||||
s.mu.Unlock()
|
||||
s.CoveredPosition.Add(submitOrder.Quantity)
|
||||
case types.SideTypeBuy:
|
||||
s.mu.Lock()
|
||||
s.CoveredPosition = s.CoveredPosition.Add(quantity.Neg())
|
||||
s.mu.Unlock()
|
||||
s.CoveredPosition.Add(submitOrder.Quantity.Neg())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) runTradeRecover(ctx context.Context) {
|
||||
|
@ -853,7 +1008,7 @@ func (s *Strategy) updateQuote(ctx context.Context, maxLayer int) {
|
|||
return
|
||||
}
|
||||
|
||||
bestBid, bestAsk, hasPrice := s.pricingBook.BestBidAndAsk()
|
||||
bestBid, bestAsk, hasPrice := s.sourceBook.BestBidAndAsk()
|
||||
if !hasPrice {
|
||||
return
|
||||
}
|
||||
|
@ -862,9 +1017,9 @@ func (s *Strategy) updateQuote(ctx context.Context, maxLayer int) {
|
|||
bestAskPrice := bestAsk.Price
|
||||
log.Infof("%s book ticker: best ask / best bid = %v / %v", s.HedgeSymbol, bestAskPrice, bestBidPrice)
|
||||
|
||||
s.lastPrice = bestBidPrice.Add(bestAskPrice).Div(Two)
|
||||
s.lastSourcePrice.Set(bestBidPrice.Add(bestAskPrice).Div(Two))
|
||||
|
||||
bookLastUpdateTime := s.pricingBook.LastUpdateTime()
|
||||
bookLastUpdateTime := s.sourceBook.LastUpdateTime()
|
||||
|
||||
if _, err := s.bidPriceHeartBeat.Update(bestBid); err != nil {
|
||||
log.WithError(err).Warnf("quote update error, %s price not updating, order book last update: %s ago",
|
||||
|
@ -898,7 +1053,7 @@ func (s *Strategy) updateQuote(ctx context.Context, maxLayer int) {
|
|||
|
||||
log.Infof("quote balance: %s, base balance: %s", quoteBalance, baseBalance)
|
||||
|
||||
submitOrders, err := s.generateMakerOrders(s.pricingBook, maxLayer, baseBalance.Available, quoteBalance.Available)
|
||||
submitOrders, err := s.generateMakerOrders(s.sourceBook, maxLayer, baseBalance.Available, quoteBalance.Available)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("generate order error")
|
||||
return
|
||||
|
|
|
@ -1030,17 +1030,19 @@ func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote, makerBalances
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Strategy) adjustHedgeQuantityWithAvailableBalance(
|
||||
account *types.Account, side types.SideType, quantity, lastPrice fixedpoint.Value,
|
||||
func AdjustHedgeQuantityWithAvailableBalance(
|
||||
account *types.Account,
|
||||
market types.Market,
|
||||
side types.SideType, quantity, lastPrice fixedpoint.Value,
|
||||
) fixedpoint.Value {
|
||||
switch side {
|
||||
|
||||
case types.SideTypeBuy:
|
||||
// check quote quantity
|
||||
if quote, ok := account.Balance(s.sourceMarket.QuoteCurrency); ok {
|
||||
if quote.Available.Compare(s.sourceMarket.MinNotional) < 0 {
|
||||
if quote, ok := account.Balance(market.QuoteCurrency); ok {
|
||||
if quote.Available.Compare(market.MinNotional) < 0 {
|
||||
// adjust price to higher 0.1%, so that we can ensure that the order can be executed
|
||||
availableQuote := s.sourceMarket.TruncateQuoteQuantity(quote.Available)
|
||||
availableQuote := market.TruncateQuoteQuantity(quote.Available)
|
||||
quantity = bbgo.AdjustQuantityByMaxAmount(quantity, lastPrice, availableQuote)
|
||||
|
||||
}
|
||||
|
@ -1048,7 +1050,7 @@ func (s *Strategy) adjustHedgeQuantityWithAvailableBalance(
|
|||
|
||||
case types.SideTypeSell:
|
||||
// check quote quantity
|
||||
if base, ok := account.Balance(s.sourceMarket.BaseCurrency); ok {
|
||||
if base, ok := account.Balance(market.BaseCurrency); ok {
|
||||
if base.Available.Compare(quantity) < 0 {
|
||||
quantity = base.Available
|
||||
}
|
||||
|
@ -1056,7 +1058,7 @@ func (s *Strategy) adjustHedgeQuantityWithAvailableBalance(
|
|||
}
|
||||
|
||||
// truncate the quantity to the supported precision
|
||||
return s.sourceMarket.TruncateQuantity(quantity)
|
||||
return market.TruncateQuantity(quantity)
|
||||
}
|
||||
|
||||
func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
|
||||
|
@ -1094,7 +1096,8 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
|
|||
return
|
||||
}
|
||||
} else {
|
||||
quantity = s.adjustHedgeQuantityWithAvailableBalance(account, side, quantity, lastPrice)
|
||||
quantity = AdjustHedgeQuantityWithAvailableBalance(
|
||||
account, s.sourceMarket, side, quantity, lastPrice)
|
||||
}
|
||||
|
||||
// truncate quantity for the supported precision
|
||||
|
|
|
@ -4,6 +4,12 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// LogErr logs the error with the message and arguments if the error is not nil.
|
||||
// It returns true if the error is not nil.
|
||||
// Examples:
|
||||
// LogErr(err)
|
||||
// LogErr(err, "error message")
|
||||
// LogErr(err, "error message %s", "with argument")
|
||||
func LogErr(err error, msgAndArgs ...interface{}) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
|
|
|
@ -2,7 +2,6 @@ package tradingutil
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -28,7 +27,7 @@ type CancelAllOrdersByGroupIDService interface {
|
|||
//
|
||||
// if CancelAllOrdersService is not supported, then it tries CancelAllOrdersBySymbolService which needs at least one symbol
|
||||
// for the cancel api request.
|
||||
func UniversalCancelAllOrders(ctx context.Context, exchange types.Exchange, openOrders []types.Order) error {
|
||||
func UniversalCancelAllOrders(ctx context.Context, exchange types.Exchange, symbol string, openOrders []types.Order) error {
|
||||
if service, ok := exchange.(CancelAllOrdersService); ok {
|
||||
if _, err := service.CancelAllOrders(ctx); err == nil {
|
||||
return nil
|
||||
|
@ -37,17 +36,17 @@ func UniversalCancelAllOrders(ctx context.Context, exchange types.Exchange, open
|
|||
}
|
||||
}
|
||||
|
||||
if len(openOrders) == 0 {
|
||||
return errors.New("to cancel all orders, openOrders can not be empty")
|
||||
}
|
||||
|
||||
var anyErr error
|
||||
if service, ok := exchange.(CancelAllOrdersBySymbolService); ok {
|
||||
var symbols = CollectOrderSymbols(openOrders)
|
||||
for _, symbol := range symbols {
|
||||
_, err := service.CancelOrdersBySymbol(ctx, symbol)
|
||||
if err != nil {
|
||||
anyErr = err
|
||||
if len(symbol) > 0 {
|
||||
_, anyErr = service.CancelOrdersBySymbol(ctx, symbol)
|
||||
} else if len(openOrders) > 0 {
|
||||
var orderSymbols = CollectOrderSymbols(openOrders)
|
||||
for _, orderSymbol := range orderSymbols {
|
||||
_, err := service.CancelOrdersBySymbol(ctx, orderSymbol)
|
||||
if err != nil {
|
||||
anyErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,6 +55,11 @@ func UniversalCancelAllOrders(ctx context.Context, exchange types.Exchange, open
|
|||
}
|
||||
}
|
||||
|
||||
if len(openOrders) == 0 {
|
||||
log.Warnf("empty open orders, unable to call specific cancel all orders api, skip")
|
||||
return nil
|
||||
}
|
||||
|
||||
if service, ok := exchange.(CancelAllOrdersByGroupIDService); ok {
|
||||
var groupIds = CollectOrderGroupIds(openOrders)
|
||||
for _, groupId := range groupIds {
|
||||
|
|
Loading…
Reference in New Issue
Block a user