Compare commits

..

11 Commits

Author SHA1 Message Date
c9s
bc1715f8ad
Merge pull request #1736 from c9s/kbearXD/session/remove-log
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
MINOR: [session] remove environment nil validation log
2024-09-09 16:17:17 +08:00
c9s
f361b19564
Merge pull request #1734 from c9s/c9s/xmaker/ioc-arb
REFACTOR: [xmaker] refactor for supporting ioc arb [part1]
2024-09-09 16:05:11 +08:00
kbearXD
f44486447e MINOR: [session] remove environment nil validation log 2024-09-09 16:04:04 +08:00
kbearXD
a2eca66af5
Merge pull request #1735 from c9s/kbearXD/dca2/fix-memory-leak
FIX: configure environment
2024-09-09 15:50:24 +08:00
kbearXD
129e2c438e FIX: add debug log 2024-09-09 15:13:02 +08:00
c9s
90749f4873
xmaker: pull out s.UseDepthPrice dependency 2024-09-09 15:04:56 +08:00
c9s
77dfe213e5
xmaker: pull out getLayerPrice and add test against the method 2024-09-09 14:41:41 +08:00
c9s
960ea89d8c
testhelper: add more test helpers 2024-09-09 14:41:27 +08:00
c9s
f24a96c8c3
xmaker: refactor getInitialLayerQuantity for quantity multiplier 2024-09-07 14:19:07 +08:00
c9s
6ad16b7488
xmaker: add EnableArbitrage option and makerBook 2024-09-07 13:47:34 +08:00
c9s
e14f09a914
xmaker: add sourceDepthLevel option 2024-09-06 21:47:43 +08:00
6 changed files with 361 additions and 132 deletions

View File

@ -405,6 +405,8 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ
return fmt.Errorf("market %s is not defined", symbol)
}
session.logger.Infof("environment config: %+v", environ.environmentConfig)
disableMarketDataStore := environ.environmentConfig != nil && environ.environmentConfig.DisableMarketDataStore
disableSessionTradeBuffer := environ.environmentConfig != nil && environ.environmentConfig.DisableSessionTradeBuffer
maxSessionTradeBufferSize := 0

View File

@ -122,6 +122,12 @@ func (trader *Trader) DisableLogging() {
}
func (trader *Trader) Configure(userConfig *Config) error {
// config environment
if userConfig.Environment != nil && trader.environment != nil {
trader.environment.environmentConfig = userConfig.Environment
}
// config risk control
if userConfig.RiskControls != nil {
trader.SetRiskControls(userConfig.RiskControls)
}

View File

@ -91,11 +91,12 @@ type Strategy struct {
SignalConfigList []SignalConfig `json:"signals"`
SignalMarginScale *bbgo.SlideRule `json:"signalMarginScale,omitempty"`
Margin fixedpoint.Value `json:"margin"`
BidMargin fixedpoint.Value `json:"bidMargin"`
AskMargin fixedpoint.Value `json:"askMargin"`
UseDepthPrice bool `json:"useDepthPrice"`
DepthQuantity fixedpoint.Value `json:"depthQuantity"`
Margin fixedpoint.Value `json:"margin"`
BidMargin fixedpoint.Value `json:"bidMargin"`
AskMargin fixedpoint.Value `json:"askMargin"`
UseDepthPrice bool `json:"useDepthPrice"`
DepthQuantity fixedpoint.Value `json:"depthQuantity"`
SourceDepthLevel types.Depth `json:"sourceDepthLevel"`
EnableBollBandMargin bool `json:"enableBollBandMargin"`
BollBandInterval types.Interval `json:"bollBandInterval"`
@ -126,6 +127,8 @@ type Strategy struct {
NotifyTrade bool `json:"notifyTrade"`
EnableArbitrage bool `json:"arbitrage"`
// RecoverTrade tries to find the missing trades via the REStful API
RecoverTrade bool `json:"recoverTrade"`
@ -159,8 +162,8 @@ type Strategy struct {
ProfitStats *ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty" persistence:"covered_position"`
book *types.StreamOrderBook
activeMakerOrders *bbgo.ActiveOrderBook
sourceBook, makerBook *types.StreamOrderBook
activeMakerOrders *bbgo.ActiveOrderBook
hedgeErrorLimiter *rate.Limiter
hedgeErrorRateReservation *rate.Reservation
@ -199,7 +202,11 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {
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"})
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"})
if s.EnableArbitrage {
makerSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{
Depth: types.DepthLevelMedium,
})
}
for _, sig := range s.SignalConfigList {
if sig.TradeVolumeWindowSignal != nil {
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 {
signal, err := s.calculateSignal(ctx)
signal, err := s.aggregateSignal(ctx)
if err != nil {
return err
}
@ -367,7 +382,7 @@ func (s *Strategy) applyBollingerMargin(
return nil
}
func (s *Strategy) calculateSignal(ctx context.Context) (float64, error) {
func (s *Strategy) aggregateSignal(ctx context.Context) (float64, error) {
sum := 0.0
voters := 0.0
for _, signal := range s.SignalConfigList {
@ -403,20 +418,99 @@ func (s *Strategy) calculateSignal(ctx context.Context) (float64, error) {
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 {
s.logger.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol)
s.activeMakerOrders.Print()
return
return nil
}
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 {
return
return err
}
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)
}
return
return nil
}
}
bestBid, bestAsk, hasPrice := s.book.BestBidAndAsk()
bestBid, bestAsk, hasPrice := s.sourceBook.BestBidAndAsk()
if !hasPrice {
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
@ -446,26 +567,26 @@ func (s *Strategy) updateQuote(ctx context.Context) {
s.priceSolver.Update(s.Symbol, s.lastPrice)
bookLastUpdateTime := s.book.LastUpdateTime()
bookLastUpdateTime := s.sourceBook.LastUpdateTime()
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.Symbol,
time.Since(bookLastUpdateTime))
return
return err
}
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.Symbol,
time.Since(bookLastUpdateTime))
return
return err
}
sourceBook := s.book.CopyDepth(10)
sourceBook := s.sourceBook.CopyDepth(10)
if valid, err := sourceBook.IsValid(); !valid {
s.logger.WithError(err).Errorf("%s invalid copied order book, skip quoting: %v", s.Symbol, err)
return
return err
}
var disableMakerBid = false
@ -636,25 +757,11 @@ func (s *Strategy) updateQuote(ctx context.Context) {
if disableMakerAsk && disableMakerBid {
log.Warnf("%s bid/ask maker is disabled due to insufficient balances", s.Symbol)
return
}
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
return nil
}
var submitOrders []types.SubmitOrder
var accumulativeBidQuantity, accumulativeAskQuantity fixedpoint.Value
var bidQuantity = s.Quantity
var askQuantity = s.Quantity
var quote = &Quote{
BestBidPrice: bestBidPrice,
@ -681,58 +788,30 @@ func (s *Strategy) updateQuote(ctx context.Context) {
bidPrice := quote.BestBidPrice
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())
askMarginMetrics.With(s.metricsLabels).Set(quote.AskMargin.Float64())
for i := 0; i < s.NumLayers; i++ {
// for maker bid orders
if !disableMakerBid {
if s.QuantityScale != nil {
qf, err := s.QuantityScale.Scale(i + 1)
if err != nil {
log.WithError(err).Errorf("quantityScale error")
return
}
log.Infof("%s scaling bid #%d quantity to %f", s.Symbol, i+1, qf)
// override the default bid quantity
bidQuantity = fixedpoint.NewFromFloat(qf)
if !disableMakerBid {
for i := 0; i < s.NumLayers; i++ {
bidQuantity, err := s.getInitialLayerQuantity(i)
if err != nil {
return err
}
// for maker bid orders
accumulativeBidQuantity = accumulativeBidQuantity.Add(bidQuantity)
requiredDepth := fixedpoint.Zero
if s.UseDepthPrice {
sideBook := sourceBook.SideBook(types.SideTypeBuy)
if s.DepthQuantity.Sign() > 0 {
if i == 0 {
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)
}
requiredDepth = s.DepthQuantity
} else {
bidPrice = aggregatePrice(sideBook, 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)
requiredDepth = accumulativeBidQuantity
}
}
bidPrice = s.getLayerPrice(i, types.SideTypeBuy, s.sourceBook, quote, requiredDepth)
if i == 0 {
s.logger.Infof("maker best bid price %f", bidPrice.Float64())
makerBestBidPriceMetrics.With(s.metricsLabels).Set(bidPrice.Float64())
@ -758,49 +837,30 @@ func (s *Strategy) updateQuote(ctx context.Context) {
hedgeQuota.Rollback()
}
if s.QuantityMultiplier.Sign() > 0 {
bidQuantity = bidQuantity.Mul(s.QuantityMultiplier)
}
}
}
// for maker ask orders
if !disableMakerAsk {
if s.QuantityScale != nil {
qf, err := s.QuantityScale.Scale(i + 1)
if err != nil {
log.WithError(err).Errorf("quantityScale error")
return
}
log.Infof("%s scaling ask #%d quantity to %f", s.Symbol, i+1, qf)
// override the default bid quantity
askQuantity = fixedpoint.NewFromFloat(qf)
// for maker ask orders
if !disableMakerAsk {
for i := 0; i < s.NumLayers; i++ {
askQuantity, err := s.getInitialLayerQuantity(i)
if err != nil {
return err
}
accumulativeAskQuantity = accumulativeAskQuantity.Add(askQuantity)
requiredDepth := fixedpoint.Zero
if s.UseDepthPrice {
if s.DepthQuantity.Sign() > 0 {
if i == 0 {
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)
}
requiredDepth = s.DepthQuantity
} else {
askPrice = aggregatePrice(sourceBook.SideBook(types.SideTypeSell), 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)
requiredDepth = accumulativeAskQuantity
}
}
askPrice = s.getLayerPrice(i, types.SideTypeSell, s.sourceBook, quote, requiredDepth)
if i == 0 {
s.logger.Infof("maker best ask price %f", askPrice.Float64())
makerBestAskPriceMetrics.With(s.metricsLabels).Set(askPrice.Float64())
@ -836,12 +896,12 @@ func (s *Strategy) updateQuote(ctx context.Context) {
if len(submitOrders) == 0 {
log.Warnf("no orders generated")
return
return nil
}
formattedOrders, err := s.makerSession.FormatOrders(submitOrders)
if err != nil {
return
return err
}
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...)
if err != nil {
log.WithError(err).Errorf("unable to place maker orders: %+v", formattedOrders)
return
return err
}
openOrderBidExposureInUsdMetrics.With(s.metricsLabels).Set(bidExposureInUsd.Float64())
@ -862,6 +922,7 @@ func (s *Strategy) updateQuote(ctx context.Context) {
_ = errIdx
_ = createdOrders
return nil
}
func (s *Strategy) adjustHedgeQuantityWithAvailableBalance(
@ -906,7 +967,7 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
}
lastPrice := s.lastPrice
sourceBook := s.book.CopyDepth(1)
sourceBook := s.sourceBook.CopyDepth(1)
switch side {
case types.SideTypeBuy:
@ -1029,6 +1090,10 @@ func (s *Strategy) Defaults() error {
s.BollBandInterval = types.Interval1m
}
if s.SourceDepthLevel == "" {
s.SourceDepthLevel = types.DepthLevelMedium
}
if s.BollBandMarginFactor.IsZero() {
s.BollBandMarginFactor = fixedpoint.One
}
@ -1085,7 +1150,7 @@ func (s *Strategy) Defaults() 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")
}
@ -1350,8 +1415,11 @@ func (s *Strategy) CrossRun(
s.ProfitStats.ProfitStats = profitStats
}
s.book = types.NewStreamBook(s.Symbol, s.sourceSession.ExchangeName)
s.book.BindStream(s.sourceSession.MarketDataStream)
s.makerBook = types.NewStreamBook(s.Symbol, s.makerSession.ExchangeName)
s.makerBook.BindStream(s.makerSession.MarketDataStream)
s.sourceBook = types.NewStreamBook(s.Symbol, s.sourceSession.ExchangeName)
s.sourceBook.BindStream(s.sourceSession.MarketDataStream)
if s.EnableSignalMargin {
scale, err := s.SignalMarginScale.Scale()
@ -1365,7 +1433,7 @@ func (s *Strategy) CrossRun(
for _, signalConfig := range s.SignalConfigList {
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 {
return err
}

View File

@ -2,28 +2,89 @@ package xmaker
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert"
. "github.com/c9s/bbgo/pkg/testing/testhelper"
)
func Test_aggregatePrice(t *testing.T) {
bids := types.PriceVolumeSlice{
{
Price: fixedpoint.NewFromFloat(1000.0),
Volume: fixedpoint.NewFromFloat(1.0),
},
{
Price: fixedpoint.NewFromFloat(1200.0),
Volume: fixedpoint.NewFromFloat(1.0),
},
{
Price: fixedpoint.NewFromFloat(1400.0),
Volume: fixedpoint.NewFromFloat(1.0),
},
func TestStrategy_getLayerPrice(t *testing.T) {
symbol := "BTCUSDT"
market := Market(symbol)
s := &Strategy{
UseDepthPrice: true,
DepthQuantity: Number(3.0),
makerMarket: market,
}
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))
assert.Equal(t, fixedpoint.NewFromFloat(1000.0), aggregatedPrice1)

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

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