Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: [xmaker] margin credit improvement #1720

Merged
merged 4 commits into from
Aug 28, 2024
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 151 additions & 50 deletions pkg/strategy/xmaker/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ type Strategy struct {
BollBandMargin fixedpoint.Value `json:"bollBandMargin"`
BollBandMarginFactor fixedpoint.Value `json:"bollBandMarginFactor"`

// MinMarginLevel is the minimum margin level to trigger the hedge
MinMarginLevel fixedpoint.Value `json:"minMarginLevel"`

StopHedgeQuoteBalance fixedpoint.Value `json:"stopHedgeQuoteBalance"`
StopHedgeBaseBalance fixedpoint.Value `json:"stopHedgeBaseBalance"`

Expand Down Expand Up @@ -126,6 +129,8 @@ type Strategy struct {

askPriceHeartBeat, bidPriceHeartBeat *types.PriceHeartBeat

accountValueCalculator *bbgo.AccountValueCalculator

lastPrice fixedpoint.Value
groupID uint32

Expand Down Expand Up @@ -190,7 +195,6 @@ func aggregatePrice(pvs types.PriceVolumeSlice, requiredQuantity fixedpoint.Valu
func (s *Strategy) Initialize() error {
s.bidPriceHeartBeat = types.NewPriceHeartBeat(priceUpdateTimeout)
s.askPriceHeartBeat = types.NewPriceHeartBeat(priceUpdateTimeout)

s.logger = logrus.WithFields(logrus.Fields{
"symbol": s.Symbol,
"strategy": ID,
Expand Down Expand Up @@ -269,7 +273,7 @@ func (s *Strategy) applyBollingerMargin(
// so that the original bid margin can be multiplied by 1.x
bollMargin := s.BollBandMargin.Mul(ratio).Mul(factor)

s.logger.Infof("%s bollband uptrend adjusting bid margin %f (askMargin) + %f (bollMargin) = %f (finalAskMargin)",
s.logger.Infof("%s bollband uptrend adjusting ask margin %f (askMargin) + %f (bollMargin) = %f (finalAskMargin)",
s.Symbol,
quote.AskMargin.Float64(),
bollMargin.Float64(),
Expand Down Expand Up @@ -366,44 +370,90 @@ func (s *Strategy) updateQuote(ctx context.Context) {
}
}

hedgeBalances := s.sourceSession.GetAccount().Balances()
// if
// 1) the source session is a margin session
// 2) the min margin level is configured
// 3) the hedge account's margin level is lower than the min margin level
hedgeAccount := s.sourceSession.GetAccount()
hedgeBalances := hedgeAccount.Balances()
hedgeQuota := &bbgo.QuotaTransaction{}
if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok {
// to make bid orders, we need enough base asset in the foreign exchange,
// if the base asset balance is not enough for selling
if s.StopHedgeBaseBalance.Sign() > 0 {
minAvailable := s.StopHedgeBaseBalance.Add(s.sourceMarket.MinQuantity)
if b.Available.Compare(minAvailable) > 0 {
hedgeQuota.BaseAsset.Add(b.Available.Sub(minAvailable))

if s.sourceSession.Margin &&
!s.MinMarginLevel.IsZero() &&
!hedgeAccount.MarginLevel.IsZero() {

if hedgeAccount.MarginLevel.Compare(s.MinMarginLevel) < 0 {
if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok {
quoteDebt := quote.Debt()
if quoteDebt.Sign() > 0 {
hedgeQuota.BaseAsset.Add(quoteDebt.Div(bestBid.Price))
}
}

if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok {
baseDebt := base.Debt()
if baseDebt.Sign() > 0 {
hedgeQuota.QuoteAsset.Add(baseDebt.Mul(bestAsk.Price))
}
}
} else {
// credit buffer
creditBufferRatio := fixedpoint.NewFromFloat(1.2)
if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok {
netQuote := quote.Net()
if netQuote.Sign() > 0 {
hedgeQuota.BaseAsset.Add(netQuote.Mul(creditBufferRatio).Div(bestBid.Price))
}
}

if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok {
netBase := base.Net()
if netBase.Sign() > 0 {
hedgeQuota.QuoteAsset.Add(netBase.Mul(creditBufferRatio).Mul(bestAsk.Price))
}
}
// netValueInUsd, err := s.accountValueCalculator.NetValue(ctx)
}

} else {
if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok {
// to make bid orders, we need enough base asset in the foreign exchange,
// if the base asset balance is not enough for selling
if s.StopHedgeBaseBalance.Sign() > 0 {
minAvailable := s.StopHedgeBaseBalance.Add(s.sourceMarket.MinQuantity)
if b.Available.Compare(minAvailable) > 0 {
hedgeQuota.BaseAsset.Add(b.Available.Sub(minAvailable))
} else {
s.logger.Warnf("%s maker bid disabled: insufficient base balance %s", s.Symbol, b.String())
disableMakerBid = true
}
} else if b.Available.Compare(s.sourceMarket.MinQuantity) > 0 {
hedgeQuota.BaseAsset.Add(b.Available)
} else {
s.logger.Warnf("%s maker bid disabled: insufficient base balance %s", s.Symbol, b.String())
disableMakerBid = true
}
} else if b.Available.Compare(s.sourceMarket.MinQuantity) > 0 {
hedgeQuota.BaseAsset.Add(b.Available)
} else {
s.logger.Warnf("%s maker bid disabled: insufficient base balance %s", s.Symbol, b.String())
disableMakerBid = true
}
}

if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok {
// to make ask orders, we need enough quote asset in the foreign exchange,
// if the quote asset balance is not enough for buying
if s.StopHedgeQuoteBalance.Sign() > 0 {
minAvailable := s.StopHedgeQuoteBalance.Add(s.sourceMarket.MinNotional)
if b.Available.Compare(minAvailable) > 0 {
hedgeQuota.QuoteAsset.Add(b.Available.Sub(minAvailable))
if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok {
// to make ask orders, we need enough quote asset in the foreign exchange,
// if the quote asset balance is not enough for buying
if s.StopHedgeQuoteBalance.Sign() > 0 {
minAvailable := s.StopHedgeQuoteBalance.Add(s.sourceMarket.MinNotional)
if b.Available.Compare(minAvailable) > 0 {
hedgeQuota.QuoteAsset.Add(b.Available.Sub(minAvailable))
} else {
s.logger.Warnf("%s maker ask disabled: insufficient quote balance %s", s.Symbol, b.String())
disableMakerAsk = true
}
} else if b.Available.Compare(s.sourceMarket.MinNotional) > 0 {
hedgeQuota.QuoteAsset.Add(b.Available)
} else {
s.logger.Warnf("%s maker ask disabled: insufficient quote balance %s", s.Symbol, b.String())
disableMakerAsk = true
}
} else if b.Available.Compare(s.sourceMarket.MinNotional) > 0 {
hedgeQuota.QuoteAsset.Add(b.Available)
} else {
s.logger.Warnf("%s maker ask disabled: insufficient quote balance %s", s.Symbol, b.String())
disableMakerAsk = true
}

}

// if max exposure position is configured, we should not:
Expand Down Expand Up @@ -650,6 +700,35 @@ func (s *Strategy) updateQuote(ctx context.Context) {
_ = createdOrders
}

func (s *Strategy) adjustHedgeQuantityWithAvailableBalance(
account *types.Account, 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 {
// adjust price to higher 0.1%, so that we can ensure that the order can be executed
availableQuote := s.sourceMarket.TruncateQuoteQuantity(quote.Available)
quantity = bbgo.AdjustQuantityByMaxAmount(quantity, lastPrice, availableQuote)

}
}

case types.SideTypeSell:
// check quote quantity
if base, ok := account.Balance(s.sourceMarket.BaseCurrency); ok {
if base.Available.Compare(quantity) < 0 {
quantity = base.Available
}
}
}

// truncate the quantity to the supported precision
return s.sourceMarket.TruncateQuantity(quantity)
}

func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
side := types.SideTypeBuy
if pos.IsZero() {
Expand Down Expand Up @@ -677,36 +756,22 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
}
}

notional := quantity.Mul(lastPrice)

// adjust quantity according to the balances
account := s.sourceSession.GetAccount()
switch side {

case types.SideTypeBuy:
// check quote quantity
if quote, ok := account.Balance(s.sourceMarket.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.sourceMarket.TruncateQuantity(quantity)
}
}

case types.SideTypeSell:
// check quote quantity
if base, ok := account.Balance(s.sourceMarket.BaseCurrency); ok {
if base.Available.Compare(quantity) < 0 {
quantity = base.Available
}
if s.sourceSession.Margin {
// check the margin level
if !s.MinMarginLevel.IsZero() && !account.MarginLevel.IsZero() && account.MarginLevel.Compare(s.MinMarginLevel) < 0 {
log.Errorf("margin level %f is too low (< %f), skip hedge", account.MarginLevel.Float64(), s.MinMarginLevel.Float64())
return
}
} else {
quantity = s.adjustHedgeQuantityWithAvailableBalance(account, side, quantity, lastPrice)
}

// truncate quantity for the supported precision
quantity = s.sourceMarket.TruncateQuantity(quantity)

if s.sourceMarket.IsDustQuantity(quantity, lastPrice) {
log.Warnf("skip dust quantity: %s", quantity.String())
log.Warnf("skip dust quantity: %s @ price %f", quantity.String(), lastPrice.Float64())
return
}

Expand Down Expand Up @@ -821,6 +886,10 @@ func (s *Strategy) Defaults() error {
s.NumLayers = 1
}

if s.MinMarginLevel.IsZero() {
s.MinMarginLevel = fixedpoint.NewFromFloat(3.0)
}

if s.BidMargin.IsZero() {
if !s.Margin.IsZero() {
s.BidMargin = s.Margin
Expand Down Expand Up @@ -892,6 +961,35 @@ func (s *Strategy) quoteWorker(ctx context.Context) {
}
}

func (s *Strategy) accountUpdater(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return

case <-ticker.C:
if _, err := s.sourceSession.UpdateAccount(ctx); err != nil {
log.WithError(err).Errorf("unable to update account")
}

if err := s.accountValueCalculator.UpdatePrices(ctx); err != nil {
log.WithError(err).Errorf("unable to update account value with prices")
return
}

netValue, err := s.accountValueCalculator.NetValue(ctx)
if err != nil {
log.WithError(err).Errorf("unable to update account")
return
}

s.logger.Infof("hedge session net value ~= %f USD", netValue.Float64())
}
}
}

func (s *Strategy) hedgeWorker(ctx context.Context) {
ticker := time.NewTicker(util.MillisecondsJitter(s.HedgeInterval.Duration(), 200))
defer ticker.Stop()
Expand Down Expand Up @@ -980,6 +1078,8 @@ func (s *Strategy) CrossRun(
return fmt.Errorf("maker session market %s is not defined", s.Symbol)
}

s.accountValueCalculator = bbgo.NewAccountValueCalculator(s.sourceSession, s.sourceMarket.QuoteCurrency)

indicators := s.sourceSession.Indicators(s.Symbol)

s.boll = indicators.BOLL(types.IntervalWindow{
Expand Down Expand Up @@ -1136,6 +1236,7 @@ func (s *Strategy) CrossRun(
go s.tradeRecover(ctx)
}

go s.accountUpdater(ctx)
go s.hedgeWorker(ctx)
go s.quoteWorker(ctx)

Expand Down
Loading