mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +00:00
Merge pull request #1720 from c9s/c9s/xmaker/margin-credit-improvement
FEATURE: [xmaker] margin credit improvement
This commit is contained in:
commit
bda66040e7
|
@ -63,6 +63,9 @@ type Strategy struct {
|
||||||
BollBandMargin fixedpoint.Value `json:"bollBandMargin"`
|
BollBandMargin fixedpoint.Value `json:"bollBandMargin"`
|
||||||
BollBandMarginFactor fixedpoint.Value `json:"bollBandMarginFactor"`
|
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"`
|
StopHedgeQuoteBalance fixedpoint.Value `json:"stopHedgeQuoteBalance"`
|
||||||
StopHedgeBaseBalance fixedpoint.Value `json:"stopHedgeBaseBalance"`
|
StopHedgeBaseBalance fixedpoint.Value `json:"stopHedgeBaseBalance"`
|
||||||
|
|
||||||
|
@ -126,6 +129,8 @@ type Strategy struct {
|
||||||
|
|
||||||
askPriceHeartBeat, bidPriceHeartBeat *types.PriceHeartBeat
|
askPriceHeartBeat, bidPriceHeartBeat *types.PriceHeartBeat
|
||||||
|
|
||||||
|
accountValueCalculator *bbgo.AccountValueCalculator
|
||||||
|
|
||||||
lastPrice fixedpoint.Value
|
lastPrice fixedpoint.Value
|
||||||
groupID uint32
|
groupID uint32
|
||||||
|
|
||||||
|
@ -190,7 +195,6 @@ func aggregatePrice(pvs types.PriceVolumeSlice, requiredQuantity fixedpoint.Valu
|
||||||
func (s *Strategy) Initialize() error {
|
func (s *Strategy) Initialize() error {
|
||||||
s.bidPriceHeartBeat = types.NewPriceHeartBeat(priceUpdateTimeout)
|
s.bidPriceHeartBeat = types.NewPriceHeartBeat(priceUpdateTimeout)
|
||||||
s.askPriceHeartBeat = types.NewPriceHeartBeat(priceUpdateTimeout)
|
s.askPriceHeartBeat = types.NewPriceHeartBeat(priceUpdateTimeout)
|
||||||
|
|
||||||
s.logger = logrus.WithFields(logrus.Fields{
|
s.logger = logrus.WithFields(logrus.Fields{
|
||||||
"symbol": s.Symbol,
|
"symbol": s.Symbol,
|
||||||
"strategy": ID,
|
"strategy": ID,
|
||||||
|
@ -269,7 +273,7 @@ func (s *Strategy) applyBollingerMargin(
|
||||||
// so that the original bid margin can be multiplied by 1.x
|
// so that the original bid margin can be multiplied by 1.x
|
||||||
bollMargin := s.BollBandMargin.Mul(ratio).Mul(factor)
|
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,
|
s.Symbol,
|
||||||
quote.AskMargin.Float64(),
|
quote.AskMargin.Float64(),
|
||||||
bollMargin.Float64(),
|
bollMargin.Float64(),
|
||||||
|
@ -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{}
|
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 s.sourceSession.Margin &&
|
||||||
// if the base asset balance is not enough for selling
|
!s.MinMarginLevel.IsZero() &&
|
||||||
if s.StopHedgeBaseBalance.Sign() > 0 {
|
!hedgeAccount.MarginLevel.IsZero() {
|
||||||
minAvailable := s.StopHedgeBaseBalance.Add(s.sourceMarket.MinQuantity)
|
|
||||||
if b.Available.Compare(minAvailable) > 0 {
|
if hedgeAccount.MarginLevel.Compare(s.MinMarginLevel) < 0 {
|
||||||
hedgeQuota.BaseAsset.Add(b.Available.Sub(minAvailable))
|
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.QuoteAsset.Add(netQuote.Mul(creditBufferRatio))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok {
|
||||||
|
netBase := base.Net()
|
||||||
|
if netBase.Sign() > 0 {
|
||||||
|
hedgeQuota.BaseAsset.Add(netBase.Mul(creditBufferRatio))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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 {
|
} else {
|
||||||
s.logger.Warnf("%s maker bid disabled: insufficient base balance %s", s.Symbol, b.String())
|
s.logger.Warnf("%s maker bid disabled: insufficient base balance %s", s.Symbol, b.String())
|
||||||
disableMakerBid = true
|
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 {
|
if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok {
|
||||||
// to make ask orders, we need enough quote asset in the foreign exchange,
|
// to make ask orders, we need enough quote asset in the foreign exchange,
|
||||||
// if the quote asset balance is not enough for buying
|
// if the quote asset balance is not enough for buying
|
||||||
if s.StopHedgeQuoteBalance.Sign() > 0 {
|
if s.StopHedgeQuoteBalance.Sign() > 0 {
|
||||||
minAvailable := s.StopHedgeQuoteBalance.Add(s.sourceMarket.MinNotional)
|
minAvailable := s.StopHedgeQuoteBalance.Add(s.sourceMarket.MinNotional)
|
||||||
if b.Available.Compare(minAvailable) > 0 {
|
if b.Available.Compare(minAvailable) > 0 {
|
||||||
hedgeQuota.QuoteAsset.Add(b.Available.Sub(minAvailable))
|
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 {
|
} else {
|
||||||
s.logger.Warnf("%s maker ask disabled: insufficient quote balance %s", s.Symbol, b.String())
|
s.logger.Warnf("%s maker ask disabled: insufficient quote balance %s", s.Symbol, b.String())
|
||||||
disableMakerAsk = true
|
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:
|
// if max exposure position is configured, we should not:
|
||||||
|
@ -650,6 +700,35 @@ func (s *Strategy) updateQuote(ctx context.Context) {
|
||||||
_ = createdOrders
|
_ = 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) {
|
func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
|
||||||
side := types.SideTypeBuy
|
side := types.SideTypeBuy
|
||||||
if pos.IsZero() {
|
if pos.IsZero() {
|
||||||
|
@ -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()
|
account := s.sourceSession.GetAccount()
|
||||||
switch side {
|
if s.sourceSession.Margin {
|
||||||
|
// check the margin level
|
||||||
case types.SideTypeBuy:
|
if !s.MinMarginLevel.IsZero() && !account.MarginLevel.IsZero() && account.MarginLevel.Compare(s.MinMarginLevel) < 0 {
|
||||||
// check quote quantity
|
log.Errorf("margin level %f is too low (< %f), skip hedge", account.MarginLevel.Float64(), s.MinMarginLevel.Float64())
|
||||||
if quote, ok := account.Balance(s.sourceMarket.QuoteCurrency); ok {
|
return
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
quantity = s.adjustHedgeQuantityWithAvailableBalance(account, side, quantity, lastPrice)
|
||||||
}
|
}
|
||||||
|
|
||||||
// truncate quantity for the supported precision
|
// truncate quantity for the supported precision
|
||||||
quantity = s.sourceMarket.TruncateQuantity(quantity)
|
quantity = s.sourceMarket.TruncateQuantity(quantity)
|
||||||
|
|
||||||
if s.sourceMarket.IsDustQuantity(quantity, lastPrice) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -821,6 +886,10 @@ func (s *Strategy) Defaults() error {
|
||||||
s.NumLayers = 1
|
s.NumLayers = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.MinMarginLevel.IsZero() {
|
||||||
|
s.MinMarginLevel = fixedpoint.NewFromFloat(3.0)
|
||||||
|
}
|
||||||
|
|
||||||
if s.BidMargin.IsZero() {
|
if s.BidMargin.IsZero() {
|
||||||
if !s.Margin.IsZero() {
|
if !s.Margin.IsZero() {
|
||||||
s.BidMargin = s.Margin
|
s.BidMargin = s.Margin
|
||||||
|
@ -892,6 +961,35 @@ func (s *Strategy) quoteWorker(ctx context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) accountUpdater(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(3 * 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) {
|
func (s *Strategy) hedgeWorker(ctx context.Context) {
|
||||||
ticker := time.NewTicker(util.MillisecondsJitter(s.HedgeInterval.Duration(), 200))
|
ticker := time.NewTicker(util.MillisecondsJitter(s.HedgeInterval.Duration(), 200))
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
@ -980,6 +1078,8 @@ func (s *Strategy) CrossRun(
|
||||||
return fmt.Errorf("maker session market %s is not defined", s.Symbol)
|
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)
|
indicators := s.sourceSession.Indicators(s.Symbol)
|
||||||
|
|
||||||
s.boll = indicators.BOLL(types.IntervalWindow{
|
s.boll = indicators.BOLL(types.IntervalWindow{
|
||||||
|
@ -1136,6 +1236,7 @@ func (s *Strategy) CrossRun(
|
||||||
go s.tradeRecover(ctx)
|
go s.tradeRecover(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go s.accountUpdater(ctx)
|
||||||
go s.hedgeWorker(ctx)
|
go s.hedgeWorker(ctx)
|
||||||
go s.quoteWorker(ctx)
|
go s.quoteWorker(ctx)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user