Merge pull request #1752 from c9s/c9s/bitget/improvements
Some checks failed
Go / build (1.21, 6.2) (push) Has been cancelled
golang-lint / lint (push) Has been cancelled

IMPROVE: [biget] several improvements
This commit is contained in:
c9s 2024-09-24 17:26:13 +08:00 committed by GitHub
commit 03221a0a71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 329 additions and 116 deletions

View File

@ -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))

View File

@ -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 {

View File

@ -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
View 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
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {