diff --git a/pkg/strategy/xmaker/delayedhedge.go b/pkg/strategy/xmaker/delayedhedge.go new file mode 100644 index 000000000..25f14e012 --- /dev/null +++ b/pkg/strategy/xmaker/delayedhedge.go @@ -0,0 +1,23 @@ +package xmaker + +import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +type DelayedHedge struct { + // EnableDelayHedge enables the delay hedge feature + Enabled bool `json:"enabled"` + + // MaxDelayDuration is the maximum delay duration to hedge the position + MaxDelayDuration types.Duration `json:"maxDelay"` + + // FixedDelayDuration is the fixed delay duration + FixedDelayDuration types.Duration `json:"fixedDelay"` + + // SignalThreshold is the signal threshold to trigger the delay hedge + SignalThreshold float64 `json:"signalThreshold"` + + // DynamicDelayScale is the dynamic delay scale + DynamicDelayScale *bbgo.SlideRule `json:"dynamicDelayScale,omitempty"` +} diff --git a/pkg/strategy/xmaker/metrics.go b/pkg/strategy/xmaker/metrics.go index 942f1933c..3662fd43e 100644 --- a/pkg/strategy/xmaker/metrics.go +++ b/pkg/strategy/xmaker/metrics.go @@ -78,6 +78,24 @@ var netProfitMarginHistogram = prometheus.NewHistogramVec( Buckets: prometheus.ExponentialBuckets(0.001, 2.0, 10), }, []string{"strategy_type", "strategy_id", "exchange", "symbol"}) +var spreadMakerCounterMetrics = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "xmaker_spread_maker_counter", + Help: "spread maker counter", + }, []string{"strategy_type", "strategy_id", "exchange", "symbol"}) + +var spreadMakerVolumeMetrics = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "xmaker_spread_maker_volume", + Help: "spread maker volume", + }, []string{"strategy_type", "strategy_id", "exchange", "symbol"}) + +var spreadMakerQuoteVolumeMetrics = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "xmaker_spread_maker_quote_volume", + Help: "spread maker quote volume", + }, []string{"strategy_type", "strategy_id", "exchange", "symbol"}) + func init() { prometheus.MustRegister( openOrderBidExposureInUsdMetrics, @@ -92,5 +110,9 @@ func init() { delayedHedgeCounterMetrics, delayedHedgeMaxDurationMetrics, netProfitMarginHistogram, + + spreadMakerCounterMetrics, + spreadMakerVolumeMetrics, + spreadMakerQuoteVolumeMetrics, ) } diff --git a/pkg/strategy/xmaker/spreadmaker.go b/pkg/strategy/xmaker/spreadmaker.go new file mode 100644 index 000000000..f1f5aebd9 --- /dev/null +++ b/pkg/strategy/xmaker/spreadmaker.go @@ -0,0 +1,209 @@ +package xmaker + +import ( + "context" + "math" + "sync" + "time" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/core" + "github.com/c9s/bbgo/pkg/exchange/retry" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type SpreadMaker struct { + Enabled bool `json:"enabled"` + + MinProfitRatio fixedpoint.Value `json:"minProfitRatio"` + MaxQuoteAmount fixedpoint.Value `json:"maxQuoteAmount"` + MaxOrderLifespan types.Duration `json:"maxOrderLifespan"` + + SignalThreshold float64 `json:"signalThreshold"` + + ReverseSignalOrderCancel bool `json:"reverseSignalOrderCancel"` + + MakerOnly bool `json:"makerOnly"` + + // order is the current spread maker order on the maker exchange + order *types.Order + + // orderStore stores the history maker orders + orderStore *core.OrderStore + + session *bbgo.ExchangeSession + + market types.Market + + orderQueryService types.ExchangeOrderQueryService + + symbol string + + mu sync.Mutex +} + +func (c *SpreadMaker) Defaults() error { + if c.MinProfitRatio.IsZero() { + c.MinProfitRatio = fixedpoint.NewFromFloat(0.01 * 0.01) + } + + if c.MaxQuoteAmount.IsZero() { + c.MaxQuoteAmount = fixedpoint.NewFromFloat(100) + } + + if c.MaxOrderLifespan == 0 { + c.MaxOrderLifespan = types.Duration(2 * time.Second) + } + + return nil +} + +func (c *SpreadMaker) updateOrder(ctx context.Context) (*types.Order, error) { + c.mu.Lock() + defer c.mu.Unlock() + + retOrder, err := c.orderQueryService.QueryOrder(ctx, c.order.AsQuery()) + if err != nil { + return nil, err + } + + c.order = retOrder + return retOrder, nil +} + +func (c *SpreadMaker) canSpreadMaking( + signal float64, position *types.Position, + makerMarket types.Market, + bestBidPrice, bestAskPrice fixedpoint.Value, // maker best bid price +) (*types.SubmitOrder, bool) { + side := position.Side() + + if !isSignalSidePosition(signal, side) { + return nil, false + } + + if math.Abs(signal) < c.SignalThreshold { + return nil, false + } + + base := position.GetBase() + cost := position.GetAverageCost() + profitPrice := getPositionProfitPrice(side, cost, c.session.MakerFeeRate.Add(c.MinProfitRatio)) + + maxQuantity := c.MaxQuoteAmount.Div(cost) + orderQuantity := base.Abs() + orderQuantity = fixedpoint.Min(orderQuantity, maxQuantity) + orderSide := side.Reverse() + + switch orderSide { + case types.SideTypeSell: + targetPrice := bestBidPrice.Add(makerMarket.TickSize) + targetPrice = fixedpoint.Max(profitPrice, targetPrice) + return c.newMakerOrder(makerMarket, orderSide, targetPrice, orderQuantity), true + + case types.SideTypeBuy: + targetPrice := bestAskPrice.Sub(makerMarket.TickSize) + targetPrice = fixedpoint.Min(profitPrice, targetPrice) + return c.newMakerOrder(makerMarket, orderSide, targetPrice, orderQuantity), true + } + + return nil, false +} + +func (c *SpreadMaker) newMakerOrder( + market types.Market, + side types.SideType, + targetPrice, orderQuantity fixedpoint.Value, +) *types.SubmitOrder { + orderType := types.OrderTypeLimit + if c.MakerOnly { + orderType = types.OrderTypeLimitMaker + } + + return &types.SubmitOrder{ + // ClientOrderID: "", + Symbol: c.symbol, + Side: side, + Type: orderType, + Price: targetPrice, + Quantity: orderQuantity, + Market: market, + TimeInForce: types.TimeInForceGTC, + } +} + +func (c *SpreadMaker) getOrder() (o types.Order, ok bool) { + c.mu.Lock() + if c.order != nil { + o = *c.order + ok = true + } + c.mu.Unlock() + return o, ok +} + +func (c *SpreadMaker) cancelOrder(ctx context.Context) error { + if order, ok := c.getOrder(); ok { + return retry.CancelOrdersUntilSuccessful(ctx, c.session.Exchange, order) + } + + return nil +} + +// cancelAndQueryOrder cancels the current order and queries the order status until the order is canceled +func (c *SpreadMaker) cancelAndQueryOrder(ctx context.Context) (*types.Order, error) { + if c.order == nil { + return nil, nil + } + + if err := c.cancelOrder(ctx); err != nil { + return nil, err + } + + c.mu.Lock() + order := c.order + c.order = nil + c.mu.Unlock() + + finalOrder, err := retry.QueryOrderUntilCanceled(ctx, c.orderQueryService, order.Symbol, order.OrderID) + if err != nil { + return nil, err + } + + return finalOrder, nil +} + +func (c *SpreadMaker) shouldKeepOrder(o types.Order, now time.Time) bool { + creationTime := o.CreationTime.Time() + if creationTime.IsZero() { + return false + } + + if creationTime.Add(c.MaxOrderLifespan.Duration()).Before(now) { + return true + } + + return false +} + +func (c *SpreadMaker) placeOrder(ctx context.Context, submitOrder *types.SubmitOrder) (*types.Order, error) { + createdOrder, err := c.session.Exchange.SubmitOrder(ctx, *submitOrder) + if err != nil { + return nil, err + } + + c.mu.Lock() + c.order = createdOrder + c.mu.Unlock() + return createdOrder, nil +} + +func (c *SpreadMaker) Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error { + c.symbol = symbol + c.orderStore = core.NewOrderStore(symbol) + c.session = session + c.market, _ = c.session.Market(symbol) + c.orderQueryService = c.session.Exchange.(types.ExchangeOrderQueryService) + return nil +} diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 6f4744ff5..9fb849268 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -121,23 +121,6 @@ type SignalMargin struct { Threshold float64 `json:"threshold,omitempty"` } -type DelayedHedge struct { - // EnableDelayHedge enables the delay hedge feature - Enabled bool `json:"enabled"` - - // MaxDelayDuration is the maximum delay duration to hedge the position - MaxDelayDuration types.Duration `json:"maxDelay"` - - // FixedDelayDuration is the fixed delay duration - FixedDelayDuration types.Duration `json:"fixedDelay"` - - // SignalThreshold is the signal threshold to trigger the delay hedge - SignalThreshold float64 `json:"signalThreshold"` - - // DynamicDelayScale is the dynamic delay scale - DynamicDelayScale *bbgo.SlideRule `json:"dynamicDelayScale,omitempty"` -} - type Strategy struct { Environment *bbgo.Environment @@ -172,6 +155,7 @@ type Strategy struct { UseDepthPrice bool `json:"useDepthPrice"` DepthQuantity fixedpoint.Value `json:"depthQuantity"` SourceDepthLevel types.Depth `json:"sourceDepthLevel"` + MakerOnly bool `json:"makerOnly"` // EnableDelayHedge enables the delay hedge feature EnableDelayHedge bool `json:"enableDelayHedge"` @@ -181,6 +165,8 @@ type Strategy struct { DelayedHedge *DelayedHedge `json:"delayedHedge,omitempty"` + SpreadMaker *SpreadMaker `json:"spreadMaker,omitempty"` + EnableBollBandMargin bool `json:"enableBollBandMargin"` BollBandInterval types.Interval `json:"bollBandInterval"` BollBandMargin fixedpoint.Value `json:"bollBandMargin"` @@ -692,13 +678,35 @@ func (s *Strategy) getLayerPrice( return price } -// margin level = totalValue / totalDebtValue -func calculateDebtQuota(totalValue, debtValue, minMarginLevel fixedpoint.Value) fixedpoint.Value { +// margin level = totalValue / totalDebtValue * MMR (maintenance margin ratio) +// on binance: +// - MMR with 10x leverage = 5% +// - MMR with 5x leverage = 9% +// - MMR with 3x leverage = 10% +func calculateDebtQuota(totalValue, debtValue, minMarginLevel, leverage fixedpoint.Value) fixedpoint.Value { if minMarginLevel.IsZero() || totalValue.IsZero() { return fixedpoint.Zero } - debtCap := totalValue.Div(minMarginLevel) + defaultMmr := fixedpoint.NewFromFloat(9.0 * 0.01) + if leverage.Compare(fixedpoint.NewFromFloat(10.0)) >= 0 { + defaultMmr = fixedpoint.NewFromFloat(5.0 * 0.01) // 5% + } else if leverage.Compare(fixedpoint.NewFromFloat(5.0)) >= 0 { + defaultMmr = fixedpoint.NewFromFloat(9.0 * 0.01) // 9% + } else if leverage.Compare(fixedpoint.NewFromFloat(3.0)) >= 0 { + defaultMmr = fixedpoint.NewFromFloat(10.0 * 0.01) // 10% + } + + debtCap := totalValue.Div(minMarginLevel).Div(defaultMmr) + marginLevel := totalValue.Div(debtValue).Div(defaultMmr) + + log.Infof("calculateDebtQuota: debtCap=%f, debtValue=%f currentMarginLevel=%f mmr=%f", + debtCap.Float64(), + debtValue.Float64(), + marginLevel.Float64(), + defaultMmr.Float64(), + ) + debtQuota := debtCap.Sub(debtValue) if debtQuota.Sign() < 0 { return fixedpoint.Zero @@ -707,7 +715,7 @@ func calculateDebtQuota(totalValue, debtValue, minMarginLevel fixedpoint.Value) return debtQuota } -func (s *Strategy) allowMarginHedge(side types.SideType) (bool, fixedpoint.Value) { +func (s *Strategy) allowMarginHedge(makerSide types.SideType) (bool, fixedpoint.Value) { zero := fixedpoint.Zero if !s.sourceSession.Margin { @@ -726,15 +734,22 @@ func (s *Strategy) allowMarginHedge(side types.SideType) (bool, fixedpoint.Value marketValue := s.accountValueCalculator.MarketValue() debtValue := s.accountValueCalculator.DebtValue() netValueInUsd := s.accountValueCalculator.NetValue() - s.logger.Infof("hedge account net value in usd: %f, debt value in usd: %f", + s.logger.Infof("hedge account net value in usd: %f, debt value in usd: %f, total value in usd: %f", netValueInUsd.Float64(), - debtValue.Float64()) + debtValue.Float64(), + marketValue.Float64(), + ) // if the margin level is higher than the minimal margin level, // we can hedge the position, but we need to check the debt quota if hedgeAccount.MarginLevel.Compare(s.MinMarginLevel) > 0 { + // debtQuota is the quota with minimal margin level - debtQuota := calculateDebtQuota(marketValue, debtValue, bufMinMarginLevel) + debtQuota := calculateDebtQuota(marketValue, debtValue, bufMinMarginLevel, s.MaxHedgeAccountLeverage) + + s.logger.Infof("hedge account margin level %f > %f, debt quota: %f", + hedgeAccount.MarginLevel.Float64(), s.MinMarginLevel.Float64(), debtQuota.Float64()) + if debtQuota.Sign() <= 0 { return false, zero } @@ -752,7 +767,7 @@ func (s *Strategy) allowMarginHedge(side types.SideType) (bool, fixedpoint.Value debtQuota = fixedpoint.Min(debtQuota, leverageQuotaInUsd) } - switch side { + switch makerSide { case types.SideTypeBuy: return true, debtQuota @@ -767,7 +782,7 @@ func (s *Strategy) allowMarginHedge(side types.SideType) (bool, fixedpoint.Value return true, zero } - // side here is the side of maker + // makerSide here is the makerSide of maker // if the margin level is too low, check if we can hedge the position with repayments to reduce the position quoteBal, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency) if !ok { @@ -779,7 +794,7 @@ func (s *Strategy) allowMarginHedge(side types.SideType) (bool, fixedpoint.Value baseBal = types.NewZeroBalance(s.sourceMarket.BaseCurrency) } - switch side { + switch makerSide { case types.SideTypeBuy: if baseBal.Available.IsZero() { return false, zero @@ -864,7 +879,7 @@ func (s *Strategy) updateQuote(ctx context.Context) error { bestBid, bestAsk, hasPrice := s.sourceBook.BestBidAndAsk() if !hasPrice { s.logger.Warnf("no valid price, skip quoting") - return fmt.Errorf("no valid book price") + return nil } bestBidPrice := bestBid.Price @@ -968,7 +983,7 @@ func (s *Strategy) updateQuote(ctx context.Context) error { !hedgeAccount.MarginLevel.IsZero() { if hedgeAccount.MarginLevel.Compare(s.MinMarginLevel) < 0 { - s.logger.Infof("hedge account margin level %s is less then the min margin level %s, calculating the borrowed positions", + s.logger.Warnf("hedge account margin level %s is less then the min margin level %s, calculating the borrowed positions", hedgeAccount.MarginLevel.String(), s.MinMarginLevel.String()) } else { @@ -977,17 +992,19 @@ func (s *Strategy) updateQuote(ctx context.Context) error { s.MinMarginLevel.String()) } - allowMarginBuy, bidQuota := s.allowMarginHedge(types.SideTypeBuy) - if allowMarginBuy { + allowMarginSell, bidQuota := s.allowMarginHedge(types.SideTypeBuy) + if allowMarginSell { hedgeQuota.BaseAsset.Add(bidQuota.Div(bestBid.Price)) } else { + s.logger.Warnf("margin hedge sell is disabled, disabling maker bid orders...") disableMakerBid = true } - allowMarginSell, sellQuota := s.allowMarginHedge(types.SideTypeSell) - if allowMarginSell { + allowMarginBuy, sellQuota := s.allowMarginHedge(types.SideTypeSell) + if allowMarginBuy { hedgeQuota.QuoteAsset.Add(sellQuota.Mul(bestAsk.Price)) } else { + s.logger.Warnf("margin hedge buy is disabled, disabling maker ask orders...") disableMakerAsk = true } } else { @@ -1052,6 +1069,11 @@ func (s *Strategy) updateQuote(ctx context.Context) error { return nil } + var orderType = types.OrderTypeLimit + if s.MakerOnly { + orderType = types.OrderTypeLimitMaker + } + var submitOrders []types.SubmitOrder var accumulativeBidQuantity, accumulativeAskQuantity fixedpoint.Value @@ -1137,7 +1159,7 @@ func (s *Strategy) updateQuote(ctx context.Context) error { submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, Market: s.makerMarket, - Type: types.OrderTypeLimit, + Type: orderType, Side: types.SideTypeBuy, Price: bidPrice, Quantity: bidQuantity, @@ -1196,7 +1218,7 @@ func (s *Strategy) updateQuote(ctx context.Context) error { submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, Market: s.makerMarket, - Type: types.OrderTypeLimit, + Type: orderType, Side: types.SideTypeSell, Price: askPrice, Quantity: askQuantity, @@ -1425,7 +1447,6 @@ func (s *Strategy) canDelayHedge(hedgeSide types.SideType, pos fixedpoint.Value) } signal := s.lastAggregatedSignal.Get() - signalAbs := math.Abs(signal) if signalAbs < s.DelayedHedge.SignalThreshold { return false @@ -1463,23 +1484,104 @@ func (s *Strategy) canDelayHedge(hedgeSide types.SideType, pos fixedpoint.Value) return false } +func (s *Strategy) cancelSpreadMakerOrderAndReturnCoveredPos(ctx context.Context, coveredPosition *fixedpoint.MutexValue) { + s.logger.Infof("canceling current spread maker order...") + + finalOrder, err := s.SpreadMaker.cancelAndQueryOrder(ctx) + if err != nil { + s.logger.WithError(err).Errorf("spread maker: cancel order error") + } + + if finalOrder != nil { + spreadMakerVolumeMetrics.With(s.metricsLabels).Add(finalOrder.ExecutedQuantity.Float64()) + spreadMakerQuoteVolumeMetrics.With(s.metricsLabels).Add(finalOrder.ExecutedQuantity.Mul(finalOrder.Price).Float64()) + + remainingQuantity := finalOrder.GetRemainingQuantity() + + s.logger.Infof("returning remaining quantity %f to the covered position", remainingQuantity.Float64()) + switch finalOrder.Side { + case types.SideTypeSell: + coveredPosition.Sub(remainingQuantity) + case types.SideTypeBuy: + coveredPosition.Add(remainingQuantity) + + } + } +} + func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { - side := types.SideTypeBuy if pos.IsZero() { return } - quantity := pos.Abs() - + side := types.SideTypeBuy if pos.Sign() < 0 { side = types.SideTypeSell } + now := time.Now() + signal := s.lastAggregatedSignal.Get() + + if s.SpreadMaker != nil && s.SpreadMaker.Enabled && s.makerBook != nil { + if makerBid, makerAsk, hasMakerPrice := s.makerBook.BestBidAndAsk(); hasMakerPrice { + if makerOrderForm, ok := s.SpreadMaker.canSpreadMaking(signal, s.Position, s.makerMarket, makerBid.Price, makerAsk.Price); ok { + + s.logger.Infof("position: %f@%f, maker book bid: %f/%f, spread maker order form: %+v", + s.Position.GetBase().Float64(), + s.Position.GetAverageCost().Float64(), + makerAsk.Price.Float64(), + makerBid.Price.Float64(), + makerOrderForm) + + // if we have the existing order, cancel it and return the covered position + // keptOrder means we kept the current order and we don't need to place a new order + keptOrder := false + curOrder, hasOrder := s.SpreadMaker.getOrder() + if hasOrder { + keptOrder = s.SpreadMaker.shouldKeepOrder(curOrder, now) + if !keptOrder { + s.logger.Infof("canceling current spread maker order...") + s.cancelSpreadMakerOrderAndReturnCoveredPos(ctx, &s.coveredPosition) + } + } + + if !hasOrder || !keptOrder { + spreadMakerCounterMetrics.With(s.metricsLabels).Inc() + s.logger.Infof("placing new spread maker order: %+v...", makerOrderForm) + + retOrder, err := s.SpreadMaker.placeOrder(ctx, makerOrderForm) + if err != nil { + s.logger.WithError(err).Errorf("unable to place spread maker order") + } else if retOrder != nil { + s.orderStore.Add(*retOrder) + + s.logger.Infof("spread maker order placed: #%d %f@%f (%s)", retOrder.OrderID, retOrder.Quantity.Float64(), retOrder.Price.Float64(), retOrder.Status) + + // add covered position from the created order + switch side { + case types.SideTypeSell: + s.coveredPosition.Add(retOrder.Quantity) + pos = pos.Add(retOrder.Quantity) + case types.SideTypeBuy: + s.coveredPosition.Sub(retOrder.Quantity) + pos = pos.Sub(retOrder.Quantity) + } + } + } + } else if s.SpreadMaker.ReverseSignalOrderCancel { + if _, hasOrder := s.SpreadMaker.getOrder(); hasOrder { + s.cancelSpreadMakerOrderAndReturnCoveredPos(ctx, &s.coveredPosition) + } + } + } + } + if s.canDelayHedge(side, pos) { return } lastPrice := s.lastPrice.Get() + quantity := pos.Abs() bestBid, bestAsk, ok := s.sourceBook.BestBidAndAsk() if ok { @@ -1511,7 +1613,7 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { } if s.sourceMarket.IsDustQuantity(quantity, lastPrice) { - s.logger.Warnf("skip dust quantity: %s @ price %f", quantity.String(), lastPrice.Float64()) + s.logger.Infof("skip dust quantity: %s @ price %f", quantity.String(), lastPrice.Float64()) return } @@ -1560,9 +1662,10 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { log.Infof("submitted hedge orders: %+v", createdOrders) // if it's selling, then we should add a positive position - if side == types.SideTypeSell { + switch side { + case types.SideTypeSell: s.coveredPosition.Add(quantity) - } else { + case types.SideTypeBuy: s.coveredPosition.Add(quantity.Neg()) } @@ -2034,6 +2137,12 @@ func (s *Strategy) CrossRun( s.ProfitStats.ProfitStats = profitStats } + if s.SpreadMaker != nil && s.SpreadMaker.Enabled { + if err := s.SpreadMaker.Bind(ctx, s.makerSession, s.Symbol); err != nil { + return err + } + } + if s.EnableArbitrage { makerMarketStream := s.makerSession.Exchange.NewStream() makerMarketStream.SetPublicOnly() @@ -2233,3 +2342,29 @@ func (s *Strategy) CrossRun( return nil } + +func isSignalSidePosition(signal float64, side types.SideType) bool { + switch side { + case types.SideTypeBuy: + return signal > 0 + + case types.SideTypeSell: + return signal < 0 + + } + + return false +} + +func getPositionProfitPrice(side types.SideType, cost, profitRatio fixedpoint.Value) fixedpoint.Value { + switch side { + case types.SideTypeBuy: + return cost.Mul(profitRatio.Add(fixedpoint.One)) + + case types.SideTypeSell: + return cost.Mul(fixedpoint.One.Sub(profitRatio)) + + } + + return cost +} diff --git a/pkg/strategy/xmaker/strategy_test.go b/pkg/strategy/xmaker/strategy_test.go index 0a067debf..1c6265e98 100644 --- a/pkg/strategy/xmaker/strategy_test.go +++ b/pkg/strategy/xmaker/strategy_test.go @@ -68,12 +68,12 @@ func TestStrategy_allowMarginHedge(t *testing.T) { allowed, quota := s.allowMarginHedge(types.SideTypeBuy) if assert.True(t, allowed) { - assert.InDelta(t, 133782.26785814, quota.Float64(), 1.0, "should be able to borrow %f USDT", quota.Float64()) + assert.InDelta(t, 2.47735853175711e+06, quota.Float64(), 0.0001, "should be able to borrow %f USDT", quota.Float64()) } allowed, quota = s.allowMarginHedge(types.SideTypeSell) if assert.True(t, allowed) { - assert.InDelta(t, 1.36512518, quota.Float64(), 0.0001, "should be able to borrow %f BTC", quota.Float64()) + assert.InDelta(t, 25.27916869, quota.Float64(), 0.0001, "should be able to borrow %f BTC", quota.Float64()) } }) diff --git a/pkg/types/order.go b/pkg/types/order.go index 183968819..7b977e5ff 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -290,6 +290,17 @@ type Order struct { IsIsolated bool `json:"isIsolated,omitempty" db:"is_isolated"` } +func (o *Order) GetRemainingQuantity() fixedpoint.Value { + return o.Quantity.Sub(o.ExecutedQuantity) +} + +func (o Order) AsQuery() OrderQuery { + return OrderQuery{ + Symbol: o.Symbol, + OrderID: strconv.FormatUint(o.OrderID, 10), + } +} + func (o Order) CsvHeader() []string { return []string{ "order_id", diff --git a/pkg/types/position.go b/pkg/types/position.go index 26dc8bb36..864499a0c 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -202,6 +202,13 @@ func (p *Position) IsDust(a ...fixedpoint.Value) bool { return p.Market.IsDustQuantity(base, price) } +func (p *Position) GetAverageCost() (averageCost fixedpoint.Value) { + p.Lock() + averageCost = p.AverageCost + p.Unlock() + return averageCost +} + // GetBase locks the mutex and return the base quantity // The base quantity can be negative func (p *Position) GetBase() (base fixedpoint.Value) { @@ -377,6 +384,16 @@ func (p *Position) Type() PositionType { return PositionClosed } +func (p *Position) Side() SideType { + if p.Base.Sign() > 0 { + return SideTypeBuy + } else if p.Base.Sign() < 0 { + return SideTypeSell + } + + return SideTypeNone +} + func (p *Position) SlackAttachment() slack.Attachment { p.Lock() defer p.Unlock() diff --git a/pkg/types/side.go b/pkg/types/side.go index 75e7a9ae1..80b063c09 100644 --- a/pkg/types/side.go +++ b/pkg/types/side.go @@ -19,6 +19,7 @@ const ( // SideTypeBoth is only used for the configuration context SideTypeBoth = SideType("BOTH") + SideTypeNone = SideType("") ) var ErrInvalidSideType = errors.New("invalid side type")