mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #1734 from c9s/c9s/xmaker/ioc-arb
REFACTOR: [xmaker] refactor for supporting ioc arb [part1]
This commit is contained in:
commit
f361b19564
|
@ -96,6 +96,7 @@ type Strategy struct {
|
||||||
AskMargin fixedpoint.Value `json:"askMargin"`
|
AskMargin fixedpoint.Value `json:"askMargin"`
|
||||||
UseDepthPrice bool `json:"useDepthPrice"`
|
UseDepthPrice bool `json:"useDepthPrice"`
|
||||||
DepthQuantity fixedpoint.Value `json:"depthQuantity"`
|
DepthQuantity fixedpoint.Value `json:"depthQuantity"`
|
||||||
|
SourceDepthLevel types.Depth `json:"sourceDepthLevel"`
|
||||||
|
|
||||||
EnableBollBandMargin bool `json:"enableBollBandMargin"`
|
EnableBollBandMargin bool `json:"enableBollBandMargin"`
|
||||||
BollBandInterval types.Interval `json:"bollBandInterval"`
|
BollBandInterval types.Interval `json:"bollBandInterval"`
|
||||||
|
@ -126,6 +127,8 @@ type Strategy struct {
|
||||||
|
|
||||||
NotifyTrade bool `json:"notifyTrade"`
|
NotifyTrade bool `json:"notifyTrade"`
|
||||||
|
|
||||||
|
EnableArbitrage bool `json:"arbitrage"`
|
||||||
|
|
||||||
// RecoverTrade tries to find the missing trades via the REStful API
|
// RecoverTrade tries to find the missing trades via the REStful API
|
||||||
RecoverTrade bool `json:"recoverTrade"`
|
RecoverTrade bool `json:"recoverTrade"`
|
||||||
|
|
||||||
|
@ -159,7 +162,7 @@ type Strategy struct {
|
||||||
ProfitStats *ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
|
ProfitStats *ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
|
||||||
CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty" persistence:"covered_position"`
|
CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty" persistence:"covered_position"`
|
||||||
|
|
||||||
book *types.StreamOrderBook
|
sourceBook, makerBook *types.StreamOrderBook
|
||||||
activeMakerOrders *bbgo.ActiveOrderBook
|
activeMakerOrders *bbgo.ActiveOrderBook
|
||||||
|
|
||||||
hedgeErrorLimiter *rate.Limiter
|
hedgeErrorLimiter *rate.Limiter
|
||||||
|
@ -199,7 +202,11 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {
|
||||||
panic(fmt.Errorf("source session %s is not defined", s.SourceExchange))
|
panic(fmt.Errorf("source session %s is not defined", s.SourceExchange))
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
|
sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{
|
||||||
|
// TODO: fix depth20 stream for binance
|
||||||
|
// Depth: s.SourceDepthLevel,
|
||||||
|
})
|
||||||
|
|
||||||
sourceSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"})
|
sourceSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"})
|
||||||
|
|
||||||
makerSession, ok := sessions[s.MakerExchange]
|
makerSession, ok := sessions[s.MakerExchange]
|
||||||
|
@ -209,9 +216,17 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {
|
||||||
|
|
||||||
makerSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"})
|
makerSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"})
|
||||||
|
|
||||||
|
if s.EnableArbitrage {
|
||||||
|
makerSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{
|
||||||
|
Depth: types.DepthLevelMedium,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for _, sig := range s.SignalConfigList {
|
for _, sig := range s.SignalConfigList {
|
||||||
if sig.TradeVolumeWindowSignal != nil {
|
if sig.TradeVolumeWindowSignal != nil {
|
||||||
sourceSession.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
|
sourceSession.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
|
||||||
|
} else if sig.BollingerBandTrendSignal != nil {
|
||||||
|
sourceSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: sig.BollingerBandTrendSignal.Interval})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -271,7 +286,7 @@ func (s *Strategy) getBollingerTrend(quote *Quote) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) applySignalMargin(ctx context.Context, quote *Quote) error {
|
func (s *Strategy) applySignalMargin(ctx context.Context, quote *Quote) error {
|
||||||
signal, err := s.calculateSignal(ctx)
|
signal, err := s.aggregateSignal(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -367,7 +382,7 @@ func (s *Strategy) applyBollingerMargin(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) calculateSignal(ctx context.Context) (float64, error) {
|
func (s *Strategy) aggregateSignal(ctx context.Context) (float64, error) {
|
||||||
sum := 0.0
|
sum := 0.0
|
||||||
voters := 0.0
|
voters := 0.0
|
||||||
for _, signal := range s.SignalConfigList {
|
for _, signal := range s.SignalConfigList {
|
||||||
|
@ -403,20 +418,99 @@ func (s *Strategy) calculateSignal(ctx context.Context) (float64, error) {
|
||||||
return sum / voters, nil
|
return sum / voters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) updateQuote(ctx context.Context) {
|
// getInitialLayerQuantity returns the initial quantity for the layer
|
||||||
|
// i is the layer index, starting from 0
|
||||||
|
func (s *Strategy) getInitialLayerQuantity(i int) (fixedpoint.Value, error) {
|
||||||
|
if s.QuantityScale != nil {
|
||||||
|
qf, err := s.QuantityScale.Scale(i + 1)
|
||||||
|
if err != nil {
|
||||||
|
return fixedpoint.Zero, fmt.Errorf("quantityScale error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%s scaling bid #%d quantity to %f", s.Symbol, i+1, qf)
|
||||||
|
|
||||||
|
// override the default quantity
|
||||||
|
return fixedpoint.NewFromFloat(qf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
q := s.Quantity
|
||||||
|
|
||||||
|
if s.QuantityMultiplier.Sign() > 0 && i > 0 {
|
||||||
|
q = fixedpoint.NewFromFloat(
|
||||||
|
q.Float64() * math.Pow(
|
||||||
|
s.QuantityMultiplier.Float64(), float64(i+1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to the fixed quantity
|
||||||
|
return q, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLayerPrice returns the price for the layer
|
||||||
|
// i is the layer index, starting from 0
|
||||||
|
// side is the side of the order
|
||||||
|
// sourceBook is the source order book
|
||||||
|
func (s *Strategy) getLayerPrice(
|
||||||
|
i int,
|
||||||
|
side types.SideType,
|
||||||
|
sourceBook *types.StreamOrderBook,
|
||||||
|
quote *Quote,
|
||||||
|
requiredDepth fixedpoint.Value,
|
||||||
|
) (price fixedpoint.Value) {
|
||||||
|
var margin, delta, pips fixedpoint.Value
|
||||||
|
|
||||||
|
switch side {
|
||||||
|
case types.SideTypeSell:
|
||||||
|
margin = quote.AskMargin
|
||||||
|
delta = margin
|
||||||
|
|
||||||
|
if quote.AskLayerPips.Sign() > 0 {
|
||||||
|
pips = quote.AskLayerPips
|
||||||
|
} else {
|
||||||
|
pips = fixedpoint.One
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
margin = quote.BidMargin
|
||||||
|
delta = margin.Neg()
|
||||||
|
|
||||||
|
if quote.BidLayerPips.Sign() > 0 {
|
||||||
|
pips = quote.BidLayerPips.Neg()
|
||||||
|
} else {
|
||||||
|
pips = fixedpoint.One.Neg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if requiredDepth.Sign() > 0 {
|
||||||
|
price = aggregatePrice(sourceBook.SideBook(side), requiredDepth)
|
||||||
|
price = price.Mul(fixedpoint.One.Add(delta))
|
||||||
|
if i > 0 {
|
||||||
|
price = price.Add(pips.Mul(s.makerMarket.TickSize))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
price = price.Mul(fixedpoint.One.Add(delta))
|
||||||
|
if i > 0 {
|
||||||
|
price = price.Add(pips.Mul(s.makerMarket.TickSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return price
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) updateQuote(ctx context.Context) error {
|
||||||
if err := s.activeMakerOrders.GracefulCancel(ctx, s.makerSession.Exchange); err != nil {
|
if err := s.activeMakerOrders.GracefulCancel(ctx, s.makerSession.Exchange); err != nil {
|
||||||
s.logger.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol)
|
s.logger.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol)
|
||||||
s.activeMakerOrders.Print()
|
s.activeMakerOrders.Print()
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.activeMakerOrders.NumOfOrders() > 0 {
|
if s.activeMakerOrders.NumOfOrders() > 0 {
|
||||||
return
|
s.logger.Warnf("unable to cancel all %s orders, skipping placing maker orders", s.Symbol)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
signal, err := s.calculateSignal(ctx)
|
signal, err := s.aggregateSignal(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Infof("aggregated signal: %f", signal)
|
s.logger.Infof("aggregated signal: %f", signal)
|
||||||
|
@ -431,14 +525,41 @@ func (s *Strategy) updateQuote(ctx context.Context) {
|
||||||
bbgo.Notify("Strategy %s is halted, reason: %s", ID, reason)
|
bbgo.Notify("Strategy %s is halted, reason: %s", ID, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bestBid, bestAsk, hasPrice := s.book.BestBidAndAsk()
|
bestBid, bestAsk, hasPrice := s.sourceBook.BestBidAndAsk()
|
||||||
if !hasPrice {
|
if !hasPrice {
|
||||||
s.logger.Warnf("no valid price, skip quoting")
|
s.logger.Warnf("no valid price, skip quoting")
|
||||||
return
|
return fmt.Errorf("no valid book price")
|
||||||
|
}
|
||||||
|
|
||||||
|
bestBidPrice := bestBid.Price
|
||||||
|
bestAskPrice := bestAsk.Price
|
||||||
|
s.logger.Infof("%s book ticker: best ask / best bid = %v / %v", s.Symbol, bestAskPrice, bestBidPrice)
|
||||||
|
|
||||||
|
if bestBidPrice.Compare(bestAskPrice) > 0 {
|
||||||
|
return fmt.Errorf("best bid price %f is higher than best ask price %f, skip quoting",
|
||||||
|
bestBidPrice.Float64(),
|
||||||
|
bestAskPrice.Float64(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.EnableArbitrage {
|
||||||
|
if makerBid, makerAsk, ok := s.makerBook.BestBidAndAsk(); ok {
|
||||||
|
if makerAsk.Price.Compare(bestBid.Price) <= 0 {
|
||||||
|
askPvs := s.makerBook.SideBook(types.SideTypeSell)
|
||||||
|
for _, pv := range askPvs {
|
||||||
|
if pv.Price.Compare(bestBid.Price) <= 0 {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// send ioc order for arbitrage
|
||||||
|
} else if makerBid.Price.Compare(bestAsk.Price) >= 0 {
|
||||||
|
// send ioc order for arbitrage
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// use mid-price for the last price
|
// use mid-price for the last price
|
||||||
|
@ -446,26 +567,26 @@ func (s *Strategy) updateQuote(ctx context.Context) {
|
||||||
|
|
||||||
s.priceSolver.Update(s.Symbol, s.lastPrice)
|
s.priceSolver.Update(s.Symbol, s.lastPrice)
|
||||||
|
|
||||||
bookLastUpdateTime := s.book.LastUpdateTime()
|
bookLastUpdateTime := s.sourceBook.LastUpdateTime()
|
||||||
|
|
||||||
if _, err := s.bidPriceHeartBeat.Update(bestBid); err != nil {
|
if _, err := s.bidPriceHeartBeat.Update(bestBid); err != nil {
|
||||||
s.logger.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago",
|
s.logger.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago",
|
||||||
s.Symbol,
|
s.Symbol,
|
||||||
time.Since(bookLastUpdateTime))
|
time.Since(bookLastUpdateTime))
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.askPriceHeartBeat.Update(bestAsk); err != nil {
|
if _, err := s.askPriceHeartBeat.Update(bestAsk); err != nil {
|
||||||
s.logger.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago",
|
s.logger.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago",
|
||||||
s.Symbol,
|
s.Symbol,
|
||||||
time.Since(bookLastUpdateTime))
|
time.Since(bookLastUpdateTime))
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceBook := s.book.CopyDepth(10)
|
sourceBook := s.sourceBook.CopyDepth(10)
|
||||||
if valid, err := sourceBook.IsValid(); !valid {
|
if valid, err := sourceBook.IsValid(); !valid {
|
||||||
s.logger.WithError(err).Errorf("%s invalid copied order book, skip quoting: %v", s.Symbol, err)
|
s.logger.WithError(err).Errorf("%s invalid copied order book, skip quoting: %v", s.Symbol, err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var disableMakerBid = false
|
var disableMakerBid = false
|
||||||
|
@ -636,25 +757,11 @@ func (s *Strategy) updateQuote(ctx context.Context) {
|
||||||
|
|
||||||
if disableMakerAsk && disableMakerBid {
|
if disableMakerAsk && disableMakerBid {
|
||||||
log.Warnf("%s bid/ask maker is disabled due to insufficient balances", s.Symbol)
|
log.Warnf("%s bid/ask maker is disabled due to insufficient balances", s.Symbol)
|
||||||
return
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
bestBidPrice := bestBid.Price
|
|
||||||
bestAskPrice := bestAsk.Price
|
|
||||||
s.logger.Infof("%s book ticker: best ask / best bid = %v / %v", s.Symbol, bestAskPrice, bestBidPrice)
|
|
||||||
|
|
||||||
if bestBidPrice.Compare(bestAskPrice) > 0 {
|
|
||||||
log.Errorf("best bid price %f is higher than best ask price %f, skip quoting",
|
|
||||||
bestBidPrice.Float64(),
|
|
||||||
bestAskPrice.Float64(),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var submitOrders []types.SubmitOrder
|
var submitOrders []types.SubmitOrder
|
||||||
var accumulativeBidQuantity, accumulativeAskQuantity fixedpoint.Value
|
var accumulativeBidQuantity, accumulativeAskQuantity fixedpoint.Value
|
||||||
var bidQuantity = s.Quantity
|
|
||||||
var askQuantity = s.Quantity
|
|
||||||
|
|
||||||
var quote = &Quote{
|
var quote = &Quote{
|
||||||
BestBidPrice: bestBidPrice,
|
BestBidPrice: bestBidPrice,
|
||||||
|
@ -681,58 +788,30 @@ func (s *Strategy) updateQuote(ctx context.Context) {
|
||||||
bidPrice := quote.BestBidPrice
|
bidPrice := quote.BestBidPrice
|
||||||
askPrice := quote.BestAskPrice
|
askPrice := quote.BestAskPrice
|
||||||
|
|
||||||
if bidPrice.Compare(askPrice) > 0 {
|
|
||||||
log.Errorf("maker bid price %f is higher than maker ask price %f, skip quoting",
|
|
||||||
bidPrice.Float64(),
|
|
||||||
askPrice.Float64(),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bidMarginMetrics.With(s.metricsLabels).Set(quote.BidMargin.Float64())
|
bidMarginMetrics.With(s.metricsLabels).Set(quote.BidMargin.Float64())
|
||||||
askMarginMetrics.With(s.metricsLabels).Set(quote.AskMargin.Float64())
|
askMarginMetrics.With(s.metricsLabels).Set(quote.AskMargin.Float64())
|
||||||
|
|
||||||
for i := 0; i < s.NumLayers; i++ {
|
|
||||||
// for maker bid orders
|
|
||||||
if !disableMakerBid {
|
if !disableMakerBid {
|
||||||
if s.QuantityScale != nil {
|
for i := 0; i < s.NumLayers; i++ {
|
||||||
qf, err := s.QuantityScale.Scale(i + 1)
|
bidQuantity, err := s.getInitialLayerQuantity(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Errorf("quantityScale error")
|
return err
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("%s scaling bid #%d quantity to %f", s.Symbol, i+1, qf)
|
|
||||||
|
|
||||||
// override the default bid quantity
|
|
||||||
bidQuantity = fixedpoint.NewFromFloat(qf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for maker bid orders
|
||||||
accumulativeBidQuantity = accumulativeBidQuantity.Add(bidQuantity)
|
accumulativeBidQuantity = accumulativeBidQuantity.Add(bidQuantity)
|
||||||
|
|
||||||
|
requiredDepth := fixedpoint.Zero
|
||||||
if s.UseDepthPrice {
|
if s.UseDepthPrice {
|
||||||
sideBook := sourceBook.SideBook(types.SideTypeBuy)
|
|
||||||
if s.DepthQuantity.Sign() > 0 {
|
if s.DepthQuantity.Sign() > 0 {
|
||||||
if i == 0 {
|
requiredDepth = s.DepthQuantity
|
||||||
bidPrice = aggregatePrice(sideBook, s.DepthQuantity)
|
|
||||||
bidPrice = bidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin))
|
|
||||||
} else if i > 0 && quote.BidLayerPips.Sign() > 0 {
|
|
||||||
pips := quote.BidLayerPips.Mul(s.makerMarket.TickSize)
|
|
||||||
bidPrice = bidPrice.Sub(pips)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
bidPrice = aggregatePrice(sideBook, accumulativeBidQuantity)
|
requiredDepth = accumulativeBidQuantity
|
||||||
bidPrice = bidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if i == 0 {
|
|
||||||
bidPrice = bidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin))
|
|
||||||
} else if i > 0 && quote.BidLayerPips.Sign() > 0 {
|
|
||||||
pips := quote.BidLayerPips.Mul(s.makerMarket.TickSize)
|
|
||||||
bidPrice = bidPrice.Sub(pips)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bidPrice = s.getLayerPrice(i, types.SideTypeBuy, s.sourceBook, quote, requiredDepth)
|
||||||
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
s.logger.Infof("maker best bid price %f", bidPrice.Float64())
|
s.logger.Infof("maker best bid price %f", bidPrice.Float64())
|
||||||
makerBestBidPriceMetrics.With(s.metricsLabels).Set(bidPrice.Float64())
|
makerBestBidPriceMetrics.With(s.metricsLabels).Set(bidPrice.Float64())
|
||||||
|
@ -758,49 +837,30 @@ func (s *Strategy) updateQuote(ctx context.Context) {
|
||||||
hedgeQuota.Rollback()
|
hedgeQuota.Rollback()
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.QuantityMultiplier.Sign() > 0 {
|
|
||||||
bidQuantity = bidQuantity.Mul(s.QuantityMultiplier)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for maker ask orders
|
// for maker ask orders
|
||||||
if !disableMakerAsk {
|
if !disableMakerAsk {
|
||||||
if s.QuantityScale != nil {
|
for i := 0; i < s.NumLayers; i++ {
|
||||||
qf, err := s.QuantityScale.Scale(i + 1)
|
askQuantity, err := s.getInitialLayerQuantity(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Errorf("quantityScale error")
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("%s scaling ask #%d quantity to %f", s.Symbol, i+1, qf)
|
|
||||||
|
|
||||||
// override the default bid quantity
|
|
||||||
askQuantity = fixedpoint.NewFromFloat(qf)
|
|
||||||
}
|
|
||||||
accumulativeAskQuantity = accumulativeAskQuantity.Add(askQuantity)
|
accumulativeAskQuantity = accumulativeAskQuantity.Add(askQuantity)
|
||||||
|
|
||||||
|
requiredDepth := fixedpoint.Zero
|
||||||
if s.UseDepthPrice {
|
if s.UseDepthPrice {
|
||||||
if s.DepthQuantity.Sign() > 0 {
|
if s.DepthQuantity.Sign() > 0 {
|
||||||
if i == 0 {
|
requiredDepth = s.DepthQuantity
|
||||||
askPrice = aggregatePrice(sourceBook.SideBook(types.SideTypeSell), s.DepthQuantity)
|
|
||||||
askPrice = askPrice.Mul(fixedpoint.One.Add(quote.AskMargin))
|
|
||||||
} else if i > 0 && quote.AskLayerPips.Sign() > 0 {
|
|
||||||
pips := quote.AskLayerPips.Mul(s.makerMarket.TickSize)
|
|
||||||
askPrice = askPrice.Add(pips)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
askPrice = aggregatePrice(sourceBook.SideBook(types.SideTypeSell), accumulativeAskQuantity)
|
requiredDepth = accumulativeAskQuantity
|
||||||
askPrice = askPrice.Mul(fixedpoint.One.Add(quote.AskMargin))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if i == 0 {
|
|
||||||
askPrice = askPrice.Mul(fixedpoint.One.Add(quote.AskMargin))
|
|
||||||
} else if i > 0 && quote.AskLayerPips.Sign() > 0 {
|
|
||||||
pips := quote.AskLayerPips.Mul(s.makerMarket.TickSize)
|
|
||||||
askPrice = askPrice.Add(pips)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
askPrice = s.getLayerPrice(i, types.SideTypeSell, s.sourceBook, quote, requiredDepth)
|
||||||
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
s.logger.Infof("maker best ask price %f", askPrice.Float64())
|
s.logger.Infof("maker best ask price %f", askPrice.Float64())
|
||||||
makerBestAskPriceMetrics.With(s.metricsLabels).Set(askPrice.Float64())
|
makerBestAskPriceMetrics.With(s.metricsLabels).Set(askPrice.Float64())
|
||||||
|
@ -836,12 +896,12 @@ func (s *Strategy) updateQuote(ctx context.Context) {
|
||||||
|
|
||||||
if len(submitOrders) == 0 {
|
if len(submitOrders) == 0 {
|
||||||
log.Warnf("no orders generated")
|
log.Warnf("no orders generated")
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedOrders, err := s.makerSession.FormatOrders(submitOrders)
|
formattedOrders, err := s.makerSession.FormatOrders(submitOrders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
orderCreateCallback := func(createdOrder types.Order) {
|
orderCreateCallback := func(createdOrder types.Order) {
|
||||||
|
@ -854,7 +914,7 @@ func (s *Strategy) updateQuote(ctx context.Context) {
|
||||||
createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.makerSession.Exchange, orderCreateCallback, formattedOrders...)
|
createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.makerSession.Exchange, orderCreateCallback, formattedOrders...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Errorf("unable to place maker orders: %+v", formattedOrders)
|
log.WithError(err).Errorf("unable to place maker orders: %+v", formattedOrders)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
openOrderBidExposureInUsdMetrics.With(s.metricsLabels).Set(bidExposureInUsd.Float64())
|
openOrderBidExposureInUsdMetrics.With(s.metricsLabels).Set(bidExposureInUsd.Float64())
|
||||||
|
@ -862,6 +922,7 @@ func (s *Strategy) updateQuote(ctx context.Context) {
|
||||||
|
|
||||||
_ = errIdx
|
_ = errIdx
|
||||||
_ = createdOrders
|
_ = createdOrders
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) adjustHedgeQuantityWithAvailableBalance(
|
func (s *Strategy) adjustHedgeQuantityWithAvailableBalance(
|
||||||
|
@ -906,7 +967,7 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
lastPrice := s.lastPrice
|
lastPrice := s.lastPrice
|
||||||
sourceBook := s.book.CopyDepth(1)
|
sourceBook := s.sourceBook.CopyDepth(1)
|
||||||
switch side {
|
switch side {
|
||||||
|
|
||||||
case types.SideTypeBuy:
|
case types.SideTypeBuy:
|
||||||
|
@ -1029,6 +1090,10 @@ func (s *Strategy) Defaults() error {
|
||||||
s.BollBandInterval = types.Interval1m
|
s.BollBandInterval = types.Interval1m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.SourceDepthLevel == "" {
|
||||||
|
s.SourceDepthLevel = types.DepthLevelMedium
|
||||||
|
}
|
||||||
|
|
||||||
if s.BollBandMarginFactor.IsZero() {
|
if s.BollBandMarginFactor.IsZero() {
|
||||||
s.BollBandMarginFactor = fixedpoint.One
|
s.BollBandMarginFactor = fixedpoint.One
|
||||||
}
|
}
|
||||||
|
@ -1085,7 +1150,7 @@ func (s *Strategy) Defaults() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) Validate() error {
|
func (s *Strategy) Validate() error {
|
||||||
if s.Quantity.IsZero() || s.QuantityScale == nil {
|
if s.Quantity.IsZero() && s.QuantityScale == nil {
|
||||||
return errors.New("quantity or quantityScale can not be empty")
|
return errors.New("quantity or quantityScale can not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1350,8 +1415,11 @@ func (s *Strategy) CrossRun(
|
||||||
s.ProfitStats.ProfitStats = profitStats
|
s.ProfitStats.ProfitStats = profitStats
|
||||||
}
|
}
|
||||||
|
|
||||||
s.book = types.NewStreamBook(s.Symbol, s.sourceSession.ExchangeName)
|
s.makerBook = types.NewStreamBook(s.Symbol, s.makerSession.ExchangeName)
|
||||||
s.book.BindStream(s.sourceSession.MarketDataStream)
|
s.makerBook.BindStream(s.makerSession.MarketDataStream)
|
||||||
|
|
||||||
|
s.sourceBook = types.NewStreamBook(s.Symbol, s.sourceSession.ExchangeName)
|
||||||
|
s.sourceBook.BindStream(s.sourceSession.MarketDataStream)
|
||||||
|
|
||||||
if s.EnableSignalMargin {
|
if s.EnableSignalMargin {
|
||||||
scale, err := s.SignalMarginScale.Scale()
|
scale, err := s.SignalMarginScale.Scale()
|
||||||
|
@ -1365,7 +1433,7 @@ func (s *Strategy) CrossRun(
|
||||||
|
|
||||||
for _, signalConfig := range s.SignalConfigList {
|
for _, signalConfig := range s.SignalConfigList {
|
||||||
if signalConfig.OrderBookBestPriceSignal != nil {
|
if signalConfig.OrderBookBestPriceSignal != nil {
|
||||||
signalConfig.OrderBookBestPriceSignal.book = s.book
|
signalConfig.OrderBookBestPriceSignal.book = s.sourceBook
|
||||||
if err := signalConfig.OrderBookBestPriceSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil {
|
if err := signalConfig.OrderBookBestPriceSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,28 +2,89 @@ package xmaker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
. "github.com/c9s/bbgo/pkg/testing/testhelper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_aggregatePrice(t *testing.T) {
|
func TestStrategy_getLayerPrice(t *testing.T) {
|
||||||
bids := types.PriceVolumeSlice{
|
symbol := "BTCUSDT"
|
||||||
{
|
market := Market(symbol)
|
||||||
Price: fixedpoint.NewFromFloat(1000.0),
|
|
||||||
Volume: fixedpoint.NewFromFloat(1.0),
|
s := &Strategy{
|
||||||
},
|
UseDepthPrice: true,
|
||||||
{
|
DepthQuantity: Number(3.0),
|
||||||
Price: fixedpoint.NewFromFloat(1200.0),
|
makerMarket: market,
|
||||||
Volume: fixedpoint.NewFromFloat(1.0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Price: fixedpoint.NewFromFloat(1400.0),
|
|
||||||
Volume: fixedpoint.NewFromFloat(1.0),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceBook := types.NewStreamBook(symbol, types.ExchangeBinance)
|
||||||
|
sourceBook.Load(types.SliceOrderBook{
|
||||||
|
Symbol: symbol,
|
||||||
|
Bids: PriceVolumeSlice(
|
||||||
|
Number(1300.0), Number(1.0),
|
||||||
|
Number(1200.0), Number(2.0),
|
||||||
|
Number(1100.0), Number(3.0),
|
||||||
|
),
|
||||||
|
Asks: PriceVolumeSlice(
|
||||||
|
Number(1301.0), Number(1.0),
|
||||||
|
Number(1400.0), Number(2.0),
|
||||||
|
Number(1500.0), Number(3.0),
|
||||||
|
),
|
||||||
|
Time: time.Time{},
|
||||||
|
LastUpdateId: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
quote := &Quote{
|
||||||
|
BestBidPrice: Number(1300.0),
|
||||||
|
BestAskPrice: Number(1301.0),
|
||||||
|
BidMargin: Number(0.001),
|
||||||
|
AskMargin: Number(0.001),
|
||||||
|
BidLayerPips: Number(100.0),
|
||||||
|
AskLayerPips: Number(100.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("depthPrice bid price at 0", func(t *testing.T) {
|
||||||
|
price := s.getLayerPrice(0, types.SideTypeBuy, sourceBook, quote, s.DepthQuantity)
|
||||||
|
|
||||||
|
// (1300 + 1200*2)/3 * (1 - 0.001)
|
||||||
|
assert.InDelta(t, 1232.10, price.Float64(), 0.01)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("depthPrice bid price at 1", func(t *testing.T) {
|
||||||
|
price := s.getLayerPrice(1, types.SideTypeBuy, sourceBook, quote, s.DepthQuantity)
|
||||||
|
|
||||||
|
// (1300 + 1200*2)/3 * (1 - 0.001) - 100 * 0.01
|
||||||
|
assert.InDelta(t, 1231.10, price.Float64(), 0.01)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("depthPrice ask price at 0", func(t *testing.T) {
|
||||||
|
price := s.getLayerPrice(0, types.SideTypeSell, sourceBook, quote, s.DepthQuantity)
|
||||||
|
|
||||||
|
// (1301 + 1400*2)/3 * (1 + 0.001)
|
||||||
|
assert.InDelta(t, 1368.367, price.Float64(), 0.01)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("depthPrice ask price at 1", func(t *testing.T) {
|
||||||
|
price := s.getLayerPrice(1, types.SideTypeSell, sourceBook, quote, s.DepthQuantity)
|
||||||
|
|
||||||
|
// (1301 + 1400*2)/3 * (1 + 0.001) + 100 * 0.01
|
||||||
|
assert.InDelta(t, 1369.367, price.Float64(), 0.01)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_aggregatePrice(t *testing.T) {
|
||||||
|
bids := PriceVolumeSliceFromText(`
|
||||||
|
1000.0, 1.0
|
||||||
|
1200.0, 1.0
|
||||||
|
1400.0, 1.0
|
||||||
|
`)
|
||||||
|
|
||||||
aggregatedPrice1 := aggregatePrice(bids, fixedpoint.NewFromFloat(0.5))
|
aggregatedPrice1 := aggregatePrice(bids, fixedpoint.NewFromFloat(0.5))
|
||||||
assert.Equal(t, fixedpoint.NewFromFloat(1000.0), aggregatedPrice1)
|
assert.Equal(t, fixedpoint.NewFromFloat(1000.0), aggregatedPrice1)
|
||||||
|
|
||||||
|
|
43
pkg/testing/testhelper/market.go
Normal file
43
pkg/testing/testhelper/market.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package testhelper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var markets = map[string]types.Market{
|
||||||
|
"BTCUSDT": {
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
PricePrecision: 2,
|
||||||
|
VolumePrecision: 8,
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
BaseCurrency: "BTC",
|
||||||
|
MinNotional: fixedpoint.MustNewFromString("0.001"),
|
||||||
|
MinAmount: fixedpoint.MustNewFromString("10.0"),
|
||||||
|
MinQuantity: fixedpoint.MustNewFromString("0.001"),
|
||||||
|
TickSize: fixedpoint.MustNewFromString("0.01"),
|
||||||
|
},
|
||||||
|
|
||||||
|
"ETHUSDT": {
|
||||||
|
Symbol: "ETH",
|
||||||
|
PricePrecision: 2,
|
||||||
|
VolumePrecision: 8,
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
BaseCurrency: "ETH",
|
||||||
|
MinNotional: fixedpoint.MustNewFromString("0.005"),
|
||||||
|
MinAmount: fixedpoint.MustNewFromString("10.0"),
|
||||||
|
MinQuantity: fixedpoint.MustNewFromString("0.001"),
|
||||||
|
TickSize: fixedpoint.MustNewFromString("0.01"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func Market(symbol string) types.Market {
|
||||||
|
market, ok := markets[symbol]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Errorf("%s market not found, valid markets: %+v", symbol, markets))
|
||||||
|
}
|
||||||
|
|
||||||
|
return market
|
||||||
|
}
|
49
pkg/testing/testhelper/pricevolumeslice.go
Normal file
49
pkg/testing/testhelper/pricevolumeslice.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package testhelper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PriceVolumeSliceFromText(str string) (slice types.PriceVolumeSlice) {
|
||||||
|
lines := strings.Split(str, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cols := strings.SplitN(line, ",", 2)
|
||||||
|
if len(cols) < 2 {
|
||||||
|
panic(fmt.Errorf("column length should be 2, got %d", len(cols)))
|
||||||
|
}
|
||||||
|
|
||||||
|
price := fixedpoint.MustNewFromString(strings.TrimSpace(cols[0]))
|
||||||
|
volume := fixedpoint.MustNewFromString(strings.TrimSpace(cols[1]))
|
||||||
|
slice = append(slice, types.PriceVolume{
|
||||||
|
Price: price,
|
||||||
|
Volume: volume,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
|
||||||
|
func PriceVolumeSlice(values ...fixedpoint.Value) (slice types.PriceVolumeSlice) {
|
||||||
|
if len(values)%2 != 0 {
|
||||||
|
panic("values should be paired")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(values); i += 2 {
|
||||||
|
slice = append(slice, types.PriceVolume{
|
||||||
|
Price: values[i],
|
||||||
|
Volume: values[i+1],
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return slice
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user